iosswiftuiswiftui-animationswiftui-textswiftui-toolbar

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!


Solution

  • 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.