feat: restore auto-complete for compose scene content input
This commit is contained in:
parent
f7d0186bf3
commit
e7ef0f79c7
|
@ -376,7 +376,6 @@
|
||||||
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; };
|
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; };
|
||||||
DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; };
|
DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; };
|
||||||
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; };
|
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; };
|
||||||
DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; };
|
|
||||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
||||||
DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ComposeViewController.swift */; };
|
DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ComposeViewController.swift */; };
|
||||||
DBC6461826A170AB00B0E31B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DBC6461626A170AB00B0E31B /* MainInterface.storyboard */; };
|
DBC6461826A170AB00B0E31B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DBC6461626A170AB00B0E31B /* MainInterface.storyboard */; };
|
||||||
|
@ -962,7 +961,6 @@
|
||||||
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = "<group>"; };
|
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = "<group>"; };
|
||||||
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
|
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
|
||||||
DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
||||||
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = "<group>"; };
|
|
||||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||||
DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
DBC6461426A170AB00B0E31B /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
DBC6461426A170AB00B0E31B /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2520,7 +2518,6 @@
|
||||||
DBBC24D526A54BCB00398BB9 /* Helper */ = {
|
DBBC24D526A54BCB00398BB9 /* Helper */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */,
|
|
||||||
DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */,
|
DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */,
|
||||||
);
|
);
|
||||||
path = Helper;
|
path = Helper;
|
||||||
|
@ -3246,7 +3243,6 @@
|
||||||
DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */,
|
DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */,
|
||||||
DB3E6FEC2806D7F100B035AE /* DiscoveryNewsViewController.swift in Sources */,
|
DB3E6FEC2806D7F100B035AE /* DiscoveryNewsViewController.swift in Sources */,
|
||||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
||||||
DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */,
|
|
||||||
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
|
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
|
||||||
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
|
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
|
||||||
2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */,
|
2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */,
|
||||||
|
|
|
@ -106,13 +106,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
// let composeToolbarBackgroundView = UIView()
|
// let composeToolbarBackgroundView = UIView()
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
// private(set) lazy var autoCompleteViewController: AutoCompleteViewController = {
|
|
||||||
// let viewController = AutoCompleteViewController()
|
|
||||||
// viewController.viewModel = AutoCompleteViewModel(context: context, authContext: viewModel.authContext)
|
|
||||||
// viewController.delegate = self
|
|
||||||
// viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel
|
|
||||||
// return viewController
|
|
||||||
// }()
|
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
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)
|
||||||
|
@ -243,33 +237,6 @@ extension ComposeViewController {
|
||||||
// // update layout when keyboard show/dismiss
|
// // update layout when keyboard show/dismiss
|
||||||
// view.layoutIfNeeded()
|
// view.layoutIfNeeded()
|
||||||
//
|
//
|
||||||
|
|
||||||
//
|
|
||||||
// // bind auto-complete
|
|
||||||
// viewModel.$autoCompleteInfo
|
|
||||||
// .receive(on: DispatchQueue.main)
|
|
||||||
// .sink { [weak self] info in
|
|
||||||
// guard let self = self else { return }
|
|
||||||
// let textEditorView = self.textEditorView
|
|
||||||
// if self.autoCompleteViewController.view.superview == nil {
|
|
||||||
// self.autoCompleteViewController.view.frame = self.view.bounds
|
|
||||||
// // add to container view. seealso: `viewDidLayoutSubviews()`
|
|
||||||
// self.viewModel.composeStatusContentTableViewCell.textEditorViewContainerView.addSubview(self.autoCompleteViewController.view)
|
|
||||||
// self.addChild(self.autoCompleteViewController)
|
|
||||||
// self.autoCompleteViewController.didMove(toParent: self)
|
|
||||||
// self.autoCompleteViewController.view.isHidden = true
|
|
||||||
// self.tableView.autoCompleteViewController = self.autoCompleteViewController
|
|
||||||
// }
|
|
||||||
// self.updateAutoCompleteViewControllerLayout()
|
|
||||||
// self.autoCompleteViewController.view.isHidden = info == nil
|
|
||||||
// guard let info = info else { return }
|
|
||||||
// let symbolBoundingRectInContainer = textEditorView.textView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView)
|
|
||||||
// self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY
|
|
||||||
// self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer
|
|
||||||
// self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText)
|
|
||||||
// }
|
|
||||||
// .store(in: &disposeBag)
|
|
||||||
//
|
|
||||||
// // bind publish bar button state
|
// // bind publish bar button state
|
||||||
// viewModel.$isPublishBarButtonItemEnabled
|
// viewModel.$isPublishBarButtonItemEnabled
|
||||||
// .receive(on: DispatchQueue.main)
|
// .receive(on: DispatchQueue.main)
|
||||||
|
@ -431,23 +398,6 @@ extension ComposeViewController {
|
||||||
// viewModel.traitCollectionDidChangePublisher.send()
|
// viewModel.traitCollectionDidChangePublisher.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
|
||||||
super.viewDidLayoutSubviews()
|
|
||||||
|
|
||||||
updateAutoCompleteViewControllerLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateAutoCompleteViewControllerLayout() {
|
|
||||||
// pin autoCompleteViewController frame to current view
|
|
||||||
// if let containerView = autoCompleteViewController.view.superview {
|
|
||||||
// let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: view)
|
|
||||||
// if viewFrameInWindow.origin.x != 0 {
|
|
||||||
// autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x
|
|
||||||
// }
|
|
||||||
// autoCompleteViewController.view.frame.size.width = view.frame.width
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//extension ComposeViewController {
|
//extension ComposeViewController {
|
||||||
|
@ -661,126 +611,11 @@ extension ComposeViewController {
|
||||||
// return true
|
// return true
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// func textViewDidChange(_ textView: UITextView) {
|
|
||||||
// switch textView {
|
|
||||||
// case textEditorView.textView:
|
|
||||||
// // update model
|
|
||||||
// let metaText = self.textEditorView
|
|
||||||
// let backedString = metaText.backedString
|
|
||||||
// viewModel.composeStatusAttribute.composeContent = backedString
|
|
||||||
// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)")
|
|
||||||
//
|
|
||||||
// // configure auto completion
|
|
||||||
// setupAutoComplete(for: textView)
|
|
||||||
// default:
|
|
||||||
// assertionFailure()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
//
|
||||||
// struct AutoCompleteInfo {
|
|
||||||
// // model
|
|
||||||
// let inputText: Substring
|
|
||||||
// // range
|
|
||||||
// let symbolRange: Range<String.Index>
|
|
||||||
// let symbolString: Substring
|
|
||||||
// let toCursorRange: Range<String.Index>
|
|
||||||
// let toCursorString: Substring
|
|
||||||
// let toHighlightEndRange: Range<String.Index>
|
|
||||||
// let toHighlightEndString: Substring
|
|
||||||
// // geometry
|
|
||||||
// var textBoundingRect: CGRect = .zero
|
|
||||||
// var symbolBoundingRect: CGRect = .zero
|
|
||||||
// }
|
|
||||||
//
|
//
|
||||||
// private func setupAutoComplete(for textView: UITextView) {
|
|
||||||
// guard var autoCompletion = ComposeViewController.scanAutoCompleteInfo(textView: textView) else {
|
|
||||||
// viewModel.autoCompleteInfo = nil
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString))
|
|
||||||
//
|
|
||||||
// // get layout text bounding rect
|
|
||||||
// var glyphRange = NSRange()
|
|
||||||
// textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textView.text), actualGlyphRange: &glyphRange)
|
|
||||||
// let textContainer = textView.layoutManager.textContainers[0]
|
|
||||||
// let textBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
|
||||||
//
|
|
||||||
// let retryLayoutTimes = viewModel.autoCompleteRetryLayoutTimes
|
|
||||||
// guard textBoundingRect.size != .zero else {
|
|
||||||
// viewModel.autoCompleteRetryLayoutTimes += 1
|
|
||||||
// // avoid infinite loop
|
|
||||||
// guard retryLayoutTimes < 3 else { return }
|
|
||||||
// // needs retry calculate layout when the rect position changing
|
|
||||||
// DispatchQueue.main.async {
|
|
||||||
// self.setupAutoComplete(for: textView)
|
|
||||||
// }
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// viewModel.autoCompleteRetryLayoutTimes = 0
|
|
||||||
//
|
|
||||||
// // get symbol bounding rect
|
|
||||||
// textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textView.text), actualGlyphRange: &glyphRange)
|
|
||||||
// let symbolBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
|
||||||
//
|
|
||||||
// // set bounding rect and trigger layout
|
|
||||||
// autoCompletion.textBoundingRect = textBoundingRect
|
|
||||||
// autoCompletion.symbolBoundingRect = symbolBoundingRect
|
|
||||||
// viewModel.autoCompleteInfo = autoCompletion
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private static func scanAutoCompleteInfo(textView: UITextView) -> AutoCompleteInfo? {
|
|
||||||
// guard let text = textView.text,
|
|
||||||
// textView.selectedRange.location > 0, !text.isEmpty,
|
|
||||||
// let selectedRange = Range(textView.selectedRange, in: text) else {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// let cursorIndex = selectedRange.upperBound
|
|
||||||
// let _highlightStartIndex: String.Index? = {
|
|
||||||
// var index = text.index(before: cursorIndex)
|
|
||||||
// while index > text.startIndex {
|
|
||||||
// let char = text[index]
|
|
||||||
// if char == "@" || char == "#" || char == ":" {
|
|
||||||
// return index
|
|
||||||
// }
|
|
||||||
// index = text.index(before: index)
|
|
||||||
// }
|
|
||||||
// assert(index == text.startIndex)
|
|
||||||
// let char = text[index]
|
|
||||||
// if char == "@" || char == "#" || char == ":" {
|
|
||||||
// return index
|
|
||||||
// } else {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// }()
|
|
||||||
//
|
|
||||||
// guard let highlightStartIndex = _highlightStartIndex else { return nil }
|
|
||||||
// let scanRange = NSRange(highlightStartIndex..<text.endIndex, in: text)
|
|
||||||
//
|
|
||||||
// guard let match = text.firstMatch(pattern: MastodonRegex.autoCompletePattern, options: [], range: scanRange) else { return nil }
|
|
||||||
// guard let matchRange = Range(match.range(at: 0), in: text) else { return nil }
|
|
||||||
// let matchStartIndex = matchRange.lowerBound
|
|
||||||
// let matchEndIndex = matchRange.upperBound
|
|
||||||
//
|
|
||||||
// guard matchStartIndex == highlightStartIndex, matchEndIndex >= cursorIndex else { return nil }
|
|
||||||
// let symbolRange = highlightStartIndex..<text.index(after: highlightStartIndex)
|
|
||||||
// let symbolString = text[symbolRange]
|
|
||||||
// let toCursorRange = highlightStartIndex..<cursorIndex
|
|
||||||
// let toCursorString = text[toCursorRange]
|
|
||||||
// let toHighlightEndRange = matchStartIndex..<matchEndIndex
|
|
||||||
// let toHighlightEndString = text[toHighlightEndRange]
|
|
||||||
//
|
|
||||||
// let inputText = toHighlightEndString
|
|
||||||
// let autoCompleteInfo = AutoCompleteInfo(
|
|
||||||
// inputText: inputText,
|
|
||||||
// symbolRange: symbolRange,
|
|
||||||
// symbolString: symbolString,
|
|
||||||
// toCursorRange: toCursorRange,
|
|
||||||
// toCursorString: toCursorString,
|
|
||||||
// toHighlightEndRange: toHighlightEndRange,
|
|
||||||
// toHighlightEndString: toHighlightEndString
|
|
||||||
// )
|
|
||||||
// return autoCompleteInfo
|
|
||||||
// }
|
|
||||||
//
|
//
|
||||||
// func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
// func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||||
// switch textView {
|
// switch textView {
|
||||||
|
|
|
@ -41,8 +41,6 @@ final class ComposeViewModel: NSObject {
|
||||||
//
|
//
|
||||||
// @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType
|
// @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType
|
||||||
// @Published var repliedToCellFrame: CGRect = .zero
|
// @Published var repliedToCellFrame: CGRect = .zero
|
||||||
// @Published var autoCompleteRetryLayoutTimes = 0
|
|
||||||
// @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil
|
|
||||||
|
|
||||||
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit
|
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit
|
||||||
// var isViewAppeared = false
|
// var isViewAppeared = false
|
||||||
|
|
|
@ -88,7 +88,7 @@ extension AutoCompleteViewController {
|
||||||
])
|
])
|
||||||
|
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
// viewModel.setupDiffableDataSource(tableView: tableView)
|
viewModel.setupDiffableDataSource(tableView: tableView)
|
||||||
|
|
||||||
// bind to layout chevron
|
// bind to layout chevron
|
||||||
viewModel.symbolBoundingRect
|
viewModel.symbolBoundingRect
|
||||||
|
|
|
@ -6,17 +6,18 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import MastodonCore
|
||||||
|
|
||||||
extension AutoCompleteViewModel {
|
extension AutoCompleteViewModel {
|
||||||
|
|
||||||
// func setupDiffableDataSource(
|
func setupDiffableDataSource(
|
||||||
// tableView: UITableView
|
tableView: UITableView
|
||||||
// ) {
|
) {
|
||||||
// diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(for: tableView)
|
diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(tableView: tableView)
|
||||||
//
|
|
||||||
// var snapshot = NSDiffableDataSourceSnapshot<AutoCompleteSection, AutoCompleteItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<AutoCompleteSection, AutoCompleteItem>()
|
||||||
// snapshot.appendSections([.main])
|
snapshot.appendSections([.main])
|
||||||
// diffableDataSource?.apply(snapshot)
|
diffableDataSource?.apply(snapshot)
|
||||||
// }
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ public final class ComposeContentViewController: UIViewController {
|
||||||
public var viewModel: ComposeContentViewModel!
|
public var viewModel: ComposeContentViewModel!
|
||||||
private(set) lazy var composeContentToolbarViewModel = ComposeContentToolbarView.ViewModel(delegate: self)
|
private(set) lazy var composeContentToolbarViewModel = ComposeContentToolbarView.ViewModel(delegate: self)
|
||||||
|
|
||||||
|
// tableView container
|
||||||
let tableView: ComposeTableView = {
|
let tableView: ComposeTableView = {
|
||||||
let tableView = ComposeTableView()
|
let tableView = ComposeTableView()
|
||||||
tableView.estimatedRowHeight = UITableView.automaticDimension
|
tableView.estimatedRowHeight = UITableView.automaticDimension
|
||||||
|
@ -29,6 +30,17 @@ public final class ComposeContentViewController: UIViewController {
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// auto complete
|
||||||
|
private(set) lazy var autoCompleteViewController: AutoCompleteViewController = {
|
||||||
|
let viewController = AutoCompleteViewController()
|
||||||
|
viewController.viewModel = AutoCompleteViewModel(context: viewModel.context, authContext: viewModel.authContext)
|
||||||
|
viewController.delegate = self
|
||||||
|
// viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel
|
||||||
|
return viewController
|
||||||
|
}()
|
||||||
|
|
||||||
|
// toolbar
|
||||||
|
|
||||||
lazy var composeContentToolbarView = ComposeContentToolbarView(viewModel: composeContentToolbarViewModel)
|
lazy var composeContentToolbarView = ComposeContentToolbarView(viewModel: composeContentToolbarViewModel)
|
||||||
var composeContentToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
|
var composeContentToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
|
||||||
let composeContentToolbarBackgroundView = UIView()
|
let composeContentToolbarBackgroundView = UIView()
|
||||||
|
@ -218,6 +230,32 @@ extension ComposeContentViewController {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind auto-complete
|
||||||
|
viewModel.$autoCompleteInfo
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] info in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let textView = self.viewModel.contentMetaText?.textView else { return }
|
||||||
|
if self.autoCompleteViewController.view.superview == nil {
|
||||||
|
self.autoCompleteViewController.view.frame = self.view.bounds
|
||||||
|
// add to container view. seealso: `viewDidLayoutSubviews()`
|
||||||
|
self.viewModel.composeContentTableViewCell.contentView.addSubview(self.autoCompleteViewController.view)
|
||||||
|
self.addChild(self.autoCompleteViewController)
|
||||||
|
self.autoCompleteViewController.didMove(toParent: self)
|
||||||
|
self.autoCompleteViewController.view.isHidden = true
|
||||||
|
self.tableView.autoCompleteViewController = self.autoCompleteViewController
|
||||||
|
}
|
||||||
|
self.updateAutoCompleteViewControllerLayout()
|
||||||
|
self.autoCompleteViewController.view.isHidden = info == nil
|
||||||
|
guard let info = info else { return }
|
||||||
|
let symbolBoundingRectInContainer = textView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView)
|
||||||
|
print(info.symbolBoundingRect)
|
||||||
|
self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY + self.viewModel.contentTextViewFrame.minY
|
||||||
|
self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer
|
||||||
|
self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind toolbar
|
// bind toolbar
|
||||||
bindToolbarViewModel()
|
bindToolbarViewModel()
|
||||||
}
|
}
|
||||||
|
@ -226,6 +264,7 @@ extension ComposeContentViewController {
|
||||||
super.viewDidLayoutSubviews()
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
viewModel.viewLayoutFrame.update(view: view)
|
viewModel.viewLayoutFrame.update(view: view)
|
||||||
|
updateAutoCompleteViewControllerLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func viewSafeAreaInsetsDidChange() {
|
public override func viewSafeAreaInsetsDidChange() {
|
||||||
|
@ -264,6 +303,17 @@ extension ComposeContentViewController {
|
||||||
viewModel.$contentWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWeightedLength)
|
viewModel.$contentWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWeightedLength)
|
||||||
viewModel.$contentWarningWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWarningWeightedLength)
|
viewModel.$contentWarningWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWarningWeightedLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateAutoCompleteViewControllerLayout() {
|
||||||
|
// pin autoCompleteViewController frame to current view
|
||||||
|
if let containerView = autoCompleteViewController.view.superview {
|
||||||
|
let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: view)
|
||||||
|
if viewFrameInWindow.origin.x != 0 {
|
||||||
|
autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x
|
||||||
|
}
|
||||||
|
autoCompleteViewController.view.frame.size.width = view.frame.width
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UIScrollViewDelegate
|
// MARK: - UIScrollViewDelegate
|
||||||
|
@ -427,3 +477,13 @@ extension ComposeContentViewController: ComposeContentToolbarViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - AutoCompleteViewControllerDelegate
|
||||||
|
extension ComposeContentViewController: AutoCompleteViewControllerDelegate {
|
||||||
|
func autoCompleteViewController(
|
||||||
|
_ viewController: AutoCompleteViewController,
|
||||||
|
didSelectItem item: AutoCompleteItem
|
||||||
|
) {
|
||||||
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did select item: \(String(describing: item))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,203 @@
|
||||||
|
//
|
||||||
|
// ComposeContentViewModel+UITextViewDelegate.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022/11/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
// MARK: - UITextViewDelegate
|
||||||
|
extension ComposeContentViewModel: UITextViewDelegate {
|
||||||
|
|
||||||
|
public func textViewDidBeginEditing(_ textView: UITextView) {
|
||||||
|
// Note:
|
||||||
|
// Xcode warning:
|
||||||
|
// Publishing changes from within view updates is not allowed, this will cause undefined behavior.
|
||||||
|
//
|
||||||
|
// Just ignore the warning and see what will happen…
|
||||||
|
switch textView {
|
||||||
|
case contentMetaText?.textView:
|
||||||
|
isContentEditing = true
|
||||||
|
case contentWarningMetaText?.textView:
|
||||||
|
isContentWarningEditing = true
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func textViewDidChange(_ textView: UITextView) {
|
||||||
|
switch textView {
|
||||||
|
case contentMetaText?.textView:
|
||||||
|
// update model
|
||||||
|
guard let metaText = self.contentMetaText else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let backedString = metaText.backedString
|
||||||
|
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)")
|
||||||
|
|
||||||
|
// configure auto completion
|
||||||
|
setupAutoComplete(for: textView)
|
||||||
|
|
||||||
|
case contentWarningMetaText?.textView:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func textViewDidEndEditing(_ textView: UITextView) {
|
||||||
|
switch textView {
|
||||||
|
case contentMetaText?.textView:
|
||||||
|
isContentEditing = false
|
||||||
|
case contentWarningMetaText?.textView:
|
||||||
|
isContentWarningEditing = false
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||||
|
switch textView {
|
||||||
|
case contentMetaText?.textView:
|
||||||
|
return true
|
||||||
|
case contentWarningMetaText?.textView:
|
||||||
|
let isReturn = text == "\n"
|
||||||
|
if isReturn {
|
||||||
|
setContentTextViewFirstResponderIfNeeds()
|
||||||
|
}
|
||||||
|
return !isReturn
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeContentViewModel {
|
||||||
|
|
||||||
|
func insertContentText(text: String) {
|
||||||
|
guard let contentMetaText = self.contentMetaText else { return }
|
||||||
|
// FIXME: smart prefix and suffix
|
||||||
|
let string = contentMetaText.textStorage.string
|
||||||
|
let isEmpty = string.isEmpty
|
||||||
|
let hasPrefix = string.hasPrefix(" ")
|
||||||
|
if hasPrefix || isEmpty {
|
||||||
|
contentMetaText.textView.insertText(text)
|
||||||
|
} else {
|
||||||
|
contentMetaText.textView.insertText(" " + text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setContentTextViewFirstResponderIfNeeds() {
|
||||||
|
guard let contentMetaText = self.contentMetaText else { return }
|
||||||
|
guard !contentMetaText.textView.isFirstResponder else { return }
|
||||||
|
contentMetaText.textView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setContentWarningTextViewFirstResponderIfNeeds() {
|
||||||
|
guard let contentWarningMetaText = self.contentWarningMetaText else { return }
|
||||||
|
guard !contentWarningMetaText.textView.isFirstResponder else { return }
|
||||||
|
contentWarningMetaText.textView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeContentViewModel {
|
||||||
|
|
||||||
|
private func setupAutoComplete(for textView: UITextView) {
|
||||||
|
guard var autoCompletion = ComposeContentViewModel.scanAutoCompleteInfo(textView: textView) else {
|
||||||
|
self.autoCompleteInfo = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString))
|
||||||
|
|
||||||
|
// get layout text bounding rect
|
||||||
|
var glyphRange = NSRange()
|
||||||
|
textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textView.text), actualGlyphRange: &glyphRange)
|
||||||
|
let textContainer = textView.layoutManager.textContainers[0]
|
||||||
|
let textBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||||
|
|
||||||
|
let retryLayoutTimes = autoCompleteRetryLayoutTimes
|
||||||
|
guard textBoundingRect.size != .zero else {
|
||||||
|
autoCompleteRetryLayoutTimes += 1
|
||||||
|
// avoid infinite loop
|
||||||
|
guard retryLayoutTimes < 3 else { return }
|
||||||
|
// needs retry calculate layout when the rect position changing
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.setupAutoComplete(for: textView)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
autoCompleteRetryLayoutTimes = 0
|
||||||
|
|
||||||
|
// get symbol bounding rect
|
||||||
|
textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textView.text), actualGlyphRange: &glyphRange)
|
||||||
|
let symbolBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||||
|
|
||||||
|
// set bounding rect and trigger layout
|
||||||
|
autoCompletion.textBoundingRect = textBoundingRect
|
||||||
|
autoCompletion.symbolBoundingRect = symbolBoundingRect
|
||||||
|
autoCompleteInfo = autoCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func scanAutoCompleteInfo(textView: UITextView) -> AutoCompleteInfo? {
|
||||||
|
guard let text = textView.text,
|
||||||
|
textView.selectedRange.location > 0, !text.isEmpty,
|
||||||
|
let selectedRange = Range(textView.selectedRange, in: text) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let cursorIndex = selectedRange.upperBound
|
||||||
|
let _highlightStartIndex: String.Index? = {
|
||||||
|
var index = text.index(before: cursorIndex)
|
||||||
|
while index > text.startIndex {
|
||||||
|
let char = text[index]
|
||||||
|
if char == "@" || char == "#" || char == ":" {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
index = text.index(before: index)
|
||||||
|
}
|
||||||
|
assert(index == text.startIndex)
|
||||||
|
let char = text[index]
|
||||||
|
if char == "@" || char == "#" || char == ":" {
|
||||||
|
return index
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let highlightStartIndex = _highlightStartIndex else { return nil }
|
||||||
|
let scanRange = NSRange(highlightStartIndex..<text.endIndex, in: text)
|
||||||
|
|
||||||
|
guard let match = text.firstMatch(pattern: MastodonRegex.autoCompletePattern, options: [], range: scanRange) else { return nil }
|
||||||
|
guard let matchRange = Range(match.range(at: 0), in: text) else { return nil }
|
||||||
|
let matchStartIndex = matchRange.lowerBound
|
||||||
|
let matchEndIndex = matchRange.upperBound
|
||||||
|
|
||||||
|
guard matchStartIndex == highlightStartIndex, matchEndIndex >= cursorIndex else { return nil }
|
||||||
|
let symbolRange = highlightStartIndex..<text.index(after: highlightStartIndex)
|
||||||
|
let symbolString = text[symbolRange]
|
||||||
|
let toCursorRange = highlightStartIndex..<cursorIndex
|
||||||
|
let toCursorString = text[toCursorRange]
|
||||||
|
let toHighlightEndRange = matchStartIndex..<matchEndIndex
|
||||||
|
let toHighlightEndString = text[toHighlightEndRange]
|
||||||
|
|
||||||
|
let inputText = toHighlightEndString
|
||||||
|
let autoCompleteInfo = AutoCompleteInfo(
|
||||||
|
inputText: inputText,
|
||||||
|
symbolRange: symbolRange,
|
||||||
|
symbolString: symbolString,
|
||||||
|
toCursorRange: toCursorRange,
|
||||||
|
toCursorString: toCursorString,
|
||||||
|
toHighlightEndRange: toHighlightEndRange,
|
||||||
|
toHighlightEndString: toHighlightEndString
|
||||||
|
)
|
||||||
|
return autoCompleteInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -34,6 +34,10 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
// author (me)
|
// author (me)
|
||||||
@Published var authContext: AuthContext
|
@Published var authContext: AuthContext
|
||||||
|
|
||||||
|
// auto-complete info
|
||||||
|
@Published var autoCompleteRetryLayoutTimes = 0
|
||||||
|
@Published var autoCompleteInfo: AutoCompleteInfo? = nil
|
||||||
|
|
||||||
// output
|
// output
|
||||||
|
|
||||||
// limit
|
// limit
|
||||||
|
@ -98,9 +102,9 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
// UI & UX
|
// UI & UX
|
||||||
@Published var replyToCellFrame: CGRect = .zero
|
@Published var replyToCellFrame: CGRect = .zero
|
||||||
@Published var contentCellFrame: CGRect = .zero
|
@Published var contentCellFrame: CGRect = .zero
|
||||||
|
@Published var contentTextViewFrame: CGRect = .zero
|
||||||
@Published var scrollViewState: ScrollViewState = .fold
|
@Published var scrollViewState: ScrollViewState = .fold
|
||||||
|
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AppContext,
|
context: AppContext,
|
||||||
authContext: AuthContext,
|
authContext: AuthContext,
|
||||||
|
@ -203,13 +207,30 @@ extension ComposeContentViewModel {
|
||||||
case mention(user: ManagedObjectRecord<MastodonUser>)
|
case mention(user: ManagedObjectRecord<MastodonUser>)
|
||||||
case reply(status: ManagedObjectRecord<Status>)
|
case reply(status: ManagedObjectRecord<Status>)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ScrollViewState {
|
public enum ScrollViewState {
|
||||||
case fold // snap to input
|
case fold // snap to input
|
||||||
case expand // snap to reply
|
case expand // snap to reply
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ComposeContentViewModel {
|
||||||
|
struct AutoCompleteInfo {
|
||||||
|
// model
|
||||||
|
let inputText: Substring
|
||||||
|
// range
|
||||||
|
let symbolRange: Range<String.Index>
|
||||||
|
let symbolString: Substring
|
||||||
|
let toCursorRange: Range<String.Index>
|
||||||
|
let toCursorString: Substring
|
||||||
|
let toHighlightEndRange: Range<String.Index>
|
||||||
|
let toHighlightEndString: Substring
|
||||||
|
// geometry
|
||||||
|
var textBoundingRect: CGRect = .zero
|
||||||
|
var symbolBoundingRect: CGRect = .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ComposeContentViewModel {
|
extension ComposeContentViewModel {
|
||||||
func createNewPollOptionIfCould() {
|
func createNewPollOptionIfCould() {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||||
|
@ -286,77 +307,6 @@ extension ComposeContentViewModel {
|
||||||
} // end func publisher()
|
} // end func publisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITextViewDelegate
|
|
||||||
extension ComposeContentViewModel: UITextViewDelegate {
|
|
||||||
public func textViewDidBeginEditing(_ textView: UITextView) {
|
|
||||||
// Note:
|
|
||||||
// Xcode warning:
|
|
||||||
// Publishing changes from within view updates is not allowed, this will cause undefined behavior.
|
|
||||||
//
|
|
||||||
// Just ignore the warning and see what will happen…
|
|
||||||
switch textView {
|
|
||||||
case contentMetaText?.textView:
|
|
||||||
isContentEditing = true
|
|
||||||
case contentWarningMetaText?.textView:
|
|
||||||
isContentWarningEditing = true
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func textViewDidEndEditing(_ textView: UITextView) {
|
|
||||||
switch textView {
|
|
||||||
case contentMetaText?.textView:
|
|
||||||
isContentEditing = false
|
|
||||||
case contentWarningMetaText?.textView:
|
|
||||||
isContentWarningEditing = false
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
|
||||||
switch textView {
|
|
||||||
case contentMetaText?.textView:
|
|
||||||
return true
|
|
||||||
case contentWarningMetaText?.textView:
|
|
||||||
let isReturn = text == "\n"
|
|
||||||
if isReturn {
|
|
||||||
setContentTextViewFirstResponderIfNeeds()
|
|
||||||
}
|
|
||||||
return !isReturn
|
|
||||||
default:
|
|
||||||
assertionFailure()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func insertContentText(text: String) {
|
|
||||||
guard let contentMetaText = self.contentMetaText else { return }
|
|
||||||
// FIXME: smart prefix and suffix
|
|
||||||
let string = contentMetaText.textStorage.string
|
|
||||||
let isEmpty = string.isEmpty
|
|
||||||
let hasPrefix = string.hasPrefix(" ")
|
|
||||||
if hasPrefix || isEmpty {
|
|
||||||
contentMetaText.textView.insertText(text)
|
|
||||||
} else {
|
|
||||||
contentMetaText.textView.insertText(" " + text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setContentTextViewFirstResponderIfNeeds() {
|
|
||||||
guard let contentMetaText = self.contentMetaText else { return }
|
|
||||||
guard !contentMetaText.textView.isFirstResponder else { return }
|
|
||||||
contentMetaText.textView.becomeFirstResponder()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setContentWarningTextViewFirstResponderIfNeeds() {
|
|
||||||
guard let contentWarningMetaText = self.contentWarningMetaText else { return }
|
|
||||||
guard !contentWarningMetaText.textView.isFirstResponder else { return }
|
|
||||||
contentWarningMetaText.textView.becomeFirstResponder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - DeleteBackwardResponseTextFieldRelayDelegate
|
// MARK: - DeleteBackwardResponseTextFieldRelayDelegate
|
||||||
extension ComposeContentViewModel: DeleteBackwardResponseTextFieldRelayDelegate {
|
extension ComposeContentViewModel: DeleteBackwardResponseTextFieldRelayDelegate {
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,11 @@ public struct ComposeContentView: View {
|
||||||
static let logger = Logger(subsystem: "ComposeContentView", category: "View")
|
static let logger = Logger(subsystem: "ComposeContentView", category: "View")
|
||||||
var logger: Logger { ComposeContentView.logger }
|
var logger: Logger { ComposeContentView.logger }
|
||||||
|
|
||||||
|
static let contentViewCoordinateSpace = "ComposeContentView.Content"
|
||||||
static var margin: CGFloat = 16
|
static var margin: CGFloat = 16
|
||||||
|
|
||||||
@ObservedObject var viewModel: ComposeContentViewModel
|
@ObservedObject var viewModel: ComposeContentViewModel
|
||||||
|
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(spacing: .zero) {
|
VStack(spacing: .zero) {
|
||||||
|
@ -106,6 +108,19 @@ public struct ComposeContentView: View {
|
||||||
.frame(minHeight: 100)
|
.frame(minHeight: 100)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.padding(.horizontal, ComposeContentView.margin)
|
.padding(.horizontal, ComposeContentView.margin)
|
||||||
|
.background(
|
||||||
|
GeometryReader { proxy in
|
||||||
|
Color.clear.preference(key: ViewFramePreferenceKey.self, value: proxy.frame(in: .named(ComposeContentView.contentViewCoordinateSpace)))
|
||||||
|
}
|
||||||
|
.onPreferenceChange(ViewFramePreferenceKey.self) { frame in
|
||||||
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content textView frame: \(frame.debugDescription)")
|
||||||
|
let rect = frame.standardized
|
||||||
|
viewModel.contentTextViewFrame = CGRect(
|
||||||
|
origin: frame.origin,
|
||||||
|
size: CGSize(width: floor(rect.width), height: floor(rect.height))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
// poll
|
// poll
|
||||||
pollView
|
pollView
|
||||||
.padding(.horizontal, ComposeContentView.margin)
|
.padding(.horizontal, ComposeContentView.margin)
|
||||||
|
@ -128,6 +143,7 @@ public struct ComposeContentView: View {
|
||||||
)
|
)
|
||||||
Spacer()
|
Spacer()
|
||||||
} // end VStack
|
} // end VStack
|
||||||
|
.coordinateSpace(name: ComposeContentView.contentViewCoordinateSpace)
|
||||||
} // end body
|
} // end body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue