Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/focus-patterns.md
1# SwiftUI Focus Patterns Reference23## Table of Contents45- [@FocusState](#focusstate)6- [Making Views Focusable](#making-views-focusable)7- [Focused Values for Commands and Menus](#focused-values-for-commands-and-menus)8- [Default Focus](#default-focus)9- [Focus Scope and Sections](#focus-scope-and-sections)10- [Focus Effects](#focus-effects)11- [Search Focus](#search-focus)12- [Common Pitfalls](#common-pitfalls)1314## @FocusState1516Always mark `@FocusState` as `private`. Use `Bool` for a single field, an optional `Hashable` enum for multiple fields.1718### Single field1920```swift21@FocusState private var isFocused: Bool2223TextField("Email", text: $email)24.focused($isFocused)25```2627### Multiple fields2829```swift30enum Field: Hashable { case name, email, password }31@FocusState private var focusedField: Field?3233TextField("Name", text: $name)34.focused($focusedField, equals: .name)35TextField("Email", text: $email)36.focused($focusedField, equals: .email)37```3839Set `focusedField = .email` to move focus programmatically; set `nil` to dismiss the keyboard.4041### `focused(_:)` vs `focused(_:equals:)` with nested views4243`.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.4445```swift46enum Focus: Hashable { case container, field }47@FocusState private var focus: Focus?4849VStack {50TextField("Name", text: $name)51.focused($focus, equals: .field)52}53.focusable()54.focused($focus, equals: .container)55```5657With `focused(_:equals:)` and a single `@FocusState`, SwiftUI distinguishes the container *receiving* focus from the container merely *containing* focus.5859### `isFocused` environment value6061Read-only environment value that returns `true` when the nearest focusable ancestor has focus. Useful for styling non-focusable child views.6263```swift64struct HighlightWrapper: View {65@Environment(\.isFocused) private var isFocused6667var body: some View {68content69.background(isFocused ? Color.accentColor.opacity(0.1) : .clear)70}71}72```7374## Making Views Focusable7576### `.focusable(_:)`7778Makes 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`.7980```swift81struct SelectableCard: View {82@FocusState private var isFocused: Bool8384var body: some View {85CardContent()86.focusable()87.focused($isFocused)88.border(isFocused ? Color.accentColor : .clear)89.onDeleteCommand { deleteCard() }90}91}92```9394### `.focusable(_:interactions:)` (iOS 17+)9596Controls which focus-driven interactions the view supports via `FocusInteractions`:9798- `.activate` -- Button-like: only focusable when system-wide keyboard navigation is on (macOS/iOS)99- `.edit` -- Captures keyboard/Digital Crown input100- `.automatic` -- Platform default (both activate and edit)101102```swift103MyTapGestureView(...)104.focusable(interactions: .activate)105```106107Use `.activate` for custom button-like views that should match system keyboard-navigation behavior.108109## Focused Values for Commands and Menus110111Focused 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.112113### Declare with `@Entry`114115```swift116extension FocusedValues {117@Entry var selectedDocument: Binding<Document>?118}119```120121Focused 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.122123### Publish from views124125```swift126// View-scoped: available when this view (or descendant) has focus127.focusedValue(\.selectedDocument, $document)128129// Scene-scoped: available when this scene has focus130.focusedSceneValue(\.selectedDocument, $document)131```132133### Consume in commands134135`@FocusedValue` reads the value; `@FocusedBinding` unwraps a `Binding` automatically.136137```swift138@main139struct MyApp: App {140@FocusedBinding(\.selectedDocument) var document141142var body: some Scene {143WindowGroup {144ContentView()145}146.commands {147CommandGroup(after: .pasteboard) {148Button("Duplicate") { document?.duplicate() }149.disabled(document == nil)150}151}152}153}154```155156### `@FocusedObject` (iOS 16+)157158For `ObservableObject` types. The view invalidates when the focused object changes.159160```swift161// Publish162.focusedObject(myObservableModel)163164// Consume165@FocusedObject var model: MyModel?166```167168Scene-scoped variant: `.focusedSceneObject(_:)`.169170## Default Focus171172### `.defaultFocus(_:_:priority:)` (iOS 17+, macOS 13+, tvOS 16+)173174Prefer `.defaultFocus` over setting `@FocusState` in `onAppear` for initial focus placement.175176```swift177@FocusState private var focusedField: Field?178179VStack {180TextField("Name", text: $name)181.focused($focusedField, equals: .name)182TextField("Email", text: $email)183.focused($focusedField, equals: .email)184}185.defaultFocus($focusedField, .email)186```187188**Priority**: `.automatic` (default) applies on window appearance and programmatic focus changes. `.userInitiated` also applies during user-driven focus navigation.189190### `prefersDefaultFocus(_:in:)` (macOS/tvOS/watchOS)191192Used with `.focusScope(_:)` to mark a preferred default target within a scoped region.193194### `resetFocus` environment action (macOS/tvOS/watchOS)195196Re-evaluates default focus within a namespace.197198```swift199@Namespace var scopeID200@Environment(\.resetFocus) private var resetFocus201202Button("Reset") { resetFocus(in: scopeID) }203```204205## Focus Scope and Sections206207### `.focusScope(_:)` (macOS/tvOS/watchOS)208209Limits default focus preferences to a namespace. Use with `prefersDefaultFocus` and `resetFocus`.210211### `.focusSection()` (macOS 13+, tvOS 15+)212213Guides 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.214215```swift216HStack {217VStack { Button("1") {}; Button("2") {}; Spacer() }218Spacer()219VStack { Spacer(); Button("A") {}; Button("B") {} }220.focusSection()221}222```223224Without `.focusSection()`, swiping right from buttons 1/2 finds nothing. With it, the VStack receives directional focus and delivers it to its first focusable child.225226## Focus Effects227228### `.focusEffectDisabled(_:)`229230Suppresses the system focus ring (macOS) or hover effect. Use when providing custom focus visuals.231232```swift233MyCustomCard()234.focusable()235.focusEffectDisabled()236.overlay { customFocusRing }237```238239`isFocusEffectEnabled` environment value reads the current state.240241## Search Focus242243### `.searchFocused(_:)` / `.searchFocused(_:equals:)`244245Bind focus state to the search field associated with the nearest `.searchable` modifier. Works like `.focused` but targets the search bar.246247```swift248@FocusState private var isSearchFocused: Bool249250NavigationStack {251ContentView()252.searchable(text: $query)253.searchFocused($isSearchFocused)254}255256// Programmatically focus the search bar257Button("Search") { isSearchFocused = true }258```259260## Common Pitfalls261262### Redundant `@FocusState` writes revoke focus263264`.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.265266```swift267// WRONG -- tap gesture redundantly sets focus, causing double evaluation268CardView()269.focusable()270.focused($isFocused)271.onTapGesture { isFocused = true } // Remove this line272273// CORRECT -- let .focusable() + .focused() handle it274CardView()275.focusable()276.focused($isFocused)277```278279### Ambiguous focus bindings280281Binding the same enum case to multiple views is ambiguous. SwiftUI picks the first candidate and emits a runtime warning.282283```swift284// WRONG -- .name bound to two views285TextField("Name", text: $name)286.focused($focusedField, equals: .name)287TextField("Full Name", text: $fullName)288.focused($focusedField, equals: .name) // ambiguous289```290291Always use distinct enum cases for each focusable view.292293### `.onAppear` focus timing294295Setting `@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.296297### Missing `.focusable()` for non-text views298299`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.300