Focusing views in SwiftUI

WWDC’21 introduced APIs for managing focus in SwiftUI. When dealing with multiple focusable views, we can create an enum for representing these and use the new @FocusState property wrapper along with focused() view modifier, where each of the focusable view binds its focus to an enum case. This can look like this:

struct ContentView: View {
@StateObject var viewModel = ViewModel()
enum FormEntry: Hashable {
case lastName, firstName, email
}
@FocusState var focused: FormEntry?
var body: some View {
VStack {
Text("Application")
TextField("Last Name", text: $viewModel.lastName, prompt: nil)
.focused($focused, equals: .lastName)
TextField("First Name", text: $viewModel.firstName, prompt: nil)
.focused($focused, equals: .firstName)
TextField("Email", text: $viewModel.email, prompt: nil)
.focused($focused, equals: .email)
Button("Reset") {
viewModel.reset()
focused = .lastName
}
}
.frame(idealWidth: 600)
.padding()
}
}

In this example we have FormEntry enum which represents focusable elements in the view. Each of the view has a focused() view modifier, which binds the current focus state to the text field if it matches with the enum case. Meaning, setting focused property to FormEntry.email will move the focus to the email text field. It works the other way around as well. If we tap on the email text field, SwiftUI will set the focused property value to FormEntry.email.

Sometimes we just want to control the focus only once. Let’s take an example use case where we enter an order id and after tapping a search button we would like to remove the focus from the order id text field. In this case, we can just use a boolean property and bind the focus state boolean property to the text field’s focused() view modifier. Setting the property value to false in the button action will tell SwiftUI to resign focus from the text field.

struct ContentView: View {
@StateObject var viewModel = ViewModel()
@FocusState var isOrderIDFocused: Bool
var body: some View {
VStack {
Text("Orders")
TextField("Order ID", text: $viewModel.orderId, prompt: nil)
.focused($isOrderIDFocused)
Button("Search") {
viewModel.search()
isOrderIDFocused = false
}
}
.padding()
}
}

If this was helpful, please let me know on Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.