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.

referencespreviews.md

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

SwiftUI Previews Reference

Table of Contents


Preview Macro

The #Preview macro (Swift 5.9+, Xcode 15+) is the modern way to declare previews. The legacy PreviewProvider protocol still works; prefer #Preview for new code because it's less verbose and supports inline traits.

Basic Usage

// Modern: #Preview macro
#Preview {
    ContentView()
}

// Named preview
#Preview("Dark Mode") {
    ContentView()
        .preferredColorScheme(.dark)
}

// Legacy: PreviewProvider — still valid, but verbose for new code
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Multiple Previews

Declare one #Preview per meaningful state so each renders independently in the canvas:

#Preview("Default") {
    SettingsRow(title: "Notifications", isOn: true)
}

#Preview("Off State") {
    SettingsRow(title: "Notifications", isOn: false)
}

#Preview("Long Title") {
    SettingsRow(title: "Enable Push Notifications for All Events", isOn: true)
}

Preview Traits

Traits configure the preview environment without modifying the view itself:

// Fixed size
#Preview(traits: .fixedLayout(width: 300, height: 100)) {
    CompactBanner(message: "Welcome")
}

// Size that fits content
#Preview(traits: .sizeThatFitsLayout) {
    BadgeView(count: 5)
}

// Landscape orientation
#Preview(traits: .landscapeLeft) {
    DashboardView()
}

Previewing Inside NavigationStack

Wrap previewed destinations in their navigation container so toolbar items, titles, and back buttons render correctly:

#Preview {
    NavigationStack {
        DetailView(item: .sample)
    }
}

Preview with Mock Data

Previews must compile and render without external dependencies. Live services, network calls, and disk I/O make previews slow, flaky, or broken; use self-contained sample data instead.

Static Sample Data

Expose sample values as static properties on the model itself so any preview can reuse them without reconstructing values inline:

struct Item: Identifiable {
    let id: UUID
    var name: String
    var price: Double
}

extension Item {
    static let sample = Item(id: UUID(), name: "Widget", price: 9.99)

    static let samples: [Item] = [
        Item(id: UUID(), name: "Widget", price: 9.99),
        Item(id: UUID(), name: "Gadget", price: 19.99),
        Item(id: UUID(), name: "Doohickey", price: 4.99),
    ]
}

#Preview {
    ItemListView(items: Item.samples)
}

Mock Observable Models

For views driven by an @Observable model (see state-management.md for fundamentals), expose pre-configured instances on the model itself:

@Observable
@MainActor
final class CartModel {
    var items: [Item] = []
    var isLoading = false

    static var preview: CartModel {
        let model = CartModel()
        model.items = Item.samples
        return model
    }

    static var emptyPreview: CartModel {
        CartModel()
    }

    static var loadingPreview: CartModel {
        let model = CartModel()
        model.isLoading = true
        return model
    }
}

#Preview("With Items") {
    CartView()
        .environment(CartModel.preview)
}

#Preview("Empty") {
    CartView()
        .environment(CartModel.emptyPreview)
}

#Preview("Loading") {
    CartView()
        .environment(CartModel.loadingPreview)
}

Preview with Environment Dependencies

Inject any environment values the view depends on so the preview reflects a realistic runtime context:

#Preview {
    OrderDetailView(order: .sample)
        .environment(CartModel.preview)
        .environment(\.locale, Locale(identifier: "ja_JP"))
        .environment(\.dynamicTypeSize, .xxxLarge)
}

Mocking Async Data Sources

When a view depends on a network or data service, give the dependency a protocol abstraction so previews can inject a synchronous mock that returns sample data immediately. This is one approach — adapt it to whatever pattern the surrounding codebase already uses.

protocol DataFetching {
    func fetchItems() async throws -> [Item]
}

struct LiveDataFetcher: DataFetching {
    let url: URL

    func fetchItems() async throws -> [Item] {
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([Item].self, from: data)
    }
}

struct MockDataFetcher: DataFetching {
    var result: Result<[Item], Error> = .success(Item.samples)

    func fetchItems() async throws -> [Item] {
        try result.get()
    }
}

#Preview {
    ItemListView(fetcher: MockDataFetcher())
}

#Preview("Error State") {
    ItemListView(fetcher: MockDataFetcher(result: .failure(URLError(.notConnectedToInternet))))
}

@Previewable Property Wrappers

@Previewable (iOS 18+, Xcode 16+) lets you use @State, @FocusState, and other property wrappers directly inside a #Preview block, removing the need for a wrapper view to host interactive state.

Interactive State

// @Previewable: interactive toggle inline in the preview
#Preview {
    @Previewable @State var isOn = false
    Toggle("Notifications", isOn: $isOn)
}

// Without @Previewable: requires a wrapper view
struct TogglePreviewWrapper: View {
    @State private var isOn = false
    var body: some View {
        Toggle("Notifications", isOn: $isOn)
    }
}

#Preview {
    TogglePreviewWrapper()
}

Multiple Interactive Controls

#Preview {
    @Previewable @State var name = "Alice"
    @Previewable @State var age = 25.0

    VStack {
        TextField("Name", text: $name)
        Slider(value: $age, in: 0...100, step: 1) {
            Text("Age: \(Int(age))")
        }
        Text("Hello, \(name)! Age: \(Int(age))")
    }
    .padding()
}

@Previewable with @FocusState

When seeding initial focus inside a preview, prefer .defaultFocus over writing to @FocusState from .onAppear. .onAppear can race the initial render and the focus assignment may be lost. See focus-patterns.md for the underlying rationale.

#Preview {
    @Previewable @FocusState var isFocused: Bool

    TextField("Search", text: .constant(""))
        .focused($isFocused)
        .defaultFocus($isFocused, true)
}

Fallback for Pre-iOS 18 Targets

If the project's minimum deployment target is below iOS 18, @Previewable is unavailable. Fall back to a wrapper view:

private struct SliderPreview: View {
    @State private var value = 0.5
    var body: some View {
        CustomSlider(value: $value)
    }
}

#Preview {
    SliderPreview()
}

Common Diagnostics

Symptom Cause Fix
#Preview body type mismatch The closure returns a non-View type Make sure the final expression is a View
@Previewable only available in iOS 18+ Using @Previewable with a lower deployment target Use a wrapper view, or gate with #available
Preview crashes with "missing environment" An @Environment(SomeType.self) value is not injected Add .environment(SomeType.preview) to the preview
Preview hangs or renders blank View depends on async data that never resolves Inject a mock that returns immediately with sample data
@MainActor-isolated model accessed from non-isolated context A preview helper touches main-actor-only API off the main actor Mark the helper or the preview body @MainActor

Summary Checklist

  • Prefer #Preview for new previews; PreviewProvider is still valid for older code
  • Provide a named preview for each meaningful state (default, empty, error, loading)
  • Use @Previewable for interactive previews when targeting iOS 18+; wrapper views otherwise
  • Expose static .sample / .preview data on models so previews don't reconstruct values inline
  • Inject mock services through a protocol when a view depends on async data
  • Never depend on live network or disk I/O in a preview
  • Prefer .defaultFocus over .onAppear writes when seeding @FocusState in previews

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.