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.

referencessheet-navigation-patterns.md

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

SwiftUI Sheet, Navigation & Inspector Patterns Reference

Table of Contents

Sheet Patterns

Item-Driven Sheets (Preferred)

Use .sheet(item:) instead of .sheet(isPresented:) when presenting model-based content.

// Good - item-driven
@State private var selectedItem: Item?

var body: some View {
    List(items) { item in
        Button(item.name) {
            selectedItem = item
        }
    }
    .sheet(item: $selectedItem) { item in
        ItemDetailSheet(item: item)
    }
}

// Avoid - boolean flag requires separate state
@State private var showSheet = false
@State private var selectedItem: Item?

var body: some View {
    List(items) { item in
        Button(item.name) {
            selectedItem = item
            showSheet = true
        }
    }
    .sheet(isPresented: $showSheet) {
        if let selectedItem {
            ItemDetailSheet(item: selectedItem)
        }
    }
}

Why: .sheet(item:) automatically handles presentation state and avoids optional unwrapping in the sheet body.

Sheets Own Their Actions

Sheets should handle their own dismiss and actions internally using @Environment(\.dismiss). Avoid passing onSave/onCancel closures from the parent -- it creates callback prop-drilling and reduces reusability.

struct EditItemSheet: View {
    @Environment(\.dismiss) private var dismiss
    let item: Item
    @State private var name: String

    init(item: Item) {
        self.item = item
        _name = State(initialValue: item.name)
    }

    var body: some View {
        NavigationStack {
            Form { TextField("Name", text: $name) }
                .navigationTitle("Edit Item")
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }
                    ToolbarItem(placement: .confirmationAction) { Button("Save") { /* save and dismiss */ } }
                }
        }
    }
}

Enum-Based Sheet Management

When presenting multiple different sheets, use an Identifiable enum with .sheet(item:) instead of multiple boolean state properties:

struct ArticlesView: View {
    enum Sheet: Identifiable {
        case add, edit(Article), categories
        var id: String {
            switch self {
            case .add: "add"
            case .edit(let a): "edit-\(a.id)"
            case .categories: "categories"
            }
        }
    }

    @State private var presentedSheet: Sheet?

    var body: some View {
        List { /* ... */ }
            .toolbar {
                Button("Add") { presentedSheet = .add }
            }
            .sheet(item: $presentedSheet) { sheet in
                switch sheet {
                case .add: AddArticleView()
                case .edit(let article): EditArticleView(article: article)
                case .categories: CategoriesView()
                }
            }
    }
}

Why: A single @State property and one .sheet(item:) modifier replaces N boolean properties and N sheet modifiers, improving readability and preventing only-one-sheet-at-a-time conflicts.

Item-Driven Alerts and Confirmation Dialogs (SDK 27)

SDK 27 adds alert(_:item:actions:message:) and confirmationDialog(_:item:titleVisibility:actions:message:). The optional binding alone drives presentation, the unwrapped value is passed to the action and message closures, and dismissal resets the binding to nil. The item does not need to conform to Identifiable.

@State private var photoToDelete: Photo?

var body: some View {
    PhotoList { photoToDelete = $0 }
        .confirmationDialog(
            "Delete photo?",
            item: $photoToDelete
        ) { photo in
            Button("Delete \(photo.name)", role: .destructive) {
                delete(photo)
            }
        } message: { photo in
            Text("\(photo.name) will be removed.")
        }
}

Prefer the item overload for an action tied to an optional value instead of synchronizing a separate Boolean or pairing isPresented with presenting:. Do not use the older Alert-returning alert(item:). These overloads require the SDK 27 toolchain but back-deploy to iOS 15, macOS 12, tvOS 15, watchOS 8, and visionOS 1; no runtime availability gate is needed at those deployment targets.

Navigation Patterns

Type-Safe Navigation with NavigationStack

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Profile", value: Route.profile)
                NavigationLink("Settings", value: Route.settings)
            }
            .navigationDestination(for: Route.self) { route in
                switch route {
                case .profile:
                    ProfileView()
                case .settings:
                    SettingsView()
                }
            }
        }
    }
}

enum Route: Hashable {
    case profile
    case settings
}

Programmatic Navigation

struct ContentView: View {
    @State private var navigationPath = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            List {
                Button("Go to Detail") {
                    navigationPath.append(DetailRoute.item(id: 1))
                }
            }
            .navigationDestination(for: DetailRoute.self) { route in
                switch route {
                case .item(let id):
                    ItemDetailView(id: id)
                }
            }
        }
    }
}

enum DetailRoute: Hashable {
    case item(id: Int)
}

Multi-Column Navigation with NavigationSplitView

Two-Column Layout

Use NavigationSplitView for sidebar-driven navigation. Available on iOS 16+, macOS 13+, tvOS 16+, watchOS 9+.

struct ContentView: View {
    @State private var selectedItem: Item.ID?

    var body: some View {
        NavigationSplitView {
            List(items, selection: $selectedItem) { item in
                Text(item.name)
            }
            .navigationTitle("Items")
        } detail: {
            if let selectedItem, let item = items.first(where: { $0.id == selectedItem }) {
                ItemDetailView(item: item)
            } else {
                ContentUnavailableView("Select an Item", systemImage: "doc")
            }
        }
    }
}

Three-Column Layout

struct ContentView: View {
    @State private var departmentId: Department.ID?
    @State private var employeeIds = Set<Employee.ID>()

