diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index 4c30cc9..9ee2c4d 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -382,6 +382,18 @@ public extension ContentDatabase { .eraseToAnyPublisher() } + func update(emojis: [Emoji]) -> AnyPublisher { + databaseWriter.writePublisher { + for emoji in emojis { + try emoji.save($0) + } + + try Emoji.filter(!emojis.map(\.shortcode).contains(Emoji.Columns.shortcode)).deleteAll($0) + } + .ignoreOutput() + .eraseToAnyPublisher() + } + func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> { ValueObservation.tracking( TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne) @@ -492,6 +504,13 @@ public extension ContentDatabase { .eraseToAnyPublisher() } + func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> { + ValueObservation.tracking(Emoji.filter(Emoji.Columns.visibleInPicker == true).fetchAll) + .removeDuplicates() + .publisher(in: databaseWriter) + .eraseToAnyPublisher() + } + func lastReadId(_ markerTimeline: Marker.Timeline) -> String? { try? databaseWriter.read { try String.fetchOne( diff --git a/DB/Sources/DB/Extensions/Emoji+Extensions.swift b/DB/Sources/DB/Extensions/Emoji+Extensions.swift new file mode 100644 index 0000000..e1c314d --- /dev/null +++ b/DB/Sources/DB/Extensions/Emoji+Extensions.swift @@ -0,0 +1,17 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import GRDB +import Mastodon + +extension Emoji: ContentDatabaseRecord {} + +extension Emoji { + enum Columns: String, ColumnExpression { + case shortcode + case staticUrl + case url + case visibleInPicker + case category + } +} diff --git a/Mastodon/Sources/Mastodon/Entities/Emoji.swift b/Mastodon/Sources/Mastodon/Entities/Emoji.swift index df39dd3..e1bea17 100644 --- a/Mastodon/Sources/Mastodon/Entities/Emoji.swift +++ b/Mastodon/Sources/Mastodon/Entities/Emoji.swift @@ -7,4 +7,5 @@ public struct Emoji: Codable, Hashable { public let staticUrl: URL public let url: URL public let visibleInPicker: Bool + public let category: String? } diff --git a/MastodonAPI/Sources/MastodonAPI/Endpoints/EmojisEndpoint.swift b/MastodonAPI/Sources/MastodonAPI/Endpoints/EmojisEndpoint.swift new file mode 100644 index 0000000..8052c3b --- /dev/null +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/EmojisEndpoint.swift @@ -0,0 +1,21 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import HTTP +import Mastodon + +public enum EmojisEndpoint { + case customEmojis +} + +extension EmojisEndpoint: Endpoint { + public typealias ResultType = [Emoji] + + public var pathComponentsInContext: [String] { + ["custom_emojis"] + } + + public var method: HTTPMethod { + .get + } +} diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index f50ee0d..e83b1aa 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -55,6 +55,8 @@ D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; }; D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; }; D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; }; + D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; }; + D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; }; D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */; }; D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */; }; D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */; }; @@ -211,6 +213,7 @@ D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+Extensions.swift"; sourceTree = ""; }; D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = ""; }; + D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewController.swift; sourceTree = ""; }; D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = ""; }; D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePageViewController.swift; sourceTree = ""; }; D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageNavigationController.swift; sourceTree = ""; }; @@ -511,6 +514,7 @@ isa = PBXGroup; children = ( D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */, + D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */, D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */, D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */, D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */, @@ -866,6 +870,7 @@ D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */, D0FCC110259C4F20000B67DF /* NewStatusView.swift in Sources */, D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */, + D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */, D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -904,6 +909,7 @@ D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */, D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */, D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */, + D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */, D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */, D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */, ); diff --git a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift index d22354f..c0b44b8 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift @@ -62,6 +62,12 @@ public extension IdentityService { .eraseToAnyPublisher() } + func refreshEmojis() -> AnyPublisher { + mastodonAPIClient.request(EmojisEndpoint.customEmojis) + .flatMap(contentDatabase.update(emojis:)) + .eraseToAnyPublisher() + } + func confirmIdentity() -> AnyPublisher { identityDatabase.confirmIdentity(id: id) } @@ -165,6 +171,10 @@ public extension IdentityService { contentDatabase.expiredFiltersPublisher() } + func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> { + contentDatabase.pickerEmojisPublisher() + } + func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher { identityDatabase.updatePreferences(preferences, id: id) .collect() diff --git a/View Controllers/EmojiPickerViewController.swift b/View Controllers/EmojiPickerViewController.swift new file mode 100644 index 0000000..bdb4e91 --- /dev/null +++ b/View Controllers/EmojiPickerViewController.swift @@ -0,0 +1,63 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Combine +import UIKit +import ViewModels + +final class EmojiPickerViewController: UIViewController { + let searchBar = UISearchBar() + + private let viewModel: EmojiPickerViewModel + private var cancellables = Set() + + init(viewModel: EmojiPickerViewModel) { + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let searchBar = UISearchBar() + + view.addSubview(searchBar) + searchBar.translatesAutoresizingMaskIntoConstraints = false + searchBar.searchBarStyle = .minimal + + NSLayoutConstraint.activate([ + searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + searchBar.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + +// print(UITextInputMode.activeInputModes.map(\.primaryLanguage)) + print(Locale.availableIdentifiers) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + guard let containerView = popoverPresentationController?.containerView else { return } + + // gets the popover presentation controller's built-in visual effect view to actually show + func setClear(view: UIView) { + view.backgroundColor = .clear + + if view == self.view { + return + } + + for view in view.subviews { + setClear(view: view) + } + } + + setClear(view: containerView) + } +} diff --git a/View Controllers/NewStatusViewController.swift b/View Controllers/NewStatusViewController.swift index ac59527..768749a 100644 --- a/View Controllers/NewStatusViewController.swift +++ b/View Controllers/NewStatusViewController.swift @@ -8,6 +8,7 @@ import SwiftUI import UniformTypeIdentifiers import ViewModels +// swiftlint:disable file_length final class NewStatusViewController: UIViewController { private let viewModel: NewStatusViewModel private let scrollView = UIScrollView() @@ -121,6 +122,13 @@ extension NewStatusViewController: UIDocumentPickerDelegate { } } +extension NewStatusViewController: UIPopoverPresentationControllerDelegate { + func adaptivePresentationStyle(for controller: UIPresentationController, + traitCollection: UITraitCollection) -> UIModalPresentationStyle { + .none + } +} + // Required by UIImagePickerController extension NewStatusViewController: UINavigationControllerDelegate {} @@ -135,6 +143,8 @@ private extension NewStatusViewController { #endif case let .presentDocumentPicker(compositionViewModel): presentDocumentPicker(compositionViewModel: compositionViewModel) + case let .presentEmojiPicker(tag): + presentEmojiPicker(tag: tag) case let .editAttachment(attachmentViewModel, compositionViewModel): presentAttachmentEditor( attachmentViewModel: attachmentViewModel, @@ -222,7 +232,10 @@ private extension NewStatusViewController { .store(in: &cancellables) viewModel.$alertItem .compactMap { $0 } - .sink { [weak self] in self?.present(alertItem: $0) } + .sink { [weak self] in + self?.dismissEmojiPickerIfPresented() + self?.present(alertItem: $0) + } .store(in: &cancellables) } @@ -258,6 +271,7 @@ private extension NewStatusViewController { picker.modalPresentationStyle = .overFullScreen picker.delegate = self + dismissEmojiPickerIfPresented() present(picker, animated: true) } @@ -280,7 +294,6 @@ private extension NewStatusViewController { alertController.addAction(openSystemSettingsAction) alertController.addAction(cancelAction) - present(alertController, animated: true) return @@ -310,6 +323,7 @@ private extension NewStatusViewController { picker.mediaTypes = [UTType.image.description] } + dismissEmojiPickerIfPresented() present(picker, animated: true) } #endif @@ -332,16 +346,49 @@ private extension NewStatusViewController { documentPickerController.delegate = self documentPickerController.allowsMultipleSelection = false documentPickerController.modalPresentationStyle = .overFullScreen - + dismissEmojiPickerIfPresented() present(documentPickerController, animated: true) } + func presentEmojiPicker(tag: Int) { + if dismissEmojiPickerIfPresented() { + return + } + + guard let fromView = view.viewWithTag(tag) else { return } + + let emojiPickerController = EmojiPickerViewController( + viewModel: .init(identification: viewModel.identification)) + + emojiPickerController.searchBar.inputAccessoryView = fromView.inputAccessoryView + emojiPickerController.preferredContentSize = view.frame.size + emojiPickerController.modalPresentationStyle = .popover + emojiPickerController.popoverPresentationController?.delegate = self + emojiPickerController.popoverPresentationController?.sourceView = fromView + emojiPickerController.popoverPresentationController?.sourceRect = fromView.bounds + emojiPickerController.popoverPresentationController?.backgroundColor = .clear + + present(emojiPickerController, animated: true) + } + + @discardableResult + func dismissEmojiPickerIfPresented() -> Bool { + let emojiPickerPresented = presentedViewController is EmojiPickerViewController + + if emojiPickerPresented { + dismiss(animated: true) + } + + return emojiPickerPresented + } + func presentAttachmentEditor(attachmentViewModel: AttachmentViewModel, compositionViewModel: CompositionViewModel) { let editAttachmentsView = EditAttachmentView { (attachmentViewModel, compositionViewModel) } let editAttachmentViewController = UIHostingController(rootView: editAttachmentsView) let navigationController = UINavigationController(rootViewController: editAttachmentViewController) navigationController.modalPresentationStyle = .overFullScreen + dismissEmojiPickerIfPresented() present(navigationController, animated: true) } @@ -383,3 +430,4 @@ private extension NewStatusViewController { return changeIdentityButton } } +// swiftlint:enable file_length diff --git a/ViewModels/Sources/ViewModels/EmojiPickerViewModel.swift b/ViewModels/Sources/ViewModels/EmojiPickerViewModel.swift new file mode 100644 index 0000000..5f769cb --- /dev/null +++ b/ViewModels/Sources/ViewModels/EmojiPickerViewModel.swift @@ -0,0 +1,12 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Combine +import Foundation + +final public class EmojiPickerViewModel: ObservableObject { + private let identification: Identification + + public init(identification: Identification) { + self.identification = identification + } +} diff --git a/ViewModels/Sources/ViewModels/NavigationViewModel.swift b/ViewModels/Sources/ViewModels/NavigationViewModel.swift index d1625b7..c5e834d 100644 --- a/ViewModels/Sources/ViewModels/NavigationViewModel.swift +++ b/ViewModels/Sources/ViewModels/NavigationViewModel.swift @@ -120,6 +120,9 @@ public extension NavigationViewModel { identification.service.refreshFilters() .sink { _ in } receiveValue: { _ in } .store(in: &cancellables) + identification.service.refreshEmojis() + .sink { _ in } receiveValue: { _ in } + .store(in: &cancellables) if identification.identity.preferences.useServerPostingReadingPreferences { identification.service.refreshServerPreferences() diff --git a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift index 84fbc0f..09aeb47 100644 --- a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift @@ -66,6 +66,7 @@ public extension NewStatusViewModel { case presentMediaPicker(CompositionViewModel) case presentCamera(CompositionViewModel) case presentDocumentPicker(CompositionViewModel) + case presentEmojiPicker(Int) case editAttachment(AttachmentViewModel, CompositionViewModel) } @@ -106,6 +107,10 @@ public extension NewStatusViewModel { eventsSubject.send(.presentDocumentPicker(viewModel)) } + func presentEmojiPicker(tag: Int) { + eventsSubject.send(.presentEmojiPicker(tag)) + } + func remove(viewModel: CompositionViewModel) { compositionViewModels.removeAll { $0 === viewModel } } diff --git a/Views/CompositionInputAccessoryView.swift b/Views/CompositionInputAccessoryView.swift index 313248f..11c772d 100644 --- a/Views/CompositionInputAccessoryView.swift +++ b/Views/CompositionInputAccessoryView.swift @@ -10,6 +10,7 @@ final class CompositionInputAccessoryView: UIView { let visibilityButton = UIButton() let addButton = UIButton() let contentWarningButton = UIButton(type: .system) + let tagForInputView = UUID().hashValue private let viewModel: CompositionViewModel private let parentViewModel: NewStatusViewModel @@ -111,6 +112,19 @@ private extension CompositionInputAccessoryView { UIAction { [weak self] _ in self?.viewModel.displayContentWarning.toggle() }, for: .touchUpInside) + let emojiButton = UIButton(primaryAction: UIAction { [weak self] _ in + guard let self = self else { return } + + self.parentViewModel.presentEmojiPicker(tag: self.tagForInputView) + }) + + stackView.addArrangedSubview(emojiButton) + emojiButton.setImage( + UIImage( + systemName: "face.smiling", + withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), + for: .normal) + stackView.addArrangedSubview(UIView()) let charactersLabel = UILabel() @@ -155,7 +169,7 @@ private extension CompositionInputAccessoryView { } .store(in: &cancellables) - for button in [attachmentButton, pollButton, visibilityButton, contentWarningButton, addButton] { + for button in [attachmentButton, pollButton, visibilityButton, contentWarningButton, emojiButton, addButton] { button.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true button.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true } diff --git a/Views/CompositionPollOptionView.swift b/Views/CompositionPollOptionView.swift index 5d22169..92a102d 100644 --- a/Views/CompositionPollOptionView.swift +++ b/Views/CompositionPollOptionView.swift @@ -8,15 +8,15 @@ final class CompositionPollOptionView: UIView { let option: CompositionViewModel.PollOption let removeButton = UIButton(type: .close) private let viewModel: CompositionViewModel - private let compositionInputAccessoryView: CompositionInputAccessoryView + private let parentViewModel: NewStatusViewModel private var cancellables = Set() init(viewModel: CompositionViewModel, - option: CompositionViewModel.PollOption, - inputAccessoryView: CompositionInputAccessoryView) { + parentViewModel: NewStatusViewModel, + option: CompositionViewModel.PollOption) { self.viewModel = viewModel + self.parentViewModel = parentViewModel self.option = option - self.compositionInputAccessoryView = inputAccessoryView super.init(frame: .zero) @@ -44,7 +44,11 @@ private extension CompositionPollOptionView { textField.borderStyle = .roundedRect textField.adjustsFontForContentSizeCategory = true textField.font = .preferredFont(forTextStyle: .body) - textField.inputAccessoryView = compositionInputAccessoryView + let textInputAccessoryView = CompositionInputAccessoryView( + viewModel: viewModel, + parentViewModel: parentViewModel) + textField.inputAccessoryView = textInputAccessoryView + textField.tag = textInputAccessoryView.tagForInputView textField.addAction( UIAction { [weak self] _ in self?.option.text = textField.text ?? "" }, diff --git a/Views/CompositionPollView.swift b/Views/CompositionPollView.swift index 89f2327..a4d480e 100644 --- a/Views/CompositionPollView.swift +++ b/Views/CompositionPollView.swift @@ -6,13 +6,13 @@ import ViewModels final class CompositionPollView: UIView { private let viewModel: CompositionViewModel - private let compositionInputAccessoryView: CompositionInputAccessoryView + private let parentViewModel: NewStatusViewModel private let stackView = UIStackView() private var cancellables = Set() - init(viewModel: CompositionViewModel, inputAccessoryView: CompositionInputAccessoryView) { + init(viewModel: CompositionViewModel, parentViewModel: NewStatusViewModel) { self.viewModel = viewModel - self.compositionInputAccessoryView = inputAccessoryView + self.parentViewModel = parentViewModel super.init(frame: .zero) @@ -118,8 +118,8 @@ private extension CompositionPollView { if !self.pollOptionViews.contains(where: { $0.option === option }) { let optionView = CompositionPollOptionView( viewModel: self.viewModel, - option: option, - inputAccessoryView: self.compositionInputAccessoryView) + parentViewModel: self.parentViewModel, + option: option) self.stackView.insertArrangedSubview(optionView, at: index) } diff --git a/Views/CompositionView.swift b/Views/CompositionView.swift index 39e2ad8..0226867 100644 --- a/Views/CompositionView.swift +++ b/Views/CompositionView.swift @@ -13,7 +13,6 @@ final class CompositionView: UIView { let removeButton = UIButton(type: .close) let inReplyToView = UIView() let hasReplyFollowingView = UIView() - let compositionInputAccessoryView: CompositionInputAccessoryView let attachmentsView = AttachmentsView() let attachmentUploadView: AttachmentUploadView let pollView: CompositionPollView @@ -27,12 +26,9 @@ final class CompositionView: UIView { self.viewModel = viewModel self.parentViewModel = parentViewModel - compositionInputAccessoryView = CompositionInputAccessoryView( - viewModel: viewModel, - parentViewModel: parentViewModel) attachmentUploadView = AttachmentUploadView(viewModel: viewModel) markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel) - pollView = CompositionPollView(viewModel: viewModel, inputAccessoryView: compositionInputAccessoryView) + pollView = CompositionPollView(viewModel: viewModel, parentViewModel: parentViewModel) super.init(frame: .zero) @@ -75,12 +71,17 @@ private extension CompositionView { stackView.axis = .vertical stackView.spacing = .defaultSpacing + let spoilerTextinputAccessoryView = CompositionInputAccessoryView( + viewModel: viewModel, + parentViewModel: parentViewModel) + stackView.addArrangedSubview(spoilerTextField) spoilerTextField.borderStyle = .roundedRect spoilerTextField.adjustsFontForContentSizeCategory = true spoilerTextField.font = .preferredFont(forTextStyle: .body) spoilerTextField.placeholder = NSLocalizedString("status.spoiler-text-placeholder", comment: "") - spoilerTextField.inputAccessoryView = compositionInputAccessoryView + spoilerTextField.inputAccessoryView = spoilerTextinputAccessoryView + spoilerTextField.tag = spoilerTextinputAccessoryView.tagForInputView spoilerTextField.addAction( UIAction { [weak self] _ in guard let self = self, let text = self.spoilerTextField.text else { return } @@ -90,6 +91,9 @@ private extension CompositionView { for: .editingChanged) let textViewFont = UIFont.preferredFont(forTextStyle: .body) + let textInputAccessoryView = CompositionInputAccessoryView( + viewModel: viewModel, + parentViewModel: parentViewModel) stackView.addArrangedSubview(textView) textView.isScrollEnabled = false @@ -97,7 +101,8 @@ private extension CompositionView { textView.font = textViewFont textView.textContainerInset = .zero textView.textContainer.lineFragmentPadding = 0 - textView.inputAccessoryView = compositionInputAccessoryView + textView.inputAccessoryView = textInputAccessoryView + textView.tag = textInputAccessoryView.tagForInputView textView.inputAccessoryView?.sizeToFit() textView.delegate = self