DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Focus System & Keyboard Internals

SwiftUI focus looks simple:

@FocusState var isFocused: Bool
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

SwiftUI resolves focus by:

  1. matching the focused value
  2. walking the view tree
  3. 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()) // ❌
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That’s why manually dismissing the keyboard without updating focus causes bugs.

Correct dismissal:

focusedField = nil
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For delayed focus (navigation / animation):

Task {
    try await Task.sleep(for: .milliseconds(100))
    focusedField = .email
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

♿ 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)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)