SwiftUI TextField - Focus Management Using @FocusState
In the previous article, I briefly introduced basic usage of TextField in SwiftUI. Here, I would like to further illustrate a common usage scenario surrounding TextField @FocusState
. @FocusState
is a property wrapper introduced in iOS 15 that helps developers easily track the focus status of text fields in apps.
*Code is tested in Xcode Version 15.4 (15F31d)
Basic Usage
The following is the most basic example using @FocusState
:
struct ContentView: View {
@State private var username = ""
@State private var password = ""
@FocusState private var focusedState: Bool
var body: some View {
VStack(alignment: .leading) {
TextField("Username:", text: $username)
.textFieldStyle(.roundedBorder)
.focused($focusedState)
Text("Username field is focused").opacity(focusedState ? 1 : 0)
}
.padding()
}
}
In this example, the text “Username field is focused” will be displayed if the TextField
is focused.
FocusState for Multiple TextFields
Most of the time, we want to track the focus state of more than one TextField
. While it is possible to use multiple @FocusState
properties in this situation, it can become cumbersome if there are three, four, or even more fields to track. For such cases, we can use a single focus state in combination with enums to control the binding of the focus state.
struct ContentView: View {
enum Field {
case name
case pass
}
@State private var username = ""
@State private var password = ""
@FocusState private var focusedField: Field?
var body: some View {
VStack(alignment: .leading) {
TextField("Username:", text: $username)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .name)
TextField("Password:", text: $password)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .pass)
}
.padding()
}
}
We used .focused(_ binding: equals:)
to help us control focus by matching a value. Quoted from Apple’s documentation about the binding:
- When focus moves to the modified view, the binding sets the bound value to the corresponding match value. If a caller sets the state value programmatically to the matching value, then focus moves to the modified view. When focus leaves the modified view, the binding sets the bound value to
nil
. If a caller sets the value tonil
, SwiftUI automatically dismisses focus.
By using this, we can conditionally modify a field based on the focused state. For example, if we want to show a border while TextField is focused:
struct ContentView: View {
enum Field {
case name
case pass
}
@State private var username = ""
@State private var password = ""
@FocusState private var focusedField: Field?
var body: some View {
VStack(alignment: .leading) {
TextField("Username:", text: $username)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .name)
.border(focusedField == .name ? .black : .clear)
TextField("Password:", text: $password)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .pass)
.border(focusedField == .pass ? .black : .clear)
}
.padding()
}
}
onSubmit()
With onSubmit
, we can add actions to a TextField
that are triggered when the user hits the return key on a software or hardware keyboard. In the following snippet, the focus automatically moves to the password field when the user presses the return key after entering their username.
TextField("Username:", text: $username)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .name)
.onSubmit {
focusedField = .pass
}
The onSubmit
modifier can be used on an individual text field or on an entire view hierarchy. This allows us to trigger actions that we would like to apply to each field easily. Here is a simple example demonstrating how to use it.
struct ContentView: View {
enum Field {
case name
case pass
}
@State private var username = ""
@State private var password = ""
@FocusState private var focusedField: Field?
var body: some View {
VStack(alignment: .leading) {
TextField("Username:", text: $username)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .name)
SecureField("Password:", text: $password)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .pass)
}
.onSubmit {
guard validate() else {
print("not validated")
return
}
print("validated")
}
.padding()
}
func validate() -> Bool {
!username.isEmpty && !password.isEmpty
}
}
In this example, the onSubmit
modifier is used to trigger validation when the user presses return key on either the username or password field. If the validation fails, “not validated” is printed to the console. If validation succeeds, “validated” is printed. This ensures that both fields are filled before proceeding.
Conclusion
That’s all for this article. In real-world applications, it is quite common to manage focus state and trigger actions on form submission. Experiment with these features in your own projects to see how they can improve the interactions and usability of your user interface.