Using @FocusState for Better Form UX
Goal
By the end of this tutorial, you will build a SwiftUI form that manages focus predictably, moves between fields with intent, and guides users to invalid input.
State it explicitly:
“Use @FocusState to control keyboard flow and validation feedback instead of relying on default first responder behavior.”
Requirements
- Basic SwiftUI knowledge
- Xcode 15+
- iOS 15+ (
@FocusStateis available from iOS 15)
The use case
Forms are where UX quality is most visible. When focus handling is sloppy, users feel friction immediately: keyboard jumps to wrong fields, “Next” does nothing useful, and submit errors provide no clear path.
@FocusState gives you an explicit focus model.
Instead of hoping the system picks the next field, you tell SwiftUI which input is focused and when it should change.
Step 1 - The simplest working version
Create one enum for fields and bind each text field to that focus state.
import SwiftUI
struct AccountFormView: View {
enum Field: Hashable {
case name
case email
case password
}
@State private var name = ""
@State private var email = ""
@State private var password = ""
@FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
.submitLabel(.next)
TextField("Email", text: $email)
.textInputAutocapitalization(.never)
.keyboardType(.emailAddress)
.focused($focusedField, equals: .email)
.submitLabel(.next)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
.submitLabel(.done)
}
.onSubmit {
advanceFocus()
}
.onAppear {
focusedField = .name
}
}
private func advanceFocus() {
switch focusedField {
case .name:
focusedField = .email
case .email:
focusedField = .password
default:
focusedField = nil
}
}
}
What this already does correctly: focus is now explicit and navigates in a predictable order.
Quick verification checkpoint: fill fields using return key only and confirm it moves Name -> Email -> Password -> dismiss.
Step 2 - Make it reusable
Extract the focus flow and validation trigger so you can reuse the same logic across forms.
struct FormFocusController<Field: Hashable> {
let next: (Field?) -> Field?
func advance(from current: Field?) -> Field? {
next(current)
}
}
Use it in the screen:
private let focusController = FormFocusController<AccountFormView.Field> { current in
switch current {
case .name: return .email
case .email: return .password
default: return nil
}
}
private func advanceFocus() {
focusedField = focusController.advance(from: focusedField)
}
What changed: focus order logic is now a separate unit, which is easier to test and reuse when forms evolve.
Quick verification checkpoint: change order in one place and confirm behavior updates across the screen.
Step 3 - SwiftUI-specific refinement
Now connect focus to validation so submit behavior guides the user directly to the first failing field.
private func submit() {
if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
focusedField = .name
return
}
if !email.contains("@") {
focusedField = .email
return
}
if password.count < 8 {
focusedField = .password
return
}
focusedField = nil
// perform save
}
Add button:
Button("Create account") {
submit()
}
Refinement points:
- validation and focus are aligned
- error recovery is immediate
- keyboard flow stays intentional
Quick verification checkpoint: leave each field invalid one by one and confirm submit always jumps to the right input.
Result
You now have a form flow that is:
- explicit
- user-guiding
- easier to maintain than implicit focus behavior
Common SwiftUI pitfalls
-
Mistake: Using boolean flags per field instead of one focused enum
Why it happens: quick setup for small forms
How to fix it: model focus as one optional field enum -
Mistake: Validation errors without focus guidance
Why it happens: validation logic separated from UX flow
How to fix it: route focus to first invalid field during submit -
Mistake: Forgetting to clear focus after success
Why it happens: no explicit final state
How to fix it: setfocusedField = nilwhen submit completes
When NOT to use this
For single-field inputs, full focus orchestration is usually unnecessary. Keep it simple unless you have multi-step keyboard flows.
Takeaway
If you remember one thing: @FocusState works best when focus order and validation order are the same model.
Next steps
If you want to extend this into larger form architecture, these are the best continuations: