SwiftUI Focus Patterns Reference
Table of Contents
- @FocusState
- Making Views Focusable
- Focused Values for Commands and Menus
- Default Focus
- Focus Scope and Sections
- Focus Effects
- Search Focus
- Common Pitfalls
@FocusState
Always mark @FocusState as private. Use Bool for a single field, an optional Hashable enum for multiple fields.
Single field
@FocusState private var isFocused: Bool
TextField("Email", text: $email)
.focused($isFocused)Multiple fields
enum Field: Hashable { case name, email, password }
@FocusState private var focusedField: Field?
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
TextField("Email", text: $email)
.focused($focusedField, equals: .email)Set focusedField = .email to move focus programmatically; set nil to dismiss the keyboard.
focused(_:) vs focused(_:equals:) with nested views
.focused($bool) reports true when the modified view *or any focusable descendant* has focus. .focused($enum, equals:) reports its value only when that specific view receives focus.
enum Focus: Hashable { case container, field }
@FocusState private var focus: Focus?
VStack {
TextField("Name", text: $name)
.focused($focus, equals: .field)
}
.focusable()
.focused($focus, equals: .container)With focused(_:equals:) and a single @FocusState, SwiftUI distinguishes the container *receiving* focus from the container merely *containing* focus.
isFocused environment value
Read-only environment value that returns true when the nearest focusable ancestor has focus. Useful for styling non-focusable child views.
struct HighlightWrapper: View {
@Environment(\.isFocused) private var isFocused
var body: some View {
content
.background(isFocused ? Color.accentColor.opacity(0.1) : .clear)
}
}Making Views Focusable
.focusable(_:)
Makes a non-text-input view participate in the focus system. Focused views can respond to keyboard events via onKeyPress and menu commands like Edit > Delete via onDeleteCommand.
struct SelectableCard: View {
@FocusState private var isFocused: Bool
var body: some View {
CardContent()
.focusable()
.focused($isFocused)
.border(isFocused ? Color.accentColor : .clear)
.onDeleteCommand { deleteCard() }
}
}.focusable(_:interactions:) (iOS 17+)
Controls which focus-driven interactions the view supports via FocusInteractions:
.activate-- Button-like: only focusable when system-wide keyboard navigation is on (macOS/iOS).edit-- Captures keyboard/Digital Crown input.automatic-- Platform default (both activate and edit)
MyTapGestureView(...)
.focusable(interactions: .activate)Use .activate for custom button-like views that should match system keyboard-navigation behavior.
Focused Values for Commands and Menus
Focused values let parent views (App, Scene, Commands) read state from whichever view currently has focus. Use for enabling/disabling menu commands based on the focused document or selection.
Declare with @Entry
extension FocusedValues {
@Entry var selectedDocument: Binding<Document>?
}Focused values are typically optional (default is nil when no view publishes them), but you can also use non-optional entries when you have a sensible default value.
Publish from views
// View-scoped: available when this view (or descendant) has focus
.focusedValue(\.selectedDocument, $document)
// Scene-scoped: available when this scene has focus
.focusedSceneValue(\.selectedDocument, $document)Consume in commands
@FocusedValue reads the value; @FocusedBinding unwraps a Binding automatically.
@main
struct MyApp: App {
@FocusedBinding(\.selectedDocument) var document
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandGroup(after: .pasteboard) {
Button("Duplicate") { document?.duplicate() }
.disabled(document == nil)
}
}
}
}@FocusedObject (iOS 16+)
For ObservableObject types. The view invalidates when the focused object changes.
// Publish
.focusedObject(myObservableModel)
// Consume
@FocusedObject var model: MyModel?Scene-scoped variant: .focusedSceneObject(_:).
Default Focus
.defaultFocus(_:_:priority:) (iOS 17+, macOS 13+, tvOS 16+)
Prefer .defaultFocus over setting @FocusState in onAppear for initial focus placement.
@FocusState private var focusedField: Field?
VStack {
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
}
.defaultFocus($focusedField, .email)Priority: .automatic (default) applies on window appearance and programmatic focus changes. .userInitiated also applies during user-driven focus navigation.
prefersDefaultFocus(_:in:) (macOS/tvOS/watchOS)
Used with .focusScope(_:) to mark a preferred default target within a scoped region.
resetFocus environment action (macOS/tvOS/watchOS)
Re-evaluates default focus within a namespace.
@Namespace var scopeID
@Environment(\.resetFocus) private var resetFocus
Button("Reset") { resetFocus(in: scopeID) }Focus Scope and Sections
.focusScope(_:) (macOS/tvOS/watchOS)
Limits default focus preferences to a namespace. Use with prefersDefaultFocus and resetFocus.
.focusSection() (macOS 13+, tvOS 15+)
Guides directional and sequential focus movement through a group of focusable descendants. Useful when focusable views are spatially separated and directional navigation would otherwise skip them.
HStack {
VStack { Button("1") {}; Button("2") {}; Spacer() }
Spacer()
VStack { Spacer(); Button("A") {}; Button("B") {} }
.focusSection()
}Without .focusSection(), swiping right from buttons 1/2 finds nothing. With it, the VStack receives directional focus and delivers it to its first focusable child.
Focus Effects
.focusEffectDisabled(_:)
Suppresses the system focus ring (macOS) or hover effect. Use when providing custom focus visuals.
MyCustomCard()
.focusable()
.focusEffectDisabled()
.overlay { customFocusRing }isFocusEffectEnabled environment value reads the current state.
Search Focus
.searchFocused(_:) / .searchFocused(_:equals:)
Bind focus state to the search field associated with the nearest .searchable modifier. Works like .focused but targets the search bar.
@FocusState private var isSearchFocused: Bool
NavigationStack {
ContentView()
.searchable(text: $query)
.searchFocused($isSearchFocused)
}
// Programmatically focus the search bar
Button("Search") { isSearchFocused = true }Common Pitfalls
Redundant @FocusState writes revoke focus
.focusable() + .focused() handles focus-on-click natively. Adding a tap gesture that *also* writes to @FocusState triggers a redundant state write, causing a second body evaluation that revokes focus. The result: focus briefly appears then disappears, and key commands like onDeleteCommand stop working.
// WRONG -- tap gesture redundantly sets focus, causing double evaluation
CardView()
.focusable()
.focused($isFocused)
.onTapGesture { isFocused = true } // Remove this line
// CORRECT -- let .focusable() + .focused() handle it
CardView()
.focusable()
.focused($isFocused)Ambiguous focus bindings
Binding the same enum case to multiple views is ambiguous. SwiftUI picks the first candidate and emits a runtime warning.
// WRONG -- .name bound to two views
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
TextField("Full Name", text: $fullName)
.focused($focusedField, equals: .name) // ambiguousAlways use distinct enum cases for each focusable view.
.onAppear focus timing
Setting @FocusState in .onAppear may fail if the view tree hasn't settled. Prefer .defaultFocus (iOS 17+) for reliable initial focus. If you must use .onAppear, wrap in DispatchQueue.main.async as a last resort.
Missing .focusable() for non-text views
TextField and SecureField are implicitly focusable. Custom views (stacks, shapes, images) are not. Forgetting .focusable() means .focused() bindings have no effect and key event handlers never fire.