SwiftUI focus looks simple:
@FocusState var isFocused: Bool
Until it isn’t.
That’s when you hit issues like:
- focus randomly dropping
- keyboard dismissing unexpectedly
- focus jumping between fields
- broken form navigation
- ScrollView + keyboard fighting each other
- accessibility focus behaving differently
- focus not restoring after navigation
This post explains how SwiftUI focus actually works internally, how it interacts with the keyboard, navigation, scroll views, and accessibility — and how to use it correctly in production apps.
🧠 The Mental Model: Focus Is State + Routing
SwiftUI focus is not just a boolean.
Internally, it’s:
- a focus graph
- driven by state
- resolved through the view hierarchy
- coordinated with keyboard + accessibility
Thin…
SwiftUI focus looks simple:
@FocusState var isFocused: Bool
Until it isn’t.
That’s when you hit issues like:
- focus randomly dropping
- keyboard dismissing unexpectedly
- focus jumping between fields
- broken form navigation
- ScrollView + keyboard fighting each other
- accessibility focus behaving differently
- focus not restoring after navigation
This post explains how SwiftUI focus actually works internally, how it interacts with the keyboard, navigation, scroll views, and accessibility — and how to use it correctly in production apps.
🧠 The Mental Model: Focus Is State + Routing
SwiftUI focus is not just a boolean.
Internally, it’s:
- a focus graph
- driven by state
- resolved through the view hierarchy
- coordinated with keyboard + accessibility
Think of focus as navigation for input.
🧩 1. Focus Is Value-Based (Not View-Based)
This is the most important rule.
Bad mental model:
“This TextField owns focus”
Correct mental model:
“Focus is state that points to a field”
That’s why this works:
enum Field {
case email
case password
}
@FocusState private var focusedField: Field?
SwiftUI resolves focus by:
- matching the focused value
- walking the view tree
- finding the first compatible focus target
🧱 2. How SwiftUI Builds the Focus Tree
At render time, SwiftUI:
- scans for focusable views
- builds a focus tree
- assigns each a focus identity
- resolves the active focus state
Focusable views include:
- TextField
- SecureField
- TextEditor
- custom focusable controls
- accessibility elements
If a view disappears → its focus node is removed.
🔄 3. Why Focus Drops “Randomly”
Focus is lost when:
- the focused view leaves the hierarchy
- the view’s identity changes
- the focus state value changes
- navigation removes the view
- the parent is recreated
- the ScrollView re-lays out content
- keyboard dismissal is triggered
This is not random — it’s identity + lifecycle.
🧠 4. Focus vs View Identity (Critical Connection)
This breaks focus:
TextField("Email", text: $email)
.id(UUID()) // ❌
Why?
- identity changes
- focus node destroyed
- focus state points to nothing
- keyboard dismisses
Rule: 📌 Focus requires stable view identity.
⌨️ 5. Keyboard Is a Side Effect, Not the Source
SwiftUI focus controls the keyboard — not the other way around.
Flow:
Focus change
↓
Responder change
↓
Keyboard presentation
That’s why manually dismissing the keyboard without updating focus causes bugs.
Correct dismissal:
focusedField = nil
Not:
- forcing resignFirstResponder
- UIKit hacks
- gesture-based dismissals without focus updates
📜 6. ScrollView + Keyboard Internals
When the keyboard appears, SwiftUI:
- adjusts safe area insets
- tries to keep the focused field visible
- may scroll automatically
- may fail if layout is complex
Common problems:
- nested ScrollViews
- GeometryReader usage
- custom layouts
- dynamic height changes
Best practice:
- keep forms simple
- avoid GeometryReader in forms
- use
.scrollDismissesKeyboard(.interactively)when appropriate
🧭 7. Programmatic Focus (The Right Way)
Correct pattern:
focusedField = .email
For delayed focus (navigation / animation):
Task {
try await Task.sleep(for: .milliseconds(100))
focusedField = .email
}
Why delay?
- focus tree must exist
- view must be rendered
- navigation must complete
🧪 8. Focus & Navigation
When navigating:
- focus does NOT automatically transfer
- new views start unfocused
- previous focus is destroyed
If you want focus restoration:
- store focus state externally
- restore it on appear
Example:
.onAppear {
focusedField = savedFocus
}
♿ 9. Focus vs Accessibility Focus
These are different systems.
- Input focus → keyboard
- Accessibility focus → VoiceOver
SwiftUI coordinates them, but:
- they can diverge
- accessibility can move focus independently
- accessibility focus does not always trigger keyboard
Never assume they are the same.
🧠 10. Custom Focusable Views
You can make custom controls focusable:
.focusable()
.focused($focusedField, equals: .custom)
Use this for:
- custom inputs
- game-like UIs
- tvOS / visionOS
- advanced keyboard navigation
⚠️ 11. The Biggest Focus Anti-Patterns
Avoid:
- inline UUID ids
- recreating form rows
- mixing UIKit responders
- dismissing keyboard without focus update
- putting focus logic in views instead of ViewModels
- heavy layout changes during focus transitions
These cause 90% of focus bugs.
🧠 Focus System Cheat Sheet
✔ Focus is state ✔ Identity must be stable ✔ Keyboard follows focus ✔ Navigation destroys focus ✔ Delay focus until views exist ✔ Accessibility focus is separate ✔ Forms require layout stability
🚀 Final Thoughts
SwiftUI focus is not fragile — it’s precise.
Once you understand:
- focus as state
- focus tree resolution
- identity + lifecycle
- keyboard as a side effect
Forms, editors, and input-heavy screens become predictable and rock solid.