    var body: some View {
        NavigationSplitView {
            List(model.departments, selection: $departmentId) { dept in
                Text(dept.name)
            }
        } content: {
            if let department = model.department(id: departmentId) {
                List(department.employees, selection: $employeeIds) { emp in
                    Text(emp.name)
                }
            } else {
                Text("Select a department")
            }
        } detail: {
            EmployeeDetails(for: employeeIds)
        }
    }
}

Configuration

  • Column visibility: NavigationSplitView(columnVisibility: $visibility) with NavigationSplitViewVisibility (.detailOnly, .doubleColumn, .all)
  • Column widths: .navigationSplitViewColumnWidth(min:ideal:max:) on each column
  • Compact column: NavigationSplitView(preferredCompactColumn: $column) to control which column shows on narrow devices
  • Style: .navigationSplitViewStyle(.balanced) or .prominentDetail (default)

Platform Behavior

Platform Behavior
macOS Columns always visible side-by-side; sidebar has translucent material; variable-width column resizing by dragging
iPadOS (regular) Sidebar can overlay or push detail; supports column visibility toggle via toolbar button
iOS / iPadOS (compact) Collapses into a single NavigationStack; sidebar items show disclosure chevrons; back button navigates between columns
iOS / iPadOS (regular) Can show columns tiled or as overlays, depending on available size and context
watchOS / tvOS Collapses into a single stack

Do not infer split-view behavior from the device family. Respond to the space SwiftUI offers: an iPhone can provide a regular-width context, including the inner display of iPhone Duo, where NavigationSplitView can show multiple columns. The same scene can later become compact and collapse, so keep selection and navigation state consistent through the transition.

Large Displays

When rows push further screens (settings, mailboxes, folders), NavigationSplitView shows the next level beside the list on large displays and collapses on compact width; see the screen-structure rule.

  • Keep the sidebar visible with columnVisibility .all plus toolbar(removing: .sidebarToggle) when hiding the list would strand the user.
  • Use navigationSplitViewColumnWidth(min:ideal:) if sidebar cards or buttons wrap at the default width. Avoid max:: in a fold-aligned pose the system can widen the sidebar to the fold, and a maximum caps it short.
  • Consider choosing the default detail by importance, not position. When pushed pages are secondary, a regular-width-only overview page selected from a summary row atop the sidebar (as in Settings) can beat auto-selecting the first row.
  • The split view can reset selection to nil on expand: re-fill it on regular width, and clear regular-only selections on collapse so compact width returns to the list.
  • Use a NavigationStack in the detail column for deeper pushes.
  • In Xcode 27.1, a selectable List rendered Link and Button rows in the primary color rather than the tint.

Inspector

Availability: iOS 17.0+, macOS 14.0+

A trailing-edge panel for supplementary information.

On wider size classes (macOS, iPad landscape), it appears as a trailing column. On compact size classes (iPhone), it adapts to a sheet automatically.

Basic Inspector

struct ShapeEditor: View {
    @State private var showInspector = false

    var body: some View {
        MyEditorView()
            .inspector(isPresented: $showInspector) {
                InspectorContent()
            }
            .toolbar {
                ToolbarItem {
                    Button {
                        showInspector.toggle()
                    } label: {
                        Label("Inspector", systemImage: "info.circle")
                    }
                }
            }
    }
}

Inspector with Column Width

MyEditorView()
    .inspector(isPresented: $showInspector) {
        InspectorContent()
            .inspectorColumnWidth(min: 200, ideal: 250, max: 400)
    }

Inspector with Fixed Width

MyEditorView()
    .inspector(isPresented: $showInspector) {
        InspectorContent()
            .inspectorColumnWidth(300)
    }

Platform Behavior

Platform Behavior
macOS Trailing-edge sidebar panel; resizable by dragging edge; integrates with window toolbar
iPadOS (regular) Trailing column alongside content; toggleable via toolbar button
iOS / iPadOS (compact) Adapts to a sheet presentation; swipe-to-dismiss supported
iOS / iPadOS (regular) Can appear as a trailing column when the presentation context provides enough space

Tip: Use InspectorCommands in your app's .commands to include the default inspector toggle keyboard shortcut.

Presentation Modifiers

Full Screen Cover

struct ContentView: View {
    @State private var showFullScreen = false
    
    var body: some View {
        Button("Show Full Screen") {
            showFullScreen = true
        }
        .fullScreenCover(isPresented: $showFullScreen) {
            FullScreenView()
        }
    }
}

Popover

struct ContentView: View {
    @State private var showPopover = false
    
    var body: some View {
        Button("Show Popover") {
            showPopover = true
        }
        .popover(isPresented: $showPopover) {
            PopoverContentView()
                .presentationCompactAdaptation(.popover)  // Don't adapt to sheet on iPhone
        }
    }
}

For older alert and confirmationDialog API patterns, see latest-apis.md. Prefer the SDK 27 item overloads above when the presentation is tied to an optional value.

Summary Checklist

  • Use .sheet(item:) for model-based sheets
  • Sheets own their actions and dismiss internally
  • Use NavigationStack with navigationDestination(for:) for type-safe navigation
  • Use NavigationPath for programmatic navigation
  • Use NavigationSplitView for sidebar-driven multi-column layouts
  • Use Inspector for trailing-edge supplementary panels
  • Set column widths with navigationSplitViewColumnWidth(min:ideal:max:) or inspectorColumnWidth(min:ideal:max:)
  • Use appropriate presentation modifiers (sheet, fullScreenCover, popover)
  • Alerts and confirmation dialogs use modern API with actions; prefer the SDK 27 item: overload for an optional value
  • Avoid passing dismiss/save callbacks to sheets
  • Use enum-based Identifiable type with .sheet(item:) when presenting multiple sheets
  • Navigation state can be saved/restored when needed

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.