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
}
}
}
}
}