Skip to content

How to use @FocusState in SwiftUI | SwiftUI Bootcamp #60

Managing keyboard focus used to mean juggling UIKit's becomeFirstResponder calls — @FocusState gives you a clean, declarative way to control which text field is active, so you can build smart onboarding flows and multi-field forms that guide users through inputs automatically.

What You'll Learn

  • How to declare @FocusState with an enum to track multiple fields
  • How to bind focus to a specific field using the focused(_:equals:) modifier
  • How to programmatically move focus from one field to another in response to user actions

Mental Model

Think of @FocusState as a spotlight in a theater. At any moment, only one performer (text field) can stand in the spotlight (have keyboard focus). The spotlight operator (@FocusState variable) decides who is lit up. When you tap a field, the spotlight moves there automatically. But you — the developer — can also grab the spotlight control and move it programmatically: "the username field is empty, so move the spotlight there right now."

The optional enum approach (OnboardingField?) means the spotlight can also be fully off — when fieldInFocus is nil, no field has focus and the keyboard is dismissed.

Detailed Explanation

@FocusState is a property wrapper introduced in iOS 15 that lets you read and write which view currently holds keyboard focus. Unlike plain @State, it creates a two-way binding between SwiftUI's focus system and your variable — SwiftUI updates it when the user taps a field, and your code can update it to move focus elsewhere.

Under the hood, SwiftUI monitors the focus engine and reconciles your @FocusState value with the actual first responder. When you write fieldInFocus = .password, SwiftUI schedules a focus change on the next render pass. This is why you should always set focus values on the main thread and within user-driven actions or lifecycle events — setting them mid-layout can be unpredictable.

Use @FocusState when you need: automatic field progression after the user finishes typing, showing or hiding UI based on whether a field is active (like a toolbar), or dismissing the keyboard programmatically by setting the value to nil. Avoid it when all you need is a simple "auto-focus on appear" — in that case, .onAppear with a short DispatchQueue.main.asyncAfter is simpler.

@FocusState pairs naturally with @State (for the text content) and onSubmit (for reacting to the keyboard's return key). The enum pattern shown here scales well — just add a new case for each field you want to track.

Code Structure

FocusStateBootcamp.swift contains a single screen with two text fields and a sign-up button. The OnboardingField enum gives each field a typed identity. Commented-out lines show the earlier Boolean approach (@FocusState private var usernameInFocus: Bool) so you can see why the enum pattern is preferable for managing multiple fields.

Complete Code

FocusStateBootcamp.swift

swift
import SwiftUI

struct FocusStateBootcamp: View {
    
    // Enum gives each field a unique, type-safe identity for focus tracking
    enum OnboardingField: Hashable {
        case username
        case password
    }
    
//    @FocusState private var usernameInFocus: Bool
    @State private var username: String = ""
//    @FocusState private var passwordInFocus: Bool
    @State private var password: String = ""
    // Single @FocusState with an enum replaces one Bool per field
    @FocusState private var fieldInFocus: OnboardingField?

    var body: some View {
        VStack(spacing: 30) {
            TextField("Add your name here...", text: $username)
                .focused($fieldInFocus, equals: .username) // highlights when fieldInFocus == .username
//                .focused($usernameInFocus)
                .padding(.leading)
                .frame(height: 55)
                .frame(maxWidth: .infinity)
                .background(Color.gray.brightness(0.3))
                .cornerRadius(10)
            
            SecureField("Add your password here...", text: $password)
                .focused($fieldInFocus, equals: .password) // highlights when fieldInFocus == .password
//                .focused($passwordInFocus)
                .padding(.leading)
                .frame(height: 55)
                .frame(maxWidth: .infinity)
                .background(Color.gray.brightness(0.3))
                .cornerRadius(10)
            
            Button("SIGN UP 🚀") {
                let usernameIsValid = !username.isEmpty
                let passwordIsValid = !password.isEmpty
                if usernameIsValid && passwordIsValid {
                    print("SIGN UP") // Both fields valid — proceed with sign-up
                } else if usernameIsValid {
                    fieldInFocus = .password // Username done, jump focus to password
//                    usernameInFocus = false
//                    passwordInFocus = true
                } else {
                    fieldInFocus = .username // Username is empty — bring focus back
//                    usernameInFocus = true
//                    passwordInFocus = false
                }
                
            }
            
//            Button("TOGGLE FOCUS STATE") {
//                usernameInFocus.toggle()
//            }
        }
        .padding(40)
//        .onAppear {
//            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
//                self.usernameInFocus = true
//            }
//        }
    }
}

struct FocusStateBootcamp_Previews: PreviewProvider {
    static var previews: some View {
        FocusStateBootcamp()
    }
}

Code Walkthrough

  1. OnboardingField enum — Defining an enum that conforms to Hashable is the recommended pattern for tracking multiple fields. Each case represents one focusable field. The enum approach is better than separate Booleans because it makes only one field active at a time by design.

  2. @FocusState private var fieldInFocus: OnboardingField? — The optional type is key. When fieldInFocus is nil, no field has focus and the keyboard is dismissed. SwiftUI automatically sets this to nil when the user taps outside a field.

  3. .focused($fieldInFocus, equals: .username) — This modifier registers the text field with the focus system. It creates a binding: the field gains focus when fieldInFocus == .username, and it updates fieldInFocus to .username when the user taps on it.

  4. Validation logic in the button — Rather than always submitting, the button checks each field and moves focus to the first empty one. This is a pattern common in onboarding flows — it keeps the keyboard on screen and guides the user to what needs filling in.

  5. Commented DispatchQueue.main.asyncAfter — This is the older workaround for auto-focusing a field on appear. With @FocusState you can still do this, but it requires the small delay because the view must be fully rendered before focus can be set.

Common Mistakes

Mistake: Using @FocusState without @State for the text value
@FocusState only tracks focus — it never holds the text. You always need a separate @State var for the actual string content. Confusing the two leads to text not persisting when you switch fields.

Mistake: Setting fieldInFocus synchronously in onAppear
SwiftUI lays out views before focus can be requested. Setting @FocusState directly in onAppear often has no effect. Wrap it in DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) to let the view settle first.

Mistake: Using a plain Bool for more than one field
When you have two separate @FocusState Booleans, nothing prevents both from being true simultaneously. The enum pattern guarantees mutual exclusivity — only one case can be active at a time.

Key Takeaways

  • @FocusState with an enum is the modern, scalable way to manage focus across multiple text fields
  • Setting the variable to nil dismisses the keyboard — no need for .resignFirstResponder workarounds
  • Focus changes should happen in response to user actions or lifecycle events, not mid-layout

Last updated: June 27, 2026

Released under the MIT License.