feat: [WP] restore the content compose via SwiftUI and support expandable reply view for compose scene

This commit is contained in:
CMK 2022-10-11 18:31:40 +08:00
parent 02e3ad9a16
commit 4367e8eaba
11 changed files with 548 additions and 268 deletions

View File

@ -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 { }
//

View File

@ -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
}
}
}

View File

@ -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)
// }
//}

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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
// }
}
}

View File

@ -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) { }
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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)")
}
}