Dave Feldman
Dave Feldman

Reputation: 184

Present sheet with a TextField and its keyboard in a single animation?

I'm building a SwiftUI to-do app. You tap an Add button that pulls up a partial-height sheet where you can enter and save a new to-do. The Add sheet's input (TextField) should be focused when the sheet appears, so in order to keep things feeling fast and smooth, I'd like the sheet and the keyboard to animate onscreen together, at the same time. After much experimentation and Googling, I still can't figure out how to do it.

It seems like there are two paths to doing something like this:

(1) Autofocus the sheet I can use @FocusState and .onAppear or .task inside the sheet to ensure the TextField is focused as soon as it comes up. It's straightforward functionally, but I can't find a permutation of it that will give me that single animation: it's sheet, then keyboard, presumably because those modifiers don't fire until the sheet is onscreen.

(2) Keyboard accessory view / toolbar The .toolbar modifier seems tailor-made for a view of custom height that sticks to the keyboard--you lose the nice sheet animation but you gain the ability to have the view auto-size. However, .toolbar is designed to present controls alongside a TextField that itself isn't stuck to the keyboard. That is, the field has to be onscreen before the keyboard so it can receive focus...I don't know of a way to put the input itself inside the toolbar. Seems like chat apps have found a way to do this but I don't know what it is.

Any help would be much appreciated! Thanks!

Upvotes: 3

Views: 1833

Answers (2)

Kaz Okui
Kaz Okui

Reputation: 1

I was able to make the view appears in a single animation by inserting hidden UITextField behind SwiftUI's TextField or SecureField and calling becomeFirstRespoder when the UITextField is initialized, like this:

class FirstResponderField: UITextField {
    init() {
        super.init(frame: .zero)
        becomeFirstResponder()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

struct FirstResponderFieldView: UIViewRepresentable {
    func makeUIView(context: Context) -> FirstResponderField {
        return FirstResponderField()
    }

    func updateUIView(_ uiView: FirstResponderField, context: Context) {}
}

struct MyView: View {

    @FocusState var isFocused: Bool

    var body: some View {
        ZStack {
            FirstResponderFieldView() // this makes the keyboard to appear with a single animation
                .frame(width: 0, height: 0)
                .opacity(0)
            TextField("Email", text: $text)
                .focused($isFocused)

        }
        .onAppear {
            isFocused = true // After the view appears, you want to focus to actual SwiftUI view.
        }
    }
}

I created a small library called focusOnAppear to achieve this using a view modifier like this:

https://github.com/naan/FocusOnAppear

TextField("text", text: $text)
  .focusOnAppear()

Upvotes: 0

viedev
viedev

Reputation: 1527

Regarding option (1), I think there is no way to sync the animation. I decided to do it this way and don't worry about the delay between sheet and keyboard animation. Regarding option (2), you could try something like this:

struct ContentView: View {
    @State var text = ""
    @FocusState var isFocused: Bool
    @FocusState var isFocusedInToolbar: Bool

    var body: some View {
        Button("Show Keyboard") {
            isFocused = true
        }
        .opacity(isFocusedInToolbar ? 0 : 1)

        TextField("Enter Text", text: $text)            // Invisible Proxy TextField
            .focused($isFocused)
            .opacity(0)
            .toolbar {
                ToolbarItem(placement: .keyboard) {
                    HStack {
                        TextField("", text: $text)          // Toolbar TextField
                            .textFieldStyle(.roundedBorder)
                            .focused($isFocusedInToolbar)
                        Button("Done") {
                                isFocused = false
                                isFocusedInToolbar = false
                                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                        }
                    }
                }
            }
            .onChange(of: isFocused) { newValue in
                if newValue {
                    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.05) {
                        isFocusedInToolbar = true
                    }
                }
            }
    }
}

The trick is, that you need a TextField in your content that triggers the keyboard initally and then switch focus to the TextField in the toolbar. Otherwise you won't get the keyboard to show up.

Upvotes: 3

Related Questions