Skip to content

SwiftUI Focus Patterns Reference

Always mark @FocusState as private. Use Bool for a single field, an optional Hashable enum for multiple fields.

@FocusState private var isFocused: Bool
TextField("Email", text: $email)
.focused($isFocused)
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

Section titled “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.

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)
}
}

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() }
}
}

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 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.

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.

// View-scoped: available when this view (or descendant) has focus
.focusedValue(\.selectedDocument, $document)
// Scene-scoped: available when this scene has focus
.focusedSceneValue(\.selectedDocument, $document)

@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)
}
}
}
}

For ObservableObject types. The view invalidates when the focused object changes.

// Publish
.focusedObject(myObservableModel)
// Consume
@FocusedObject var model: MyModel?

Scene-scoped variant: .focusedSceneObject(_:).

.defaultFocus(_:_:priority:) (iOS 17+, macOS 13+, tvOS 16+)

Section titled “.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)

Section titled “prefersDefaultFocus(_:in:) (macOS/tvOS/watchOS)”

Used with .focusScope(_:) to mark a preferred default target within a scoped region.

resetFocus environment action (macOS/tvOS/watchOS)

Section titled “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) }

Limits default focus preferences to a namespace. Use with prefersDefaultFocus and resetFocus.

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.

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.

.searchFocused(_:) / .searchFocused(_:equals:)

Section titled “.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 }

.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)

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) // ambiguous

Always use distinct enum cases for each focusable view.

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.

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.