SwiftUI @FocusState in iOS 26: Form Focus, Keyboard Navigation, and Focus Chains
SwiftUI @FocusState binds view focus to a value. Learn iOS 26 patterns for keyboard toolbars, focus chains, sheet dismissals, OTP inputs, and hardware keyboard support with working code.
SwiftUI @FocusState is a property wrapper that binds a view's focus state to a value. Use a Bool for a single field or an enum for a set of mutually exclusive fields, then attach .focused($state, equals: .field) to any focusable view to read or write its focus programmatically. In iOS 26, @FocusState is the primary tool for building keyboard-driven forms, hardware-keyboard navigation on iPad and Mac, and multi-step onboarding flows where focus needs to advance predictably as the user types.
@FocusState comes in two shapes: Bool for a single field, and an Optional<Enum> for coordinating focus across many fields. Pick the enum form whenever you have more than one TextField.
Attach focus with .focused($state, equals: .someCase). The modifier is bidirectional, so writing to $state moves focus and tapping the field updates the state.
The correct place to build a "Next / Done" bar is a ToolbarItemGroup(placement: .keyboard). It survives keyboard splits on iPad and works with hardware keyboards.
.onSubmit fires on Return; combined with .submitLabel(.next) it gives you native "Next" and "Go" keyboard buttons without a custom toolbar.
To dismiss the keyboard, set the focus state to nil (enum) or false (Bool). Never call UIApplication.shared.sendAction(#selector(...)); it breaks on iPad multitasking.
iOS 26 tightens hardware keyboard focus rings on iPad and adds default focus behavior for .focusable() in sheets, so audit any custom focusEffect when you upgrade.
What is @FocusState and when should you use it?
@FocusState is SwiftUI's property wrapper for reading and writing the current focus of any focusable view: TextField, SecureField, TextEditor, or any view marked .focusable(). Unlike @State, which stores arbitrary values, @FocusState is scoped to the platform's focus system. When the user taps a field, hits Tab on a hardware keyboard, or you write to the state from code, the wrapper coordinates with UIKit, AppKit, or TVUIKit under the hood.
So, when should you actually reach for it? Any UI where focus is a first-class concept: sign-in forms, checkout flows, OTP inputs, chat composers, in-line editing on iPad, or full-keyboard controls on Mac Catalyst and visionOS. If you find yourself reaching for becomeFirstResponder() or trying to force focus through a UIViewRepresentable, you're almost certainly rebuilding what @FocusState already gives you. It also composes cleanly with FocusedValues and the @Entry macro when you need focused commands to reach up into CommandMenu at the top of the scene.
The wrapper has been available since iOS 15, but the ergonomics matured significantly in iOS 26. Focus persists more reliably across sheet dismissals, and the hardware-keyboard focus ring on iPad is now driven by SwiftUI's own focusEffect API instead of the older UIKit ring. So the guidance in this article assumes iOS 17+; where iOS 26-only behavior matters, I call it out explicitly.
Bool vs. enum: choosing the right FocusState shape
@FocusState supports two shapes. The Bool form tracks focus for exactly one field. The Optional<Enum: Hashable> form tracks which of several fields is focused, with nil meaning no focus. In my experience the enum shape is almost always the right default, because forms grow. I've been burned by starting with a Bool and then having to refactor every .focused() call site and every branch that dismisses the keyboard when a second field showed up two sprints later.
import SwiftUI
struct SignInForm: View {
enum Field: Hashable {
case email
case password
}
@State private var email = ""
@State private var password = ""
@FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Email", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.submitLabel(.next)
.focused($focusedField, equals: .email)
SecureField("Password", text: $password)
.textContentType(.password)
.submitLabel(.go)
.focused($focusedField, equals: .password)
}
.onSubmit {
switch focusedField {
case .email: focusedField = .password
case .password: signIn()
case .none: break
}
}
}
private func signIn() {
focusedField = nil // dismisses the keyboard
// ...perform sign in
}
}
Two things worth noticing. First, .submitLabel paints the Return key as "Next" or "Go" (free UX that most apps skip). Second, the .onSubmit closure reads focusedField and either advances it or fires the primary action. That switch statement is the entire "Next" logic; you don't need a separate coordinator or delegate.
How do I move focus between text fields in SwiftUI?
You move focus by writing to the @FocusState value. If you use an enum, assign the next case; if you use a Bool, toggle it. The system handles the animation, the keyboard change, and the accessibility announcement. You never touch the responder chain directly.
Wrapping the assignment in a withAnimation spring with response 0.4 and damping 0.85 is my house style. It matches the system's own field-to-field motion without feeling sluggish. If you need to move focus in response to typing (say, an OTP where each digit advances automatically), watch the field's onChange and hop when the character count matches:
struct OTPField: View {
enum Slot: Int, Hashable, CaseIterable { case one, two, three, four, five, six }
@State private var digits: [Slot: String] = [:]
@FocusState private var focused: Slot?
var body: some View {
HStack(spacing: 8) {
ForEach(Slot.allCases, id: \.self) { slot in
TextField("", text: binding(for: slot))
.keyboardType(.numberPad)
.textContentType(.oneTimeCode)
.frame(width: 44, height: 56)
.multilineTextAlignment(.center)
.focused($focused, equals: slot)
.onChange(of: digits[slot] ?? "") { _, new in
guard new.count == 1 else { return }
let next = slot.rawValue + 1
focused = Slot(rawValue: next) // nil at the end auto-dismisses
}
}
}
.onAppear { focused = .one }
}
private func binding(for slot: Slot) -> Binding {
Binding(
get: { digits[slot] ?? "" },
set: { digits[slot] = String($0.prefix(1)) }
)
}
}
Note the textContentType(.oneTimeCode). iOS will surface the code from the SMS QuickType bar and, in iOS 26, from a paired Apple Watch. Combine that with the auto-advance above, and you get a pasteable OTP without any custom paste handling.
How do I dismiss the keyboard in SwiftUI?
To dismiss the keyboard, set your @FocusState to nil (enum) or false (Bool). This is the SwiftUI-native way, and it works on iPhone, iPad, and Mac Catalyst without touching UIKit APIs.
Button("Done") { focusedField = nil }
For dismissing on background tap, use a .onTapGesture on the outermost container. Or the newer .scrollDismissesKeyboard(.interactively) modifier if the form lives inside a ScrollView. The interactive mode is the one designers usually ask for: it drags the keyboard down with the finger instead of snapping it away.
ScrollView {
// form content
}
.scrollDismissesKeyboard(.interactively)
Building a keyboard toolbar with Next, Previous, and Done
Long forms deserve a keyboard accessory bar. In SwiftUI you build one with ToolbarItemGroup(placement: .keyboard). The toolbar attaches to the keyboard, moves with it, and survives the split keyboard on iPad. Here's the pattern I ship in every form of three fields or more:
struct CheckoutForm: View {
enum Field: Hashable, CaseIterable {
case name, address, city, postalCode
}
@State private var name = ""
@State private var address = ""
@State private var city = ""
@State private var postalCode = ""
@FocusState private var focus: Field?
var body: some View {
NavigationStack {
Form {
TextField("Name", text: $name) .focused($focus, equals: .name)
TextField("Address", text: $address) .focused($focus, equals: .address)
TextField("City", text: $city) .focused($focus, equals: .city)
TextField("Postal code", text: $postalCode) .focused($focus, equals: .postalCode)
.keyboardType(.numberPad) // no Return key, so the keyboard bar is the only way out
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button {
move(-1)
} label: {
Image(systemName: "chevron.up")
}
.disabled(focus == .name)
Button {
move(+1)
} label: {
Image(systemName: "chevron.down")
}
.disabled(focus == .postalCode)
Spacer()
Button("Done") { focus = nil }.bold()
}
}
}
}
private func move(_ delta: Int) {
guard let current = focus,
let index = Field.allCases.firstIndex(of: current) else { return }
let target = index + delta
guard Field.allCases.indices.contains(target) else { return }
withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) {
focus = Field.allCases[target]
}
}
}
A few UX details are baked into that snippet. The chevrons use SF Symbols so they respect the user's Dynamic Type and Bold Text settings. The Done button is bold because it's the primary action of the bar. The postal code field is a .numberPad, which does not include a Return key, so a keyboard bar isn't just polish. It's the only way out for that field. Skipping the toolbar there is a real accessibility bug.
Setting focus programmatically after appear or navigation
Setting focus in .onAppear is the classic gotcha. In iOS 15 and 16 it often silently failed because the field had not yet been added to the responder chain. iOS 26 is far more reliable, but the safest pattern is still to defer to the next runloop turn using a tiny Task:
For sheets, do the same in the sheet's own .onAppear, not the presenting view's onChange(of: isPresented). For NavigationStack pushes, iOS 26 will call onAppear after the transition completes; before iOS 26 you sometimes needed a short delay. If you're targeting mixed versions, one Task hop covers both cases without a magic-number sleep.
To move focus in response to a data event (say, a Combine publisher firing when a validation error arrives), write to @FocusState from an .onReceive or .task. Because the wrapper is bound to the view, you cannot store it on a view model. But you can pass a FocusState<Field?>.Binding down to a subview using @FocusState.Binding, which is how you factor a large form into smaller components without losing focus coordination:
struct AddressSection: View {
@FocusState.Binding var focus: CheckoutForm.Field?
@Binding var address: String
@Binding var city: String
var body: some View {
Section("Ship to") {
TextField("Address", text: $address).focused($focus, equals: .address)
TextField("City", text: $city) .focused($focus, equals: .city)
}
}
}
What is the difference between focused and focusable in SwiftUI?
.focusable() declares that a view can receive focus. .focused($state, equals:) declares that a view's focus is tracked by a specific @FocusState. Text-input views (TextField, SecureField, TextEditor) are focusable by default, so you rarely call .focusable() on them. You reach for .focusable() when you want a plain Text, Image, or custom view to participate in Tab-key navigation on iPad or Mac. For example, a card grid that a hardware-keyboard user should be able to arrow through.
struct CardTile: View {
let title: String
@FocusState private var isFocused: Bool
var body: some View {
Text(title)
.padding()
.background(.regularMaterial, in: .rect(cornerRadius: 12))
.overlay {
RoundedRectangle(cornerRadius: 12)
.strokeBorder(isFocused ? Color.accentColor : .clear, lineWidth: 3)
}
.focusable()
.focused($isFocused)
.focusEffectDisabled() // draw our own ring
.animation(.spring(response: 0.35, dampingFraction: 0.8), value: isFocused)
}
}
Here .focusable() puts the tile in the Tab order, .focused($isFocused) reads whether it is currently focused, and .focusEffectDisabled() suppresses the platform focus ring so we can draw our own accent-colored stroke. The spring at response 0.35 keeps the focus feedback tight. Long enough to notice, short enough to feel snappy on a keyboard user's rapid Tab.
What's new for focus in iOS 26
iOS 26 ships three meaningful changes to focus. First, the hardware-keyboard focus ring on iPad is now rendered by SwiftUI using the focusEffect modifier, which means custom shapes (like the rounded pill on a chip control) get correct hit-testing without extra work. Second, @FocusState in a presented sheet now restores to the previously focused field when the sheet dismisses. A long-standing paper cut that used to require manual bookkeeping. Third, ToolbarItemGroup(placement: .keyboard) respects the new floating tab bar layout, so the keyboard bar no longer clips behind the tab bar on iPhone.
The focusSection() modifier, originally for tvOS, is now more useful on iPadOS 26 with hardware keyboards. Wrapping a group of .focusable() views in a focusSection() lets arrow-key navigation "jump" between sections instead of iterating one focusable at a time. Apple's FocusState reference documentation now includes a section on section-based focus that is worth a read if you build iPad-first UI.
One iOS 26 gotcha to watch: .focused() attached to a view inside a List row that uses id:-based diffing can drop focus during row reorders. The fix is to attach the modifier to a stable identity (the row's model id, not the position index). This is documented in the focused(_:equals:) modifier reference.
Focus, VoiceOver, and hardware keyboards
Focus and accessibility focus are related but distinct systems, and treating them as one causes real bugs. @FocusState tracks the keyboard and pointer focus. VoiceOver focus is tracked separately via AccessibilityFocusState. When you move keyboard focus programmatically, VoiceOver usually follows, but not always. Sheets and full-screen covers are the common exceptions.
@FocusState private var keyboardFocus: Field?
@AccessibilityFocusState private var a11yFocus: Field?
func focusEmail() {
keyboardFocus = .email
a11yFocus = .email // announce it to VoiceOver users too
}
Any custom .focusable() view should also carry an .accessibilityLabel and an .accessibilityHint. The focus ring tells sighted keyboard users something is focused, but VoiceOver needs the label to say what it is. For a deeper walkthrough of these APIs, our SwiftUI accessibility complete guide covers dynamic type, VoiceOver rotors, and inclusive design patterns end-to-end.
On the hardware-keyboard side, iPad Magic Keyboard users expect Tab to advance through fields, Shift-Tab to reverse, and Cmd-Return to submit. SwiftUI wires Tab and Shift-Tab automatically once fields are focusable, but Cmd-Return you must wire yourself with a hidden .keyboardShortcut(.return, modifiers: .command) on your primary button. This matters for iPad users writing long-form content in a TextEditor, like the input surface of the SwiftUI rich text editor built on AttributedString, where a keyboard shortcut to send or save is the difference between a laptop replacement and a toy.
Why is my @FocusState not working? Common bugs and fixes
Nine out of ten "@FocusState is not working" reports fall into one of these buckets. Honestly, check them in order. The top ones are the most common:
Setting focus in .onAppear before iOS 17 without a Task hop. Wrap the assignment in Task { @MainActor in focus = .field }.
Declaring @FocusState in a view model. Property wrappers that update views must live in a View. Pass a @FocusState.Binding down to children instead.
Attaching .focused() to a Group or container. The modifier applies to individual focusable views. Attach it directly to the TextField, not its wrapper.
Reusing the same enum case on two fields. Enum cases must be unique; two fields with .focused($focus, equals: .name) will never behave predictably.
Ignoring @Observable ownership. If a parent replaces the entire form model, the child views identity-diff and lose focus. Give the row a stable .id().
Blocking the main actor. A synchronous JSON decode on the main thread during onSubmit can starve the responder handoff. Move it into a Task.
Frequently Asked Questions
Can I set focus programmatically in SwiftUI?
Yes. Assign a value to your @FocusState property (the case of an enum, or true for a Bool) and SwiftUI will focus the matching view. For focus in onAppear, wrap the assignment in Task { @MainActor in ... } so it runs after the view is added to the responder chain.
Why does @FocusState reset when my sheet opens?
Each presented view has its own focus scope, so a @FocusState declared in the sheet starts as nil. Declare focus state inside the sheet's body view, not in the presenting view. On iOS 26 the previous outer focus is restored automatically when the sheet dismisses.
Does @FocusState work with TextEditor?
Yes. TextEditor is focusable by default. Attach .focused($focus, equals: .someCase) exactly like a TextField. On iOS 26, TextEditor also participates in Tab-key navigation on iPad hardware keyboards without any additional setup.
How do I add a Done button above the keyboard in SwiftUI?
Use ToolbarItemGroup(placement: .keyboard) inside a .toolbar modifier on the enclosing NavigationStack. Put a Spacer() before the Done button to right-align it, and set the button action to focus = nil to dismiss.
Should I still use UIViewRepresentable and becomeFirstResponder for focus?
No, not in new SwiftUI code. @FocusState handles every case UIKit's responder chain does, plus hardware keyboard navigation, VoiceOver coordination, and iPad multitasking. Reach for UIViewRepresentable only when the input control itself is UIKit-only, and even then bridge focus through a @Binding<Bool>.
A production-ready guide to bridging SwiftUI and UIKit in iOS 26: UIViewRepresentable, UIViewControllerRepresentable, coordinators, UIHostingController, sizing, and the pitfalls that trip up mixed codebases.
The SwiftUI @Entry macro replaces the classic EnvironmentKey boilerplate with a single line. Here's how to use it for EnvironmentValues, FocusedValues, Transaction, and ContainerValues in iOS 26 apps.
How to use SwiftUI PhotosPicker in iOS 26 for multi-select, video filtering, and file-based loading. Covers race conditions, iCloud progress, and VoiceOver.