swiftui · tutorial · forms

Using @FocusState for Better Form UX

Published March 19, 2026 · 4 min read · intermediate

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+ (@FocusState is 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: set focusedField = nil when 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: