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")
|
let logger = Logger(subsystem: "ComposeViewController", category: "logic")
|
||||||
|
|
||||||
lazy var composeContentViewModel: ComposeContentViewModel = {
|
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 = {
|
private(set) lazy var composeContentViewController: ComposeContentViewController = {
|
||||||
let composeContentViewController = ComposeContentViewController()
|
let composeContentViewController = ComposeContentViewController()
|
||||||
|
@ -39,20 +43,20 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
return composeContentViewController
|
return composeContentViewController
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
||||||
// let characterCountLabel: UILabel = {
|
let characterCountLabel: UILabel = {
|
||||||
// let label = UILabel()
|
let label = UILabel()
|
||||||
// label.font = .systemFont(ofSize: 15, weight: .regular)
|
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
// label.text = "500"
|
label.text = "500"
|
||||||
// label.textColor = Asset.Colors.Label.secondary.color
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
// label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500)
|
label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500)
|
||||||
// return label
|
return label
|
||||||
// }()
|
}()
|
||||||
// private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = {
|
private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = {
|
||||||
// let barButtonItem = UIBarButtonItem(customView: characterCountLabel)
|
let barButtonItem = UIBarButtonItem(customView: characterCountLabel)
|
||||||
// return barButtonItem
|
return barButtonItem
|
||||||
// }()
|
}()
|
||||||
//
|
|
||||||
// let publishButton: UIButton = {
|
// let publishButton: UIButton = {
|
||||||
// let button = RoundedEdgesButton(type: .custom)
|
// let button = RoundedEdgesButton(type: .custom)
|
||||||
// button.cornerRadius = 10
|
// button.cornerRadius = 10
|
||||||
|
@ -83,23 +87,6 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
// publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
// publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
||||||
// publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
// 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 {
|
// var systemKeyboardHeight: CGFloat = .zero {
|
||||||
// didSet {
|
// didSet {
|
||||||
|
@ -177,6 +164,22 @@ extension ComposeViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.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)
|
addChild(composeContentViewController)
|
||||||
composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(composeContentViewController.view)
|
view.addSubview(composeContentViewController.view)
|
||||||
|
@ -212,21 +215,6 @@ extension ComposeViewController {
|
||||||
// self.setupBackgroundColor(theme: theme)
|
// self.setupBackgroundColor(theme: theme)
|
||||||
// }
|
// }
|
||||||
// .store(in: &disposeBag)
|
// .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
|
// scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -533,26 +521,6 @@ extension ComposeViewController {
|
||||||
// })
|
// })
|
||||||
// .store(in: &disposeBag)
|
// .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)
|
// configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value)
|
||||||
// Publishers.CombineLatest(
|
// Publishers.CombineLatest(
|
||||||
// keyboardHasShortcutBar,
|
// keyboardHasShortcutBar,
|
||||||
|
@ -746,17 +714,17 @@ extension ComposeViewController {
|
||||||
//
|
//
|
||||||
//}
|
//}
|
||||||
//
|
//
|
||||||
//extension ComposeViewController {
|
extension ComposeViewController {
|
||||||
//
|
|
||||||
// @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||||
// guard viewModel.shouldDismiss else {
|
// guard viewModel.shouldDismiss else {
|
||||||
// showDismissConfirmAlertController()
|
// showDismissConfirmAlertController()
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
// dismiss(animated: true, completion: nil)
|
dismiss(animated: true, completion: nil)
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
// @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
// do {
|
// do {
|
||||||
|
@ -779,9 +747,9 @@ extension ComposeViewController {
|
||||||
//
|
//
|
||||||
// dismiss(animated: true, completion: nil)
|
// dismiss(animated: true, completion: nil)
|
||||||
// }
|
// }
|
||||||
//
|
|
||||||
//}
|
}
|
||||||
//
|
|
||||||
//// MARK: - MetaTextDelegate
|
//// MARK: - MetaTextDelegate
|
||||||
//extension ComposeViewController: MetaTextDelegate {
|
//extension ComposeViewController: MetaTextDelegate {
|
||||||
// func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
|
// 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
|
//// MARK: - UITableViewDelegate
|
||||||
//extension ComposeViewController: UITableViewDelegate { }
|
//extension ComposeViewController: UITableViewDelegate { }
|
||||||
//
|
//
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonMeta
|
||||||
|
|
||||||
extension MastodonUser {
|
extension MastodonUser {
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ extension MastodonUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonUser {
|
extension MastodonUser {
|
||||||
|
|
||||||
public var profileURL: URL {
|
public var profileURL: URL {
|
||||||
if let urlString = self.url,
|
if let urlString = self.url,
|
||||||
let url = URL(string: urlString) {
|
let url = URL(string: urlString) {
|
||||||
|
@ -72,4 +73,30 @@ extension MastodonUser {
|
||||||
items.append(profileURL)
|
items.append(profileURL)
|
||||||
return items
|
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 os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
public final class ComposeContentViewController: UIViewController {
|
public final class ComposeContentViewController: UIViewController {
|
||||||
|
|
||||||
let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController")
|
let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController")
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
public var viewModel: ComposeContentViewModel!
|
public var viewModel: ComposeContentViewModel!
|
||||||
|
|
||||||
let tableView: ComposeTableView = {
|
let tableView: ComposeTableView = {
|
||||||
let tableView = ComposeTableView()
|
let tableView = ComposeTableView()
|
||||||
|
tableView.estimatedRowHeight = UITableView.automaticDimension
|
||||||
tableView.alwaysBounceVertical = true
|
tableView.alwaysBounceVertical = true
|
||||||
tableView.separatorStyle = .none
|
tableView.separatorStyle = .none
|
||||||
tableView.tableFooterView = UIView()
|
tableView.tableFooterView = UIView()
|
||||||
|
@ -45,6 +48,96 @@ extension ComposeContentViewController {
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
viewModel.setupDataSource(tableView: tableView)
|
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 UIKit
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import UIHostingConfigurationBackport
|
||||||
|
|
||||||
extension ComposeContentViewModel {
|
extension ComposeContentViewModel {
|
||||||
|
|
||||||
|
@ -25,21 +26,37 @@ extension ComposeContentViewModel {
|
||||||
enum Section: CaseIterable {
|
enum Section: CaseIterable {
|
||||||
case replyTo
|
case replyTo
|
||||||
case status
|
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 {
|
switch kind {
|
||||||
case .post:
|
case .post:
|
||||||
break
|
break
|
||||||
case .reply(let status):
|
case .reply(let status):
|
||||||
let cell = composeReplyToTableViewCell
|
let cell = composeReplyToTableViewCell
|
||||||
// bind frame publisher
|
// bind frame publisher
|
||||||
// cell.framePublisher
|
cell.$framePublisher
|
||||||
// .receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
// .assign(to: \.repliedToCellFrame, on: self)
|
.assign(to: \.replyToCellFrame, on: self)
|
||||||
// .store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
// set initial width
|
// set initial width
|
||||||
cell.statusView.frame.size.width = tableView.frame.width
|
cell.statusView.frame.size.width = tableView.frame.width
|
||||||
|
@ -70,8 +87,6 @@ extension ComposeContentViewModel: UITableViewDataSource {
|
||||||
default: return 0
|
default: return 0
|
||||||
}
|
}
|
||||||
case .status: return 1
|
case .status: return 1
|
||||||
case .attachment: return 1
|
|
||||||
case .poll: return 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,11 +95,7 @@ extension ComposeContentViewModel: UITableViewDataSource {
|
||||||
case .replyTo:
|
case .replyTo:
|
||||||
return composeReplyToTableViewCell
|
return composeReplyToTableViewCell
|
||||||
case .status:
|
case .status:
|
||||||
return UITableViewCell()
|
return composeContentTableViewCell
|
||||||
case .attachment:
|
|
||||||
return UITableViewCell()
|
|
||||||
case .poll:
|
|
||||||
return UITableViewCell()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,29 +5,79 @@
|
||||||
// Created by MainasuK on 22/9/30.
|
// Created by MainasuK on 22/9/30.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
|
import Meta
|
||||||
|
import MastodonMeta
|
||||||
|
|
||||||
public final class ComposeContentViewModel: NSObject, ObservableObject {
|
public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "ComposeContentViewModel", category: "ViewModel")
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
// tableViewCell
|
// tableViewCell
|
||||||
let composeReplyToTableViewCell = ComposeReplyToTableViewCell()
|
let composeReplyToTableViewCell = ComposeReplyToTableViewCell()
|
||||||
|
let composeContentTableViewCell = ComposeContentTableViewCell()
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let kind: Kind
|
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(
|
public init(
|
||||||
context: AppContext,
|
context: AppContext,
|
||||||
|
authContext: AuthContext,
|
||||||
kind: Kind
|
kind: Kind
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.authContext = authContext
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
super.init()
|
super.init()
|
||||||
// end 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 {
|
extension ComposeContentViewModel {
|
||||||
|
@ -38,7 +88,7 @@ extension ComposeContentViewModel {
|
||||||
case reply(status: ManagedObjectRecord<Status>)
|
case reply(status: ManagedObjectRecord<Status>)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ViewState {
|
public enum ScrollViewState {
|
||||||
case fold // snap to input
|
case fold // snap to input
|
||||||
case expand // snap to reply
|
case expand // snap to reply
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// ComposeStatusContentTableViewCell.swift
|
// ComposeContentTableViewCell.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by MainasuK Cirno on 2021-6-28.
|
// Created by MainasuK Cirno on 2021-6-28.
|
||||||
|
@ -12,16 +12,16 @@ import MetaTextKit
|
||||||
import UITextView_Placeholder
|
import UITextView_Placeholder
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
import MastodonUI
|
import UIHostingConfigurationBackport
|
||||||
|
|
||||||
//protocol ComposeStatusContentTableViewCellDelegate: AnyObject {
|
//protocol ComposeStatusContentTableViewCellDelegate: AnyObject {
|
||||||
// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool
|
// 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>()
|
// var disposeBag = Set<AnyCancellable>()
|
||||||
// weak var delegate: ComposeStatusContentTableViewCellDelegate?
|
// weak var delegate: ComposeStatusContentTableViewCellDelegate?
|
||||||
//
|
//
|
||||||
|
@ -74,27 +74,26 @@ final class ComposeStatusContentTableViewCell: UITableViewCell {
|
||||||
// metaText.delegate = nil
|
// metaText.delegate = nil
|
||||||
// metaText.textView.delegate = nil
|
// metaText.textView.delegate = nil
|
||||||
// }
|
// }
|
||||||
//
|
|
||||||
// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
// super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
// _init()
|
_init()
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
// super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
// _init()
|
_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()
|
// let containerStackView = UIStackView()
|
||||||
// containerStackView.axis = .vertical
|
// containerStackView.axis = .vertical
|
||||||
// containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
// containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -134,7 +133,7 @@ extension ComposeStatusContentTableViewCell {
|
||||||
// metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).priority(.defaultHigh),
|
// metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).priority(.defaultHigh),
|
||||||
// ])
|
// ])
|
||||||
// statusContentWarningEditorView.textView.delegate = self
|
// 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…
Reference in New Issue