mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-01-30 17:14:51 +01:00
feat: [WP] restore the content compose via SwiftUI and support expandable reply view for compose scene
This commit is contained in:
parent
02e3ad9a16
commit
4367e8eaba
@ -31,7 +31,11 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||
let logger = Logger(subsystem: "ComposeViewController", category: "logic")
|
||||
|
||||
lazy var composeContentViewModel: ComposeContentViewModel = {
|
||||
return ComposeContentViewModel(context: context, kind: viewModel.kind)
|
||||
return ComposeContentViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
kind: viewModel.kind
|
||||
)
|
||||
}()
|
||||
private(set) lazy var composeContentViewController: ComposeContentViewController = {
|
||||
let composeContentViewController = ComposeContentViewController()
|
||||
@ -39,20 +43,20 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||
return composeContentViewController
|
||||
}()
|
||||
|
||||
// private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
||||
// let characterCountLabel: UILabel = {
|
||||
// let label = UILabel()
|
||||
// label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
// label.text = "500"
|
||||
// label.textColor = Asset.Colors.Label.secondary.color
|
||||
// label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500)
|
||||
// return label
|
||||
// }()
|
||||
// private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = {
|
||||
// let barButtonItem = UIBarButtonItem(customView: characterCountLabel)
|
||||
// return barButtonItem
|
||||
// }()
|
||||
//
|
||||
private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
||||
let characterCountLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
label.text = "500"
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500)
|
||||
return label
|
||||
}()
|
||||
private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(customView: characterCountLabel)
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
// let publishButton: UIButton = {
|
||||
// let button = RoundedEdgesButton(type: .custom)
|
||||
// button.cornerRadius = 10
|
||||
@ -83,23 +87,6 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||
// publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
||||
// publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
||||
// }
|
||||
|
||||
// let scrollView: UIScrollView = {
|
||||
// let scrollView = UIScrollView()
|
||||
// scrollView.alwaysBounceVertical = true
|
||||
// return scrollView
|
||||
// }()
|
||||
|
||||
// let tableView: ComposeTableView = {
|
||||
// let tableView = ComposeTableView()
|
||||
// tableView.register(ComposeRepliedToStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self))
|
||||
// tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self))
|
||||
// tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self))
|
||||
// tableView.alwaysBounceVertical = true
|
||||
// tableView.separatorStyle = .none
|
||||
// tableView.tableFooterView = UIView()
|
||||
// return tableView
|
||||
// }()
|
||||
|
||||
// var systemKeyboardHeight: CGFloat = .zero {
|
||||
// didSet {
|
||||
@ -177,6 +164,22 @@ extension ComposeViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.leftBarButtonItem = cancelBarButtonItem
|
||||
// navigationItem.rightBarButtonItem = publishBarButtonItem
|
||||
// viewModel.traitCollectionDidChangePublisher
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] _ in
|
||||
// guard let self = self else { return }
|
||||
// guard self.traitCollection.userInterfaceIdiom == .pad else { return }
|
||||
// var items = [self.publishBarButtonItem]
|
||||
// if self.traitCollection.horizontalSizeClass == .regular {
|
||||
// items.append(self.characterCountBarButtonItem)
|
||||
// }
|
||||
// self.navigationItem.rightBarButtonItems = items
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||
|
||||
addChild(composeContentViewController)
|
||||
composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(composeContentViewController.view)
|
||||
@ -212,21 +215,6 @@ extension ComposeViewController {
|
||||
// self.setupBackgroundColor(theme: theme)
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// navigationItem.leftBarButtonItem = cancelBarButtonItem
|
||||
// navigationItem.rightBarButtonItem = publishBarButtonItem
|
||||
// viewModel.traitCollectionDidChangePublisher
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] _ in
|
||||
// guard let self = self else { return }
|
||||
// guard self.traitCollection.userInterfaceIdiom == .pad else { return }
|
||||
// var items = [self.publishBarButtonItem]
|
||||
// if self.traitCollection.horizontalSizeClass == .regular {
|
||||
// items.append(self.characterCountBarButtonItem)
|
||||
// }
|
||||
// self.navigationItem.rightBarButtonItems = items
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||
//
|
||||
//
|
||||
// scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
@ -533,26 +521,6 @@ extension ComposeViewController {
|
||||
// })
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// // setup snap behavior
|
||||
// Publishers.CombineLatest(
|
||||
// viewModel.$repliedToCellFrame,
|
||||
// viewModel.$collectionViewState
|
||||
// )
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] repliedToCellFrame, collectionViewState in
|
||||
// guard let self = self else { return }
|
||||
// guard repliedToCellFrame != .zero else { return }
|
||||
// switch collectionViewState {
|
||||
// case .fold:
|
||||
// self.tableView.contentInset.top = -repliedToCellFrame.height
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: set contentInset.top: -%s", ((#file as NSString).lastPathComponent), #line, #function, repliedToCellFrame.height.description)
|
||||
//
|
||||
// case .expand:
|
||||
// self.tableView.contentInset.top = 0
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value)
|
||||
// Publishers.CombineLatest(
|
||||
// keyboardHasShortcutBar,
|
||||
@ -746,17 +714,17 @@ extension ComposeViewController {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//extension ComposeViewController {
|
||||
//
|
||||
// @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
extension ComposeViewController {
|
||||
|
||||
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
// guard viewModel.shouldDismiss else {
|
||||
// showDismissConfirmAlertController()
|
||||
// return
|
||||
// }
|
||||
// dismiss(animated: true, completion: nil)
|
||||
// }
|
||||
//
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
// do {
|
||||
@ -779,9 +747,9 @@ extension ComposeViewController {
|
||||
//
|
||||
// dismiss(animated: true, completion: nil)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
|
||||
}
|
||||
|
||||
//// MARK: - MetaTextDelegate
|
||||
//extension ComposeViewController: MetaTextDelegate {
|
||||
// func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
|
||||
@ -1020,58 +988,7 @@ extension ComposeViewController {
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//// MARK: - UIScrollViewDelegate
|
||||
//extension ComposeViewController {
|
||||
//// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
//// guard scrollView === tableView else { return }
|
||||
////
|
||||
//// let repliedToCellFrame = viewModel.repliedToCellFrame
|
||||
//// guard repliedToCellFrame != .zero else { return }
|
||||
////
|
||||
//// // try to find some patterns:
|
||||
//// // print("""
|
||||
//// // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height)
|
||||
//// // scrollView.contentOffset.y: \(scrollView.contentOffset.y)
|
||||
//// // scrollView.contentSize.height: \(scrollView.contentSize.height)
|
||||
//// // scrollView.frame: \(scrollView.frame)
|
||||
//// // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top)
|
||||
//// // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom)
|
||||
//// // """)
|
||||
////
|
||||
//// switch viewModel.collectionViewState {
|
||||
//// case .fold:
|
||||
//// os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
//// guard velocity.y < 0 else { return }
|
||||
//// let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top
|
||||
//// if offsetY < -44 {
|
||||
//// tableView.contentInset.top = 0
|
||||
//// targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top)
|
||||
//// viewModel.collectionViewState = .expand
|
||||
//// }
|
||||
////
|
||||
//// case .expand:
|
||||
//// os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
//// guard velocity.y > 0 else { return }
|
||||
//// // check if top across
|
||||
//// let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height
|
||||
////
|
||||
//// // check if bottom bounce
|
||||
//// let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom)
|
||||
//// let bottomOffset = bottomOffsetY - scrollView.contentSize.height
|
||||
////
|
||||
//// if topOffset > 44 {
|
||||
//// // do not interrupt user scrolling
|
||||
//// viewModel.collectionViewState = .fold
|
||||
//// } else if bottomOffset > 44 {
|
||||
//// tableView.contentInset.top = -repliedToCellFrame.height
|
||||
//// targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height)
|
||||
//// viewModel.collectionViewState = .fold
|
||||
//// }
|
||||
//// }
|
||||
//// }
|
||||
//}
|
||||
//
|
||||
|
||||
//// MARK: - UITableViewDelegate
|
||||
//extension ComposeViewController: UITableViewDelegate { }
|
||||
//
|
||||
|
@ -8,6 +8,7 @@
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
extension MastodonUser {
|
||||
|
||||
@ -57,7 +58,7 @@ extension MastodonUser {
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
|
||||
|
||||
public var profileURL: URL {
|
||||
if let urlString = self.url,
|
||||
let url = URL(string: urlString) {
|
||||
@ -72,4 +73,30 @@ extension MastodonUser {
|
||||
items.append(profileURL)
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
public var nameMetaContent: MastodonMetaContent? {
|
||||
do {
|
||||
let content = MastodonContent(content: displayNameWithFallback, emojis: emojis.asDictionary)
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
return metaContent
|
||||
} catch {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var bioMetaContent: MastodonMetaContent? {
|
||||
guard let note = note else { return nil }
|
||||
do {
|
||||
let content = MastodonContent(content: note, emojis: emojis.asDictionary)
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
return metaContent
|
||||
} catch {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,100 +0,0 @@
|
||||
//
|
||||
// ComposeContentView.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK on 22/9/30.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct ComposeContentView: View {
|
||||
|
||||
@ObservedObject var viewModel: ComposeContentViewModel
|
||||
|
||||
@State var contentOffsetDelta: CGFloat = .zero
|
||||
|
||||
public var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: .zero) {
|
||||
GeometryReader { geometry in
|
||||
Color.clear.preference(
|
||||
key: ScrollOffsetPreferenceKey.self,
|
||||
value: geometry.frame(in: .named("scrollView")).origin
|
||||
)
|
||||
}.frame(width: 0, height: 0)
|
||||
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
|
||||
print("contentOffset: \(offset)")
|
||||
}
|
||||
VStack {
|
||||
Text("Reply")
|
||||
}
|
||||
.frame(height: 100)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.blue)
|
||||
.background(
|
||||
GeometryReader { geometry in
|
||||
Color.clear.preference(
|
||||
key: ViewFramePreferenceKey.self,
|
||||
value: geometry.frame(in: .named("scrollView"))
|
||||
)
|
||||
}
|
||||
.onPreferenceChange(ViewFramePreferenceKey.self) { frame in
|
||||
print("reply frame: \(frame)")
|
||||
}
|
||||
)
|
||||
VStack {
|
||||
Text("Content")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.orange)
|
||||
} // end VStack
|
||||
.offset(y: contentOffsetDelta)
|
||||
} // end ScrollView
|
||||
.coordinateSpace(name: "scrollView")
|
||||
} // end body
|
||||
}
|
||||
|
||||
private struct ScrollOffsetPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGPoint = .zero
|
||||
|
||||
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { }
|
||||
}
|
||||
|
||||
private struct ViewFramePreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGRect = .zero
|
||||
|
||||
static func reduce(value: inout CGRect, nextValue: () -> CGRect) { }
|
||||
}
|
||||
|
||||
//struct ScrollView<Content: View>: View {
|
||||
// let axes: Axis.Set
|
||||
// let showsIndicators: Bool
|
||||
// let offsetChanged: (CGPoint) -> Void
|
||||
// let content: Content
|
||||
//
|
||||
// init(
|
||||
// axes: Axis.Set = .vertical,
|
||||
// showsIndicators: Bool = true,
|
||||
// offsetChanged: @escaping (CGPoint) -> Void = { _ in },
|
||||
// @ViewBuilder content: () -> Content
|
||||
// ) {
|
||||
// self.axes = axes
|
||||
// self.showsIndicators = showsIndicators
|
||||
// self.offsetChanged = offsetChanged
|
||||
// self.content = content()
|
||||
// }
|
||||
//
|
||||
// var body: some View {
|
||||
// SwiftUI.ScrollView(axes, showsIndicators: showsIndicators) {
|
||||
// GeometryReader { geometry in
|
||||
// Color.clear.preference(
|
||||
// key: ScrollOffsetPreferenceKey.self,
|
||||
// value: geometry.frame(in: .named("scrollView")).origin
|
||||
// )
|
||||
// }.frame(width: 0, height: 0)
|
||||
// content
|
||||
// }
|
||||
// .coordinateSpace(name: "scrollView")
|
||||
// .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged)
|
||||
// }
|
||||
//}
|
@ -8,15 +8,18 @@
|
||||
import os.log
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
public final class ComposeContentViewController: UIViewController {
|
||||
|
||||
let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController")
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
public var viewModel: ComposeContentViewModel!
|
||||
|
||||
let tableView: ComposeTableView = {
|
||||
let tableView = ComposeTableView()
|
||||
tableView.estimatedRowHeight = UITableView.automaticDimension
|
||||
tableView.alwaysBounceVertical = true
|
||||
tableView.separatorStyle = .none
|
||||
tableView.tableFooterView = UIView()
|
||||
@ -45,6 +48,96 @@ extension ComposeContentViewController {
|
||||
tableView.delegate = self
|
||||
viewModel.setupDataSource(tableView: tableView)
|
||||
|
||||
|
||||
// setup snap behavior
|
||||
Publishers.CombineLatest(
|
||||
viewModel.$replyToCellFrame,
|
||||
viewModel.$scrollViewState
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] replyToCellFrame, scrollViewState in
|
||||
guard let self = self else { return }
|
||||
guard replyToCellFrame != .zero else { return }
|
||||
switch scrollViewState {
|
||||
case .fold:
|
||||
self.tableView.contentInset.top = -replyToCellFrame.height
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: set contentInset.top: -%s", ((#file as NSString).lastPathComponent), #line, #function, replyToCellFrame.height.description)
|
||||
case .expand:
|
||||
self.tableView.contentInset.top = 0
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
public override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
viewModel.viewLayoutFrame.update(view: view)
|
||||
}
|
||||
|
||||
public override func viewSafeAreaInsetsDidChange() {
|
||||
super.viewSafeAreaInsetsDidChange()
|
||||
|
||||
viewModel.viewLayoutFrame.update(view: view)
|
||||
}
|
||||
|
||||
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
coordinator.animate { [weak self] coordinatorContext in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.viewLayoutFrame.update(view: self.view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
extension ComposeContentViewController {
|
||||
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
guard scrollView === tableView else { return }
|
||||
|
||||
let replyToCellFrame = viewModel.replyToCellFrame
|
||||
guard replyToCellFrame != .zero else { return }
|
||||
|
||||
// try to find some patterns:
|
||||
// print("""
|
||||
// repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height)
|
||||
// scrollView.contentOffset.y: \(scrollView.contentOffset.y)
|
||||
// scrollView.contentSize.height: \(scrollView.contentSize.height)
|
||||
// scrollView.frame: \(scrollView.frame)
|
||||
// scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top)
|
||||
// scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom)
|
||||
// """)
|
||||
|
||||
switch viewModel.scrollViewState {
|
||||
case .fold:
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fold")
|
||||
guard velocity.y < 0 else { return }
|
||||
let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top
|
||||
if offsetY < -44 {
|
||||
tableView.contentInset.top = 0
|
||||
targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top)
|
||||
viewModel.scrollViewState = .expand
|
||||
}
|
||||
|
||||
case .expand:
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): expand")
|
||||
guard velocity.y > 0 else { return }
|
||||
// check if top across
|
||||
let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - replyToCellFrame.height
|
||||
|
||||
// check if bottom bounce
|
||||
let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom)
|
||||
let bottomOffset = bottomOffsetY - scrollView.contentSize.height
|
||||
|
||||
if topOffset > 44 {
|
||||
// do not interrupt user scrolling
|
||||
viewModel.scrollViewState = .fold
|
||||
} else if bottomOffset > 44 {
|
||||
tableView.contentInset.top = -replyToCellFrame.height
|
||||
targetContentOffset.pointee = CGPoint(x: 0, y: -replyToCellFrame.height)
|
||||
viewModel.scrollViewState = .fold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@
|
||||
import UIKit
|
||||
import MastodonCore
|
||||
import CoreDataStack
|
||||
import UIHostingConfigurationBackport
|
||||
|
||||
extension ComposeContentViewModel {
|
||||
|
||||
@ -25,21 +26,37 @@ extension ComposeContentViewModel {
|
||||
enum Section: CaseIterable {
|
||||
case replyTo
|
||||
case status
|
||||
case attachment
|
||||
case poll
|
||||
}
|
||||
|
||||
private func setupTableViewCell(tableView: UITableView) {
|
||||
private func setupTableViewCell(tableView: UITableView) {
|
||||
composeContentTableViewCell.contentConfiguration = UIHostingConfigurationBackport {
|
||||
ComposeContentView(viewModel: self)
|
||||
}
|
||||
|
||||
$contentCellFrame
|
||||
.map { $0.height }
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] height in
|
||||
guard let self = self else { return }
|
||||
guard !tableView.visibleCells.isEmpty else { return }
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.beginUpdates()
|
||||
self.composeContentTableViewCell.frame.size.height = height
|
||||
tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
switch kind {
|
||||
case .post:
|
||||
break
|
||||
case .reply(let status):
|
||||
let cell = composeReplyToTableViewCell
|
||||
// bind frame publisher
|
||||
// cell.framePublisher
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .assign(to: \.repliedToCellFrame, on: self)
|
||||
// .store(in: &cell.disposeBag)
|
||||
cell.$framePublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.replyToCellFrame, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
// set initial width
|
||||
cell.statusView.frame.size.width = tableView.frame.width
|
||||
@ -70,8 +87,6 @@ extension ComposeContentViewModel: UITableViewDataSource {
|
||||
default: return 0
|
||||
}
|
||||
case .status: return 1
|
||||
case .attachment: return 1
|
||||
case .poll: return 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,11 +95,7 @@ extension ComposeContentViewModel: UITableViewDataSource {
|
||||
case .replyTo:
|
||||
return composeReplyToTableViewCell
|
||||
case .status:
|
||||
return UITableViewCell()
|
||||
case .attachment:
|
||||
return UITableViewCell()
|
||||
case .poll:
|
||||
return UITableViewCell()
|
||||
return composeContentTableViewCell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,29 +5,79 @@
|
||||
// Created by MainasuK on 22/9/30.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import Meta
|
||||
import MastodonMeta
|
||||
|
||||
public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||
|
||||
let logger = Logger(subsystem: "ComposeContentViewModel", category: "ViewModel")
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// tableViewCell
|
||||
let composeReplyToTableViewCell = ComposeReplyToTableViewCell()
|
||||
let composeContentTableViewCell = ComposeContentTableViewCell()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let kind: Kind
|
||||
|
||||
@Published var viewLayoutFrame = ViewLayoutFrame()
|
||||
@Published var authContext: AuthContext
|
||||
|
||||
// output
|
||||
|
||||
// content
|
||||
@Published public var initialContent = ""
|
||||
@Published public var content = ""
|
||||
@Published public var contentWeightedLength = 0
|
||||
@Published public var isContentEmpty = true
|
||||
@Published public var isContentValid = true
|
||||
@Published public var isContentEditing = false
|
||||
|
||||
// author
|
||||
@Published var avatarURL: URL?
|
||||
@Published var name: MetaContent = PlaintextMetaContent(string: "")
|
||||
@Published var username: String = ""
|
||||
|
||||
// UI & UX
|
||||
@Published var replyToCellFrame: CGRect = .zero
|
||||
@Published var contentCellFrame: CGRect = .zero
|
||||
@Published var scrollViewState: ScrollViewState = .fold
|
||||
|
||||
|
||||
public init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
kind: Kind
|
||||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.kind = kind
|
||||
super.init()
|
||||
// end init
|
||||
|
||||
// bind author
|
||||
$authContext
|
||||
.sink { [weak self] authContext in
|
||||
guard let self = self else { return }
|
||||
guard let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return }
|
||||
self.avatarURL = user.avatarImageURL()
|
||||
self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback)
|
||||
self.username = user.acctWithDomain
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeContentViewModel {
|
||||
@ -38,7 +88,7 @@ extension ComposeContentViewModel {
|
||||
case reply(status: ManagedObjectRecord<Status>)
|
||||
}
|
||||
|
||||
public enum ViewState {
|
||||
public enum ScrollViewState {
|
||||
case fold // snap to input
|
||||
case expand // snap to reply
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// ComposeStatusContentTableViewCell.swift
|
||||
// ComposeContentTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-28.
|
||||
@ -12,16 +12,16 @@ import MetaTextKit
|
||||
import UITextView_Placeholder
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
import UIHostingConfigurationBackport
|
||||
|
||||
//protocol ComposeStatusContentTableViewCellDelegate: AnyObject {
|
||||
// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool
|
||||
//}
|
||||
|
||||
final class ComposeStatusContentTableViewCell: UITableViewCell {
|
||||
final class ComposeContentTableViewCell: UITableViewCell {
|
||||
|
||||
let logger = Logger(subsystem: "ComposeContentTableViewCell", category: "View")
|
||||
|
||||
// let logger = Logger(subsystem: "ComposeStatusContentTableViewCell", category: "View")
|
||||
//
|
||||
// var disposeBag = Set<AnyCancellable>()
|
||||
// weak var delegate: ComposeStatusContentTableViewCellDelegate?
|
||||
//
|
||||
@ -74,27 +74,26 @@ final class ComposeStatusContentTableViewCell: UITableViewCell {
|
||||
// metaText.delegate = nil
|
||||
// metaText.textView.delegate = nil
|
||||
// }
|
||||
//
|
||||
// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
// super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
// required init?(coder: NSCoder) {
|
||||
// super.init(coder: coder)
|
||||
// _init()
|
||||
// }
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeStatusContentTableViewCell {
|
||||
extension ComposeContentTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
layer.zPosition = 999
|
||||
backgroundColor = .clear
|
||||
|
||||
// private func _init() {
|
||||
// selectionStyle = .none
|
||||
// layer.zPosition = 999
|
||||
// backgroundColor = .clear
|
||||
// preservesSuperviewLayoutMargins = true
|
||||
//
|
||||
// let containerStackView = UIStackView()
|
||||
// containerStackView.axis = .vertical
|
||||
// containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
@ -134,7 +133,7 @@ extension ComposeStatusContentTableViewCell {
|
||||
// metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).priority(.defaultHigh),
|
||||
// ])
|
||||
// statusContentWarningEditorView.textView.delegate = self
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,97 @@
|
||||
//
|
||||
// ComposeContentView.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK on 22/9/30.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import SwiftUI
|
||||
import MastodonLocalization
|
||||
|
||||
public struct ComposeContentView: View {
|
||||
|
||||
static let logger = Logger(subsystem: "ComposeContentView", category: "View")
|
||||
var logger: Logger { ComposeContentView.logger }
|
||||
|
||||
static var margin: CGFloat = 16
|
||||
|
||||
@ObservedObject var viewModel: ComposeContentViewModel
|
||||
|
||||
public var body: some View {
|
||||
VStack(spacing: .zero) {
|
||||
Group {
|
||||
authorView
|
||||
.padding(.top, 14)
|
||||
MetaTextViewRepresentable(
|
||||
string: $viewModel.content,
|
||||
width: viewModel.viewLayoutFrame.layoutFrame.width - ComposeContentView.margin * 2,
|
||||
configurationHandler: { metaText in
|
||||
metaText.textView.attributedPlaceholder = {
|
||||
var attributes = metaText.textAttributes
|
||||
attributes[.foregroundColor] = UIColor.secondaryLabel
|
||||
return NSAttributedString(
|
||||
string: L10n.Scene.Compose.contentInputPlaceholder,
|
||||
attributes: attributes
|
||||
)
|
||||
}()
|
||||
metaText.textView.keyboardType = .twitter
|
||||
// metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.content.rawValue
|
||||
// metaText.textView.delegate = viewModel
|
||||
// metaText.delegate = viewModel
|
||||
metaText.textView.becomeFirstResponder()
|
||||
}
|
||||
)
|
||||
.frame(minHeight: 100)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
Color.clear.preference(key: ViewFramePreferenceKey.self, value: proxy.frame(in: .local))
|
||||
}
|
||||
.onPreferenceChange(ViewFramePreferenceKey.self) { frame in
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content frame: \(frame.debugDescription)")
|
||||
viewModel.contentCellFrame = frame
|
||||
}
|
||||
)
|
||||
Spacer()
|
||||
} // end VStack
|
||||
.padding(.horizontal, ComposeContentView.margin)
|
||||
// .frame(alignment: .top)
|
||||
} // end body
|
||||
}
|
||||
|
||||
extension ComposeContentView {
|
||||
var authorView: some View {
|
||||
HStack(spacing: 8) {
|
||||
AnimatedImage(imageURL: viewModel.avatarURL)
|
||||
.frame(width: 46, height: 46)
|
||||
.background(Color(UIColor.systemFill))
|
||||
.cornerRadius(12)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Spacer()
|
||||
MetaLabelRepresentable(
|
||||
textStyle: .statusName,
|
||||
metaContent: viewModel.name
|
||||
)
|
||||
Text(viewModel.username)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//private struct ScrollOffsetPreferenceKey: PreferenceKey {
|
||||
// static var defaultValue: CGPoint = .zero
|
||||
//
|
||||
// static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { }
|
||||
//}
|
||||
|
||||
private struct ViewFramePreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGRect = .zero
|
||||
|
||||
static func reduce(value: inout CGRect, nextValue: () -> CGRect) { }
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
//
|
||||
// MetaLabelRepresentable.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK on 22/10/11.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import MastodonCore
|
||||
import MetaTextKit
|
||||
|
||||
public struct MetaLabelRepresentable: UIViewRepresentable {
|
||||
|
||||
public let textStyle: MetaLabel.Style
|
||||
public let metaContent: MetaContent
|
||||
|
||||
public init(
|
||||
textStyle: MetaLabel.Style,
|
||||
metaContent: MetaContent
|
||||
) {
|
||||
self.textStyle = textStyle
|
||||
self.metaContent = metaContent
|
||||
}
|
||||
|
||||
public func makeUIView(context: Context) -> MetaLabel {
|
||||
let view = MetaLabel(style: textStyle)
|
||||
view.isUserInteractionEnabled = false
|
||||
return view
|
||||
}
|
||||
|
||||
public func updateUIView(_ view: MetaLabel, context: Context) {
|
||||
view.configure(content: metaContent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct MetaLabelRepresentable_Preview: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MetaLabelRepresentable(
|
||||
textStyle: .statusUsername,
|
||||
metaContent: PlaintextMetaContent(string: "Name")
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
@ -0,0 +1,82 @@
|
||||
//
|
||||
// MetaTextViewRepresentable.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-16.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import UITextView_Placeholder
|
||||
import MetaTextKit
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
|
||||
public struct MetaTextViewRepresentable: UIViewRepresentable {
|
||||
|
||||
let metaText = MetaText()
|
||||
|
||||
// input
|
||||
@Binding var string: String
|
||||
let width: CGFloat
|
||||
|
||||
// handler
|
||||
let configurationHandler: (MetaText) -> Void
|
||||
|
||||
public func makeUIView(context: Context) -> MetaTextView {
|
||||
let textView = metaText.textView
|
||||
|
||||
textView.backgroundColor = .clear // clear background
|
||||
textView.textContainer.lineFragmentPadding = 0 // remove leading inset
|
||||
textView.isScrollEnabled = false // enable dynamic height
|
||||
|
||||
// set width constraint
|
||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
textView.widthAnchor.constraint(equalToConstant: width).priority(.required - 1)
|
||||
])
|
||||
// make textView horizontal filled
|
||||
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
// setup editor appearance
|
||||
let font = UIFont.preferredFont(forTextStyle: .body)
|
||||
metaText.textView.font = font
|
||||
metaText.textAttributes = [
|
||||
.font: font,
|
||||
.foregroundColor: UIColor.label,
|
||||
]
|
||||
metaText.linkAttributes = [
|
||||
.font: font,
|
||||
.foregroundColor: Asset.Colors.brand.color,
|
||||
]
|
||||
|
||||
configurationHandler(metaText)
|
||||
|
||||
metaText.configure(content: PlaintextMetaContent(string: string))
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
public func updateUIView(_ metaTextView: MetaTextView, context: Context) {
|
||||
// update layout
|
||||
context.coordinator.widthLayoutConstraint.constant = width
|
||||
}
|
||||
|
||||
public func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
public class Coordinator: NSObject, UITextViewDelegate {
|
||||
let view: MetaTextViewRepresentable
|
||||
var widthLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
init(_ view: MetaTextViewRepresentable) {
|
||||
self.view = view
|
||||
super.init()
|
||||
|
||||
widthLayoutConstraint = view.metaText.textView.widthAnchor.constraint(equalToConstant: 100)
|
||||
widthLayoutConstraint.isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
//
|
||||
// ViewLayoutFrame.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK on 2022-8-17.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import CoreGraphics
|
||||
|
||||
public struct ViewLayoutFrame {
|
||||
let logger = Logger(subsystem: "ViewLayoutFrame", category: "ViewLayoutFrame")
|
||||
|
||||
public var layoutFrame: CGRect
|
||||
public var safeAreaLayoutFrame: CGRect
|
||||
public var readableContentLayoutFrame: CGRect
|
||||
|
||||
public init(
|
||||
layoutFrame: CGRect = .zero,
|
||||
safeAreaLayoutFrame: CGRect = .zero,
|
||||
readableContentLayoutFrame: CGRect = .zero
|
||||
) {
|
||||
self.layoutFrame = layoutFrame
|
||||
self.safeAreaLayoutFrame = safeAreaLayoutFrame
|
||||
self.readableContentLayoutFrame = readableContentLayoutFrame
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewLayoutFrame {
|
||||
public mutating func update(view: UIView) {
|
||||
guard view.window != nil else {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): layoutFrame update for a view without attached window. Skip this invalid update")
|
||||
return
|
||||
}
|
||||
|
||||
let layoutFrame = view.frame
|
||||
if self.layoutFrame != layoutFrame {
|
||||
self.layoutFrame = layoutFrame
|
||||
}
|
||||
|
||||
let safeAreaLayoutFrame = view.safeAreaLayoutGuide.layoutFrame
|
||||
if self.safeAreaLayoutFrame != safeAreaLayoutFrame {
|
||||
self.safeAreaLayoutFrame = safeAreaLayoutFrame
|
||||
}
|
||||
|
||||
let readableContentLayoutFrame = view.readableContentGuide.layoutFrame
|
||||
if self.readableContentLayoutFrame != readableContentLayoutFrame {
|
||||
self.readableContentLayoutFrame = readableContentLayoutFrame
|
||||
}
|
||||
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): layoutFrame: \(layoutFrame.debugDescription)")
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): safeAreaLayoutFrame: \(safeAreaLayoutFrame.debugDescription)")
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): readableContentLayoutFrame: \(readableContentLayoutFrame.debugDescription)")
|
||||
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user