All skills
avdlee avatar

/swiftui-expert-skill

@1e522cf

Use when writing, reviewing, or refactoring SwiftUI code for iOS or macOS, including state and `@Observable` data flow, view composition, resizable layouts, safe areas, display scale, performance, lists, environment, localization, animation, Liquid Glass, and API migration. Also use for iPhone Duo, foldable, or large-display layouts (`NavigationSplitView` on large displays, two-column reflow, foldable grids, `ArrangementView`, `ReservedRegion`), hinge effects, vertical bars, `@State` initialization or synthesized-property diagnostics, `@ContentBuilder` ambiguity, `reorderable` drag/drop, custom `AsyncImage` `URLSession`, swipe actions outside List, item-bound `alert`/`confirmationDialog`, `ToolbarOverflowMenu`, `AnimatableValues`, Document APIs (`Document`/`DocumentReader`), and Instruments `.trace` capture or analysis.

Use this Skill: https://skilld.dev/gh/avdlee/swiftui-agent-skill/swiftui-expert-skill

This session only. Nothing lands on disk.

referencesfocus-patterns.md

≈2.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

SwiftUI Focus Patterns Reference

Table of Contents

@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 @Entry values must be optional and use the implicit nil default, representing that no focused view currently publishes the 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)  // ambiguous

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

Source: SKILL.md on GitHub

No alerts2d5 checks · Risk SAFE
  • Gen Agent Trust Hub2d

    The skill is a professional-grade assistant for SwiftUI development and performance profiling. It includes Python scripts to interface with the Xcode xctrace CLI for recording and analyzing Instruments traces. All identified code and instructions are consistent with its stated purpose and follow security best practices.

  • Socket2d

    No alerts

  • Snyk2d

    Risk: LOW · No issues

  • Runlayer6mo

    19 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 1e522cf. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 3 days ago
  • Performance
  • swiftui
  • ios
  • macos
  • instruments
  • state-management
  • view-composition
  • accessibility
  • animations

README badge

README badge for avdlee/swiftui-agent-skill/swiftui-expert-skill

Provides guidance for writing, reviewing, and refactoring SwiftUI code for iOS and macOS, including state management, view composition, performance optimization, and Instruments trace recording and analysis. Covers deprecated API detection, animation patterns, accessibility, and Liquid Glass adoption.

Generated from the current SKILL.md.

Does this skill work with both iOS and macOS?
Yes. The skill covers SwiftUI for both iOS and macOS, with dedicated reference sections for macOS-specific patterns like scenes, window styling, and views (HSplitView, Table, PasteButton).
Can this skill help me record and analyze Instruments traces?
Yes. The skill includes workflows to record traces via `record_trace.py` (with template selection for real devices vs simulators) and analyze them via `analyze_trace.py` to identify hangs, hitches, CPU hotspots, and excessive view updates.
Does this skill enforce a specific architecture pattern?
No. It focuses on correctness and performance without mandating MVVM, VIPER, or other architectural styles, though it encourages separating business logic from views for testability.
What does the skill do about deprecated APIs?
It consults `references/latest-apis.md` at the start of every task to identify and replace deprecated APIs with modern equivalents across iOS 15+ through iOS 26+, and gates version-specific APIs with `#available`.
Does this skill handle Liquid Glass effects?
Yes, but only when explicitly requested by the user. It includes guidance in `references/liquid-glass.md` for iOS 26+ Liquid Glass adoption with sensible fallbacks for earlier versions.

Generated from the current SKILL.md. These answers refresh after source changes.