From 1135094c216e8cd6bf8ff94636297cddb2929076 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 3 Mar 2025 19:05:47 -0500 Subject: [PATCH] Remove input accessory toolbar --- .../Sources/ComposeUI/ComposeInput.swift | 46 +--- .../Sources/ComposeUI/KeyboardReader.swift | 40 ---- .../Autocomplete/AutocompleteEmojiView.swift | 4 +- .../AutocompleteHashtagView.swift | 4 +- .../AutocompleteMentionView.swift | 4 +- .../ComposeUI/Views/ComposeToolbarView.swift | 18 +- .../Sources/ComposeUI/Views/ComposeView.swift | 211 +----------------- .../ComposeUI/Views/EmojiTextField.swift | 5 - .../ComposeUI/Views/NewMainTextView.swift | 7 - 9 files changed, 19 insertions(+), 320 deletions(-) delete mode 100644 Packages/ComposeUI/Sources/ComposeUI/KeyboardReader.swift diff --git a/Packages/ComposeUI/Sources/ComposeUI/ComposeInput.swift b/Packages/ComposeUI/Sources/ComposeUI/ComposeInput.swift index 2fab0422d3..a9c65ca49d 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/ComposeInput.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/ComposeInput.swift @@ -72,53 +72,17 @@ struct FocusedInputModifier: ViewModifier { } } -// In the input accessory view toolbar, we get the focused input through the box injected from the ComposeView. -// Otherwise we get it from @FocusedValue (which doesn't seem to work via the hacks we use for the input accessory). -// This property wrapper abstracts over them both. -@propertyWrapper -struct FocusedInput: DynamicProperty { - @FocusedValue(\.composeInput) private var input - #if !targetEnvironment(macCatalyst) && !os(visionOS) - @Environment(\.toolbarInjectedFocusedInputBox) private var box - @StateObject private var updater = Updater() - #endif - - var wrappedValue: (any ComposeInput)? { - #if !targetEnvironment(macCatalyst) && !os(visionOS) - box?.wrappedValue ?? input ?? nil - #else - input ?? nil - #endif - } - - #if !targetEnvironment(macCatalyst) && !os(visionOS) - func update() { - updater.update(box: box) - } - - private class Updater: ObservableObject { - private var cancellable: AnyCancellable? - - func update(box: MutableObservableBox<(any ComposeInput)?>?) { - cancellable = box?.objectWillChange.sink { [unowned self] _ in - self.objectWillChange.send() - } - } - } - #endif -} - @propertyWrapper struct FocusedInputAutocompleteState: DynamicProperty { - @FocusedInput private var input + @FocusedValue(\.composeInput) private var input @StateObject private var updater = Updater() var wrappedValue: AutocompleteState? { - input?.autocompleteState + input??.autocompleteState } func update() { - updater.update(input: input) + updater.update(input: input ?? nil) } @MainActor @@ -133,8 +97,8 @@ struct FocusedInputAutocompleteState: DynamicProperty { lastInput = input cancellable = input?.autocompleteStatePublisher.sink { [unowned self] _ in // the autocomplete state sometimes changes during a view update, so defer this - DispatchQueue.main.async { - self.objectWillChange.send() + DispatchQueue.main.async { [weak self] in + self?.objectWillChange.send() } } } diff --git a/Packages/ComposeUI/Sources/ComposeUI/KeyboardReader.swift b/Packages/ComposeUI/Sources/ComposeUI/KeyboardReader.swift deleted file mode 100644 index 69ebe96755..0000000000 --- a/Packages/ComposeUI/Sources/ComposeUI/KeyboardReader.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// KeyboardReader.swift -// ComposeUI -// -// Created by Shadowfacts on 3/7/23. -// - -#if !os(visionOS) - -import UIKit -import Combine - -@available(iOS, obsoleted: 16.0) -class KeyboardReader: ObservableObject { - @Published var keyboardHeight: CGFloat = 0 - - var isVisible: Bool { - // when a hardware keyboard is connected, the height is very short, so we don't consider that being "visible" - keyboardHeight > 72 - } - - init() { - NotificationCenter.default.addObserver(self, selector: #selector(willShow), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(willHide), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - @objc func willShow(_ notification: Foundation.Notification) { - let endFrame = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect - keyboardHeight = endFrame.height - } - - @objc func willHide() { - // sometimes willHide is called during a SwiftUI view update - DispatchQueue.main.async { - self.keyboardHeight = 0 - } - } -} - -#endif diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteEmojiView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteEmojiView.swift index 43fb718f51..72dd2e87a3 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteEmojiView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteEmojiView.swift @@ -144,12 +144,12 @@ private struct ExpandedEmojiSection: View { private struct AutocompleteEmojiButton: View { let emoji: Emoji - @FocusedInput private var input + @FocusedValue(\.composeInput) private var input @Environment(\.composeUIDelegate) private var delegate var body: some View { Button { - input?.autocomplete(with: ":\(emoji.shortcode):") + input??.autocomplete(with: ":\(emoji.shortcode):") } label: { Label { Text(verbatim: ":\(emoji.shortcode):") diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteHashtagView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteHashtagView.swift index 3abcbbbd03..4b23d28e03 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteHashtagView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteHashtagView.swift @@ -14,7 +14,7 @@ struct AutocompleteHashtagView: View { let mastodonController: any ComposeMastodonContext @State private var loading = false @State private var hashtags: [Hashtag] = [] - @FocusedInput private var input + @FocusedValue(\.composeInput) private var input var body: some View { ScrollView(.horizontal) { @@ -90,6 +90,6 @@ struct AutocompleteHashtagView: View { } private func autocomplete(hashtag: Hashtag) { - input?.autocomplete(with: "#\(hashtag.name)") + input??.autocomplete(with: "#\(hashtag.name)") } } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteMentionView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteMentionView.swift index f6dbe9969e..c8f4d2982f 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteMentionView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/Autocomplete/AutocompleteMentionView.swift @@ -15,7 +15,7 @@ struct AutocompleteMentionView: View { let mastodonController: any ComposeMastodonContext @State private var loading = false @State private var accounts: [AnyAccount] = [] - @FocusedInput private var input + @FocusedValue(\.composeInput) private var input var body: some View { ScrollView(.horizontal) { @@ -48,7 +48,7 @@ struct AutocompleteMentionView: View { } private func autocomplete(account: AnyAccount) { - input?.autocomplete(with: "@\(account.value.acct)") + input??.autocomplete(with: "@\(account.value.acct)") } private func queryChanged() async { diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeToolbarView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeToolbarView.swift index 2219575fc8..611a4bf71c 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeToolbarView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeToolbarView.swift @@ -207,11 +207,11 @@ private struct LocalOnlyButton: View { } private struct InsertEmojiButton: View { - @FocusedInput private var input + @FocusedValue(\.composeInput) private var input @ScaledMetric(relativeTo: .body) private var imageSize: CGFloat = 22 var body: some View { - if input?.toolbarElements.contains(.emojiPicker) == true { + if input??.toolbarElements.contains(.emojiPicker) == true { Button(action: beginAutocompletingEmoji) { Label("Insert custom emoji", systemImage: "face.smiling") } @@ -225,6 +225,7 @@ private struct InsertEmojiButton: View { private func beginAutocompletingEmoji() { if let input, + let input, input.autocompleteState == nil { input.beginAutocompletingEmoji() } @@ -232,11 +233,12 @@ private struct InsertEmojiButton: View { } private struct FormatButtons: View { - @FocusedInput private var input + @FocusedValue(\.composeInput) private var input @PreferenceObserving(\.$statusContentType) private var contentType var body: some View { if let input, + let input, input.toolbarElements.contains(.formattingButtons), contentType != .plain { @@ -269,16 +271,6 @@ private struct FormatButton: View { } } -private struct InputAccessoryToolbarHost: EnvironmentKey { - static var defaultValue: UIView? { nil } -} -extension EnvironmentValues { - var inputAccessoryToolbarHost: UIView? { - get { self[InputAccessoryToolbarHost.self] } - set { self[InputAccessoryToolbarHost.self] = newValue } - } -} - //#Preview { // ComposeToolbarView() //} diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeView.swift index 055e464711..683dc785c6 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeView.swift @@ -59,14 +59,6 @@ public struct ComposeView: View { .environment(\.composeUIConfig, config) .environment(\.composeUIDelegate, delegate) .environment(\.currentAccount, currentAccount) - #if !targetEnvironment(macCatalyst) && !os(visionOS) - .injectInputAccessoryHost( - state: state, - mastodonController: mastodonController, - focusedField: $focusedField, - delegate: delegate - ) - #endif .onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: DraftsPersistentContainer.shared.viewContext), perform: self.managedObjectsDidChange) } @@ -154,9 +146,6 @@ private struct ComposeViewBody: View { #if !os(visionOS) .scrollDismissesKeyboardInteractivelyIfAvailable() #endif - #if !os(visionOS) && !targetEnvironment(macCatalyst) - .modifier(ToolbarSafeAreaInsetModifier()) - #endif } .overlay(alignment: .top) { if let poster = state.poster { @@ -165,33 +154,13 @@ private struct ComposeViewBody: View { } } #if !os(visionOS) - .overlay(alignment: .bottom, content: { - // This needs to be in an overlay, ignoring the keyboard safe area - // doesn't work with the safeAreaInset modifier. - - // When we're using the input accessory toolbar, hide the overlay toolbar so that, - // on iPad, we don't get two toolbars showing. - #if targetEnvironment(macCatalyst) || os(visionOS) - let showOverlayToolbar = true - #else - let showOverlayToolbar = if #available(iOS 16.0, *) { - focusedField == nil - } else { - true - } - #endif - - if config.showToolbar, - showOverlayToolbar { + .safeAreaInset(edge: .bottom) { + if config.showToolbar { toolbarView - .frame(maxHeight: .infinity, alignment: .bottom) - #if !targetEnvironment(macCatalyst) && !os(visionOS) - .modifier(IgnoreKeyboardSafeAreaIfUsingInputAccessory()) - #endif .transition(.move(edge: .bottom)) .animation(.snappy, value: config.showToolbar) } - }) + } #endif // Have these after the overlays so they barely work instead of not working at all. FB11790805 .modifier(DropAttachmentModifier(draft: draft)) @@ -304,180 +273,6 @@ enum FocusableField: Hashable { case pollOption(NSManagedObjectID) } -#if !os(visionOS) && !targetEnvironment(macCatalyst) -private struct ToolbarSafeAreaInsetModifier: ViewModifier { - @StateObject private var keyboardReader = KeyboardReader() - @FocusedInputAutocompleteState private var autocompleteState - - private var inset: CGFloat { - var height: CGFloat = 0 - if keyboardReader.isVisible { - height += ComposeToolbarView.toolbarHeight - } - if autocompleteState != nil { - height += ComposeToolbarView.autocompleteHeight - } - return height - } - - func body(content: Content) -> some View { - if #available(iOS 17.0, *) { - content - .safeAreaPadding(.bottom, inset) - } else { - content - .safeAreaInset(edge: .bottom) { - if !keyboardReader.isVisible { - Color.clear.frame(height: inset) - } - } - } - } -} -#endif - -#if !targetEnvironment(macCatalyst) && !os(visionOS) -private struct IgnoreKeyboardSafeAreaIfUsingInputAccessory: ViewModifier { - func body(content: Content) -> some View { - if #available(iOS 16.0, *) { - content - .ignoresSafeArea(.keyboard) - } else { - content - } - } -} - -private extension View { - @ViewBuilder - func injectInputAccessoryHost( - state: ComposeViewState, - mastodonController: any ComposeMastodonContext, - focusedField: FocusState.Binding, - delegate: (any ComposeUIDelegate)? - ) -> some View { - if #available(iOS 16.0, *) { - self.modifier( - InputAccessoryHostInjector( - state: state, - mastodonController: mastodonController, - focusedField: focusedField, - delegate: delegate - ) - ) - } else { - self - } - } -} - -@available(iOS 16.0, *) -private struct InputAccessoryHostInjector: ViewModifier { - // This is in a StateObject so we can use the autoclosure StateObject initializer. - @StateObject private var factory: ViewFactory - @FocusedValue(\.composeInput) private var composeInput - - init( - state: ComposeViewState, - mastodonController: any ComposeMastodonContext, - focusedField: FocusState.Binding, - delegate: (any ComposeUIDelegate)? - ) { - self._factory = StateObject( - wrappedValue: ViewFactory( - state: state, - mastodonController: mastodonController, - focusedField: focusedField, - delegate: delegate - ) - ) - } - - func body(content: Content) -> some View { - content - .environment(\.inputAccessoryToolbarHost, factory.controller.view) - .onChange(of: ComposeInputEquatableBox(input: composeInput ?? nil)) { newValue in - factory.focusedInput = newValue.input ?? nil - } - } - - @MainActor - private class ViewFactory: ObservableObject { - let controller: UIViewController - @MutableObservableBox var focusedInput: (any ComposeInput)? - - init( - state: ComposeViewState, - mastodonController: any ComposeMastodonContext, - focusedField: FocusState.Binding, - delegate: (any ComposeUIDelegate)? - ) { - self._focusedInput = MutableObservableBox(wrappedValue: nil) - let view = InputAccessoryToolbarView( - state: state, - mastodonController: mastodonController, - focusedField: focusedField, - focusedInputBox: _focusedInput, - delegate: delegate - ) - let controller = UIHostingController(rootView: view) - controller.sizingOptions = .intrinsicContentSize - controller.view.autoresizingMask = .flexibleHeight - self.controller = controller - } - } -} - -private struct ComposeInputEquatableBox: Equatable { - let input: (any ComposeInput)? - - static func ==(lhs: Self, rhs: Self) -> Bool { - lhs.input === rhs.input - } -} - -/// FocusedValue doesn't seem to work through the hacks we're doing for the input accessory. -private struct FocusedInputBoxEnvironmentKey: EnvironmentKey { - static var defaultValue: MutableObservableBox<(any ComposeInput)?>? { nil } -} -extension EnvironmentValues { - var toolbarInjectedFocusedInputBox: MutableObservableBox<(any ComposeInput)?>? { - get { self[FocusedInputBoxEnvironmentKey.self] } - set { self[FocusedInputBoxEnvironmentKey.self] = newValue } - } -} - -private struct InputAccessoryToolbarView: View { - @ObservedObject var state: ComposeViewState - let mastodonController: any ComposeMastodonContext - @FocusState.Binding var focusedField: FocusableField? - let focusedInputBox: MutableObservableBox<(any ComposeInput)?> - let delegate: (any ComposeUIDelegate)? - @PreferenceObserving(\.$accentColor) private var accentColor - - init( - state: ComposeViewState, - mastodonController: any ComposeMastodonContext, - focusedField: FocusState.Binding, - focusedInputBox: MutableObservableBox<(any ComposeInput)?>, - delegate: (any ComposeUIDelegate)? - ) { - self.state = state - self.mastodonController = mastodonController - self._focusedField = focusedField - self.focusedInputBox = focusedInputBox - self.delegate = delegate - } - - var body: some View { - ComposeToolbarView(draft: state.draft, mastodonController: mastodonController, focusedField: $focusedField) - .environment(\.toolbarInjectedFocusedInputBox, focusedInputBox) - .environment(\.composeUIDelegate, delegate) - .tint(accentColor.color.map(Color.init(uiColor:))) - } -} -#endif - //#Preview { // ComposeView() //} diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift index babd858a89..7f7f1f34a9 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift @@ -13,7 +13,6 @@ struct EmojiTextField: UIViewRepresentable { @Environment(\.composeUIConfig.fillColor) private var fillColor @Environment(\.colorScheme) private var colorScheme @Environment(\.composeInputBox) private var inputBox - @Environment(\.inputAccessoryToolbarHost) private var inputAccessoryToolbarHost @Binding var text: String let placeholder: String @@ -64,10 +63,6 @@ struct EmojiTextField: UIViewRepresentable { #if !os(visionOS) uiView.backgroundColor = colorScheme == .dark ? UIColor(fillColor) : .secondarySystemBackground - - if uiView.inputAccessoryView !== inputAccessoryToolbarHost { - uiView.inputAccessoryView = inputAccessoryToolbarHost - } #endif } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/NewMainTextView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/NewMainTextView.swift index a46bc4e093..4f545e548b 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/NewMainTextView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/NewMainTextView.swift @@ -41,7 +41,6 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable { @PreferenceObserving(\.$useTwitterKeyboard) private var useTwitterKeyboard @Environment(\.composeUIConfig.textSelectionStartsAtBeginning) private var textSelectionStartsAtBeginning @PreferenceObserving(\.$statusContentType) private var statusContentType - @Environment(\.inputAccessoryToolbarHost) private var inputAccessoryToolbarHost func makeUIView(context: Context) -> WrappedTextView { // TODO: if we're not doing the pill background, reevaluate whether this version fork is necessary @@ -95,12 +94,6 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable { // uiView.backgroundColor = colorScheme == .dark ? UIColor(fillColor) : .secondarySystemBackground // #endif - #if !os(visionOS) - if uiView.inputAccessoryView !== inputAccessoryToolbarHost { - uiView.inputAccessoryView = inputAccessoryToolbarHost - } - #endif - // Trying to set this with the @FocusState binding in onAppear results in the // keyboard not appearing until after the sheet presentation animation completes :/ if becomeFirstResponder {