Mango Snippets » #3

SwiftUI: How To Programmatically Make a TextField First Responder

It's a common scenario to make a text field first responder when it first appears. A typical example is the login screen. As soon as it appears, you want the account field to be in focus and the keyboard to appear. Then the user can start typing right way.

SwiftUI in iOS 15 introduced a new property wrapper called @FocusState. It allows you to control which input has focus. It can be bound to a Bool or an enum:

struct ContentView: View {
    @FocusState private var focused: Bool
    @State private var name = "Mango Umbrella"

    var body: some View {
        VStack {
            TextField("Name", text: self.$name)
                .focused(self.$focused)
            Button("Focus on name") {
                self.focused = true
            }
        }
    }

But how can you make the text field focus as soon as the view appears? You can't assign an initial value to the property. Another thought is to change its value in .onAppear like this:

struct ContentView: View {
    @FocusState private var focused: Bool

    var body: some View {
        VStack {
            // ...
        }
        .onAppear {
            self.focused = true
        }
    }
}

This won't work unfortunately. It appears that @FocusState only works when the view has been rendered after some time. A not-so-good workaround is to add a delay:

struct ContentView: View {
    @FocusState private var focused: Bool

    var body: some View {
        VStack {
            // ...
        }
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                self.focused = true
            }
        }
    }
}

I don't recommend this approach though. The delay is arbitrary, and slower devices might need a longer delay. It isn't a great user experience anyway.

For this task, it's best falling back to UIKit's becomeFirstResponder(). It's easy to wrap a UITextField in a UIViewRepresentable like this:

struct MyTextField: UIViewRepresentable {
    typealias UIViewType = UITextField

    @Binding var becomeFirstResponder: Bool

    func makeUIView(context: Context) -> UITextField {
        return UITextField()
    }
    
    func updateUIView(_ textField: UITextField, context: Context) {
        if self.becomeFirstResponder {
            DispatchQueue.main.async {
                textField.becomeFirstResponder()
                self.becomeFirstResponder = false
            }
        }
    }
}

Notice the async call. It is necessary because it's modifying the state and it isn't allowed in updateUIView. If you forget, Xcode will warn you Modifying state during view update, this will cause undefined behavior.

To make it first responder when the view appears, just call it in .onAppear. And it works without a delay:

struct ContentView: View {
    @State private var becomeFirstResponder = false

    var body: some View {
        MyTextField(becomeFirstResponder: self.$becomeFirstResponder)
            .onAppear {
                self.becomeFirstResponder = true
            }
    }
}

TIP: If you use SwiftUI Instrospect, you can directly inspect the backing UITextField without your own wrapper:

struct ContentView: View {
    @State private var name = "Mango Umbrella"
    @State private var becomeFirstResponder = true

    var body: some View {
        TextField("Name", text: self.$name)
            .introspectTextField { textField in
                if self.becomeFirstResponder {
                    textField.becomeFirstResponder()
                    self.becomeFirstResponder = false
                }
            }
        }
    }
}