forked from shadowfacts/Tusker
Remove input accessory toolbar
This commit is contained in:
parent
84ed9e92ee
commit
1135094c21
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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):")
|
||||
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
//}
|
||||
|
@ -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<FocusableField?>.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<FocusableField?>.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<FocusableField?>.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<FocusableField?>.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()
|
||||
//}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user