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.

referencesenvironment-patterns.md

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

SwiftUI Environment Patterns

Use this reference for @Environment, EnvironmentValues, @Entry, @FocusedValue, and values propagated through the environment.

Read Framework Values

Use @Environment to read framework-provided values and actions from the nearest ancestor:

struct DetailView: View {
    @Environment(\.dismiss) private var dismiss
    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        Button("Done") { dismiss() }
    }
}

Keep the declaration in the smallest view that uses it so environment changes do not invalidate a broader parent.

Share Observable Models

On iOS 17+ and aligned platforms, inject an @Observable model by type and read it with type-based @Environment:

@MainActor
@Observable
final class AppState {
    var isLoggedIn = false
}

RootView()
    .environment(AppState())

struct AccountView: View {
    @Environment(AppState.self) private var appState

    var body: some View {
        Text(appState.isLoggedIn ? "Signed in" : "Signed out")
    }
}

Observation tracks the model properties read during body, so unrelated properties do not invalidate the view. Use @Bindable locally when a control needs bindings to the injected model.

For older deployment targets, @EnvironmentObject with .environmentObject(...) remains the corresponding ObservableObject pattern. Do not create the shared model inline at multiple reader sites.

Prefer @Entry for Custom Values

Use @Entry instead of manual key conformances when defining custom environment, transaction, container, or focused values:

extension EnvironmentValues {
    @Entry var accentTheme: Theme = .default
}

extension FocusedValues {
    @Entry var selectedDocument: Document?
}

Focused-value entries are optional and do not specify a non-nil default.

When reviewing existing manual EnvironmentKey / FocusedValueKey boilerplate, surface an @Entry refactor as a top-line finding. Do not rewrite it unprompted.

@FocusedValue uses the same comparison model as @Environment. Rules below for closures, defaults, unused reads, and high-frequency updates apply to both.

Never Store Closures in Custom Keys

SwiftUI cannot reliably compare functions. A closure in a custom environment or focused-value key can therefore make every reader invalidate whenever the environment propagates. Wrapping the closure in a struct or storing it on a View does not fix comparison; the closure is still present.

Framework action values such as \.dismiss, \.openURL, and \.refresh are designed for this purpose and are not affected by this rule.

Represent custom behavior with a value that stores comparable inputs and exposes a method or callAsFunction, or share an @Observable model when the behavior belongs with shared state:

// AVOID
extension EnvironmentValues {
    @Entry var submit: (String) -> Void = { _ in }
}

// PREFER
struct SubmitAction {
    func callAsFunction(_ draft: String) { /* submit */ }
}

extension EnvironmentValues {
    @Entry var submit = SubmitAction()
}

When injected handlers differ by context (form vs cart, independent implementations), one option is a protocol plus concrete handler types stored as the @Entry value. When handlers share state and the set is closed, a single @Observable model can be simpler.

Keep Default Values Stable

An @Entry default expression is evaluated when a reader falls back to it. The default is unstable when repeated evaluation produces a different value, such as a fresh class instance, Date(), UUID(), or a struct containing a newly allocated reference. Any unrelated environment write can then make fallback readers appear changed.

// AVOID: creates a different instance on each fallback read
extension EnvironmentValues {
    @Entry var model = Model()
}

// PREFER: resolves to the same instance
extension EnvironmentValues {
    @Entry var model = defaultModel
    private static let defaultModel = Model()
}

Use an optional with a nil default when absence is meaningful. This is preferable when readers currently test a sentinel such as an empty identifier:

extension EnvironmentValues {
    @Entry var editingSession: EditingSession?
}

Equatable conformance does not repair an unstable default: the expression still allocates or changes on every read. Conversely, do not rewrite already-stable defaults. Literals, enum cases without associated values, nil, and structs built only from deterministic values or stable references are stable even without Equatable.

A live unstable default has readers falling back and paying invalidation now. A latent one is currently covered by an upstream .environment injection; fixing it is still correct but is a regression guard, not a current-cost recovery.

A manual EnvironmentKey with static let defaultValue is a deliberate stability fix (evaluated once). Do not use static var defaultValue: T { Model() } — that re-evaluates on every fallback read, the same problem @Entry has with an inline allocation.

Avoid High-Frequency Environment Updates

Every environment write propagates through the subtree and makes environment readers check their values. Do not put per-frame or rapidly changing measurements such as scroll offsets, drag positions, geometry, timer ticks, or animation progress in custom environment keys.

For visual scroll effects, prefer scrollTransition or visualEffect(in:). When the value drives logic, consider an @Observable model that exposes a coarsened property such as isWide instead of raw width:

@MainActor
@Observable
final class ViewportModel {
    var width: CGFloat = 0 {
        didSet { isWide = width > 600 }
    }

    private(set) var isWide = false
}

The model alone is not the optimization: readers must observe a value that changes less often than the raw input.

A shared Set of visible indices on one @Observable fires only on boundary crosses, which is better than a raw offset. Observation still tracks the whole Set property, so every row that read it invalidates. Persist a per-item @Observable whose own properties (for example isVisible) each row reads.

Remove Unused Reads

An unused key-path declaration such as @Environment(\.theme) still subscribes the view to that key. Remove it when neither body nor anything called from body reads the value.

Type-based @Environment(Model.self) uses Observation's property-level tracking. Merely declaring the model without reading one of its properties does not establish the same live dependency, though removing dead declarations still improves clarity.

Checklist

  • Custom values use @Entry; flag leftover manual keys without rewriting them unprompted
  • Custom environment and focused-value keys do not store closures
  • Default expressions return the same result on every fallback read (live or latent)
  • @FocusedValue follows the same comparison and unused-read rules as @Environment
  • Optional defaults represent semantic absence instead of sentinel instances
  • High-frequency raw values do not flow through the environment
  • Key-path environment declarations are actually read

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.