Skill: kageroumado/swiftui-macos (version 1.0.0)

--- name: swiftui-macos description: Expert SwiftUI guidance for macOS apps — covers writing and reviewing code with runtime-level understanding of observation, concurrency, performance, and platform integration. Use when user asks to "build a SwiftUI view", "review my SwiftUI code", "fix observation issues", "bridge AppKit", "architect state management", "debug view updates", or works on any macOS SwiftUI project — building features, reviewing existing code, debugging view updates or performance, fixing concurrency issues, architecting state management, bridging AppKit, or making design decisions. Do NOT use for iOS/iPadOS-only SwiftUI, UIKit bridging, or basic Swift language questions. Focuses on non-obvious traps and deep patterns, not basics. license: MIT compatibility: Requires macOS 26+ targeting and Swift 6.2 with strict concurrency. Best suited for Claude Code and environments with filesystem access to load references on demand. metadata: author: kageroumado version: 1.0.0 --- Guide SwiftUI development for macOS apps. Whether writing new code or reviewing existing code, apply deep understanding of SwiftUI's runtime behavior — how the observation registrar dispatches, how the attribute graph diffs view trees, how actor isolation interacts with view lifecycles. ## How to Use **Writing code**: Load relevant references before implementation. The patterns here prevent bugs that are expensive to find later — observation cascades, identity thrashing, concurrency overhead. When making architecture decisions (state ownership, view decomposition, AppKit bridging), consult the relevant reference for tradeoffs. **Reviewing code**: Check each reference area systematically. Focus on genuine problems — observation inefficiencies, deprecated API, accessibility gaps, concurrency anti-patterns. Don't flag obvious issues or invent problems. **Partial scope**: Load only what the task needs. Not every change requires all eight references. ## References | Reference | Load when | |---|---| | `references/observation.md` | `@Observable`, `@State`, `ForEach`, data-driven views, `Observations {}` streams | | `references/concurrency.md` | `Task`, `async`/`await`, actors, `DispatchQueue`, `AsyncStream`, scheduling decisions | | `references/performance.md` | View composition, animations, gestures, large collections, `Canvas`, `TimelineView` | | `references/views.md` | View decomposition, navigation, `.task(id:)`, preference keys, custom `Layout` | | `references/data.md` | State architecture, SwiftData, environment injection, bindings, persistence | | `references/platform.md` | `NSViewRepresentable`, `NSHostingView`, multi-window, AppKit bridging, window chrome | | `references/api.md` | API choice, deprecated patterns, modern Swift/SwiftUI idioms | | `references/accessibility.md` | VoiceOver, keyboard navigation, Dynamic Type, Reduce Motion | ## Principles - Target **macOS 26+**, Swift 6.2 with strict concurrency and `@MainActor` default isolation. - Check whether the project enables **`NonisolatedNonsendingByDefault`** (SE-0461) — this changes where `nonisolated` async functions execute. See `references/concurrency.md`. - Prefer SwiftUI-native solutions. Use AppKit (`NSViewRepresentable`, `NSHostingView`, `NSWindow`) only when SwiftUI has no equivalent. - **Understand the mechanism.** When suggesting a pattern, know *why* it works — what the observation registrar does, what the attribute graph diffing costs, what `_modify` vs `set` means for notification. Rules without understanding produce cargo-culted code. - Do not introduce third-party frameworks without asking. Apple's open-source Swift packages (`swift-collections`, `swift-algorithms`, `swift-async-algorithms`) are exceptions — prefer them over reimplementing non-trivial data structures or algorithms. See `references/api.md`. ## Examples **User**: "Review observation patterns in these sidebar views" → Load `references/observation.md`. Check each file for: state capture in ForEach, non-Equatable types on observed properties, missing `@ObservationIgnored` on caches, `_modify` vs `set` notification waste. **User**: "I need to embed an NSCollectionView in SwiftUI" → Load `references/platform.md`. Apply `NSViewRepresentable` lifecycle, `Equatable` conformance for update gating, `Coordinator` for delegates. **User**: "This view is re-rendering too much, help me debug it" → Load `references/performance.md` and `references/observation.md`. Check view identity (structural vs explicit), observation scope (are unrelated properties tracked in the same body?), `@State` vs `@Observable` for gesture-driven values. ## Review Output Organize findings by file. For each issue: 1. File and relevant line(s). 2. The pattern being violated and *why it matters* at the runtime level. 3. Brief before/after code fix. Skip clean files. End with a prioritized summary of the most impactful changes.

path: references/accessibility.md
content: # Accessibility

## VoiceOver

**Icon-only buttons are invisible.** Always include a text label:

```swift
// Bad: VoiceOver can't describe this
Button(action: add) { Image(systemName: "plus") }

// Good
Button("Add Item", systemImage: "plus", action: add)
```

Same applies to `Menu`:

```swift
Menu("Options", systemImage: "ellipsis.circle") { ... }
```

**`onTapGesture` is not a button.** VoiceOver can't activate it. Use `Button` unless you specifically need tap location or count. If `onTapGesture` must be used, add `.accessibilityAddTraits(.isButton)`.

**Decorative images**: `Image(decorative:)` or `.accessibilityHidden(true)`. Flag images with meaningless generated labels like `Image(.newBanner2026)`.

**Complex button labels**: Use `accessibilityInputLabels()` for buttons with dynamic or frequently changing text. For example, a button showing "AAPL $271.68" should have an input label of "Apple" for Voice Control.

## Dynamic Type

- Never hard-code font sizes. Use semantic fonts: `.body`, `.headline`, `.caption`.
- `.caption2` is extremely small — avoid. Even `.caption` should be used carefully.
- Custom sizes: `@ScaledMetric` (macOS 14 and earlier) or `.font(.body.scaled(by:))` (macOS 26+).
- Avoid fixed frames that can't accommodate larger text sizes.

## Reduce Motion

Check `@Environment(\.accessibilityReduceMotion)` and replace motion-heavy animations with opacity crossfades:

```swift
@Environment(\.accessibilityReduceMotion) private var reduceMotion

withAnimation(reduceMotion ? .none : .spring) {
    // state change
}
```

## Color Independence

When color conveys meaning, check `accessibilityDifferentiateWithoutColor` and add secondary differentiators — icons, patterns, strokes, text labels.

## Accessibility Grouping

Use `accessibilityElement(children:)` to control how VoiceOver navigates composite views:

```swift
// Bad: VoiceOver reads icon, title, subtitle as three separate elements
HStack {
    Image(systemName: "folder")
    VStack(alignment: .leading) {
        Text(item.title)
        Text(item.subtitle).foregroundStyle(.secondary)
    }
}

// Good: combined into one element with composed label
HStack {
    Image(systemName: "folder")
    VStack(alignment: .leading) {
        Text(item.title)
        Text(item.subtitle).foregroundStyle(.secondary)
    }
}
.accessibilityElement(children: .combine)
```

Options: `.combine` (merge children into one element), `.contain` (group as a container), `.ignore` (skip children, provide custom label).

## Custom Control Accessibility

Use `accessibilityRepresentation` to give custom controls standard VoiceOver behavior without restructuring their visual appearance:

```swift
struct StarRating: View {
    @Binding var rating: Int

    var body: some View {
        HStack {
            ForEach(1...5, id: \.self) { star in
                Image(systemName: star <= rating ? "star.fill" : "star")
                    .onTapGesture { rating = star }
            }
        }
        .accessibilityRepresentation {
            Slider(value: .init(get: { Double(rating) }, set: { rating = Int($0) }),
                   in: 1...5, step: 1)
        }
    }
}
```

VoiceOver exposes this as a slider (adjustable trait, increment/decrement) while the visual remains star icons.

## Accessibility Rotors

`accessibilityRotor` defines custom VoiceOver navigation landmarks within a view. Users swipe up/down to jump between matches — essential for long lists, tables, or content with semantic categories:

```swift
List(items) { item in
    ItemRow(item: item)
}
.accessibilityRotor("Pinned") {
    ForEach(items.filter(\.isPinned)) { item in
        AccessibilityRotorEntry(item.title, id: item.id)
    }
}
.accessibilityRotor("Unread") {
    ForEach(items.filter { !$0.isRead }) { item in
        AccessibilityRotorEntry(item.title, id: item.id)
    }
}
```

Without rotors, VoiceOver users must navigate every item linearly. With rotors, they jump directly between pinned items, unread items, or any semantic group you define.

## Programmatic VoiceOver Focus

`@AccessibilityFocusState` moves VoiceOver's cursor programmatically — use after inserting new content, completing async operations, or showing alerts that VoiceOver should announce:

```swift
@AccessibilityFocusState private var focusedItem: Item.ID?

var body: some View {
    List(items) { item in
        ItemRow(item: item)
            .accessibilityFocused($focusedItem, equals: item.id)
    }
}

func addItem() {
    let newItem = Item(...)
    items.append(newItem)
    focusedItem = newItem.id  // VoiceOver jumps to the new item
}
```

Without this, VoiceOver stays wherever it was before the mutation — the user doesn't know something was added.

## Drag and Drop Accessibility

Drag-and-drop is invisible to VoiceOver by default. Provide accessibility actions as alternatives:

```swift
ForEach(items) { item in
    ItemRow(item: item)
        .draggable(item)
        .accessibilityAction(named: "Move Up") {
            moveItem(item, direction: .up)
        }
        .accessibilityAction(named: "Move Down") {
            moveItem(item, direction: .down)
        }
}
.dropDestination(for: Item.self) { items, location in
    // visual drag and drop handling
}
```

Custom accessibility actions appear in VoiceOver's actions menu (swipe up/down with VO). Every reorderable list must provide these.

## macOS Keyboard Navigation

- Every interactive element must be reachable via Tab key.
- Focus rings must be visible — don't suppress or hide them.
- `@FocusState` for managing keyboard focus programmatically.
- Test with Full Keyboard Access enabled in System Settings > Accessibility > Keyboard.
- Add `.keyboardShortcut()` for frequently used actions.
- Ensure custom controls respond to Enter/Space for activation.

## Testing

- **Accessibility Inspector** (Xcode → Open Developer Tool): inspect any element's label, traits, and value. Use the audit feature to catch missing labels and low-contrast text.
- **VoiceOver** (Cmd+F5): navigate the entire interface. Every interactive element should be reachable and clearly described. Test rotors and custom actions.
- **Full Keyboard Access** (System Settings → Accessibility → Keyboard): verify tab order is logical and all actions are triggerable via keyboard.
- **Increase Contrast** and **Reduce Motion**: verify the app respects these preferences. Check `accessibilityReduceMotion`, `accessibilityIncreaseContrast`, `accessibilityDifferentiateWithoutColor` environment values.


path: references/api.md
content: # Modern API

## Deprecated SwiftUI API

- `foregroundColor()` → `foregroundStyle()`.
- `cornerRadius()` → `clipShape(.rect(cornerRadius:))`.
- `tabItem()` → `Tab` type with `TabView`.
- `overlay(content, alignment:)` → `overlay(alignment:) { content }`.
- `.navigationBarLeading`/`.navigationBarTrailing` → `.topBarLeading`/`.topBarTrailing`.
- `NavigationView` → `NavigationStack` or `NavigationSplitView`.
- `NavigationLink(destination:)` → `navigationDestination(for:)`.
- `PreviewProvider` → `#Preview`.
- `onChange(of:perform:)` (1-parameter) → `onChange(of:) { old, new in }` or `onChange(of:) { }`.
- `GeometryReader` → `containerRelativeFrame()`, `visualEffect()`, or `Layout` protocol when possible. `GeometryReader` is still correct for reading sizes for custom layouts or coordinate space conversions.
- `animation(_:)` without value → `.animation(.bouncy, value: score)`.
- `showsIndicators: false` in ScrollView init → `.scrollIndicators(.hidden)`.
- `Text("A") + Text("B")` concatenation → `Text("\(styledA)\(styledB)")` via interpolation.

## Preferred Modern Patterns

- `@Entry` macro for `EnvironmentValues`, `FocusValues`, `Transaction`, `ContainerValues` keys. Replaces the manual `EnvironmentKey` + `defaultValue` + computed property extension pattern.
- Fill and stroke on shapes can be chained directly — no overlay needed (macOS 14+).
- `ForEach(items.enumerated(), id: \.element.id)` — do not convert to array first.
- `ContentUnavailableView` for empty states. `ContentUnavailableView.search` auto-includes the search term.
- `Label` over `HStack { Image(...); Text(...) }` for icon-text pairs.
- System hierarchical styles (`.secondary`, `.tertiary`) over manual `.opacity()`.
- `bold()` over `fontWeight(.bold)` — lets the system choose the correct weight for context.
- `Image(.assetName)` (generated symbols) over `Image("assetName")` when the project supports it.
- Static member lookup: `.circle` over `Circle()`, `.borderedProminent` over `BorderedProminentButtonStyle()`.
- In `Form`, wrap controls in `LabeledContent` for correct title/control layout.
- `RoundedRectangle` defaults to `.continuous` corner style — no need to specify.
- `@Previewable` macro for using `@State` directly inside `#Preview` without wrapper views: `#Preview { @Previewable @State var count = 0; Stepper("Count: \(count)", value: $count) }`.

## Swift Modernisms

- `FormatStyle` over `String(format:)`: `Text(value, format: .number.precision(.fractionLength(2)))`.
- `localizedStandardContains()` for user-input text filtering — not `contains()` or `localizedCaseInsensitiveContains()`.
- `Double` over `CGFloat` everywhere except optionals and `inout` parameters (bridging doesn't work there).
- Date formatting for display: use `"y"` not `"yyyy"` for correct localization. Prefer `FormatStyle` over manual format strings entirely.
- `Date(string, strategy: .iso8601)` over manual `DateFormatter` for parsing.
- `ObservableObject` requires explicit `import Combine` — no longer re-exported by SwiftUI.
- `URL.documentsDirectory` over `FileManager` directory lookups. `url.appending(path:)` to append.
- `replacing("a", with: "b")` over Foundation's `replacingOccurrences(of:with:)`.

## Apple's Open-Source Swift Packages

These are maintained by Apple's Swift team, follow Swift Evolution, and cover non-trivial algorithms and data structures that should not be reimplemented. Prefer them over hand-rolled alternatives.

### swift-collections

Efficient, well-tested data structures missing from the standard library.

| Type | Use instead of | Why |
|---|---|---|
| `OrderedDictionary` | `[Key: Value]` + separate `[Key]` for ordering | Maintains insertion order with O(1) key lookup. Eliminates the common pattern of keeping a dictionary and array in sync. |
| `OrderedSet` | `Array` with manual uniqueness checks or `Set` + order tracking | O(1) membership test with stable ordering. Correct `Hashable`, `Equatable`, `Codable`. |
| `Deque` | `Array` used as a queue (removing from front) | O(1) amortized prepend and append. `Array.removeFirst()` is O(n). Essential for FIFO queues, ring buffers, sliding windows. |
| `Heap` | Sorted array with manual insertion | O(log n) insert/remove-min. Use for priority queues, top-K tracking, scheduling. |
| `TreeDictionary` | `Dictionary` in persistent/functional patterns | Hash array mapped trie (HAMT). Efficient structural sharing for copy-on-write snapshots — useful for undo stacks or diffing state. |

```swift
// Maintaining unique recently-visited items in order:
var recentURLs = OrderedSet<URL>()
recentURLs.append(url)           // O(1), no-op if already present
recentURLs.move(url, to: 0)     // Move to front if exists

// Priority queue for download scheduling:
var pending = Heap<Download>()   // Downloads comparable by priority
pending.insert(download)
let next = pending.removeMin()
```

### swift-algorithms

Sequence and collection algorithms from the Swift Algorithms package. These cover operations that are error-prone to implement correctly or are repetitive boilerplate.

| Algorithm | Use instead of | Example |
|---|---|---|
| `uniqued(on:)` | Manual `Set`-based dedup loop | `items.uniqued(on: \.id)` |
| `chunks(ofCount:)` | Manual stride-based slicing | `data.chunks(ofCount: 50)` for batch processing |
| `windows(ofCount:)` | Manual index arithmetic for sliding windows | `values.windows(ofCount: 3)` for moving averages |
| `adjacentPairs()` | `zip(array, array.dropFirst())` | `path.adjacentPairs()` for edge iteration |
| `compacted()` | `.compactMap { $0 }` | `optionals.compacted()` — clearer intent |
| `indexed()` | `.enumerated()` when you need true collection indices | `items.indexed()` for index-based access |
| `partitioned(by:)` | Two separate `filter` passes | `items.partitioned(by: \.isActive)` — single pass |
| `minAndMax()` | Separate `.min()` and `.max()` calls | Single-pass min/max extraction |

### swift-async-algorithms

Async sequence operators for combining, transforming, and timing async streams. These handle edge cases (backpressure, cancellation, buffering) that hand-rolled versions typically get wrong.

| Algorithm | Use instead of | When to use |
|---|---|---|
| `merge(_:_:)` | Manual `TaskGroup` + `AsyncStream` fan-in | Interleaving events from multiple independent sources (e.g., keyboard events + mouse events + notification stream) |
| `combineLatest(_:_:)` | Multiple `Observations {}` streams combined manually | Reacting to the latest value from two+ streams simultaneously (e.g., filter text + sort order) |
| `debounce(for:)` | Manual `Task.sleep` + cancellation | Search-as-you-type, window resize handlers, any rapid-fire event that should coalesce |
| `throttle(for:)` | Manual timestamp tracking | Rate-limiting updates (e.g., progress reporting, scroll position syncing) |
| `chain(_:_:)` | Wrapping sequences in `AsyncStream` to concatenate | Playing async sequences in order (e.g., cached results then network results) |
| `zip(_:_:)` | Manual coordination with continuations | Pairing elements from two streams 1:1 |

```swift
// Debounce search input from an async stream of keystrokes:
for await query in searchTerms.debounce(for: .milliseconds(300)) {
    await performSearch(query)
}

// Merge independent event sources:
for await event in merge(keyboardEvents, mouseEvents, systemNotifications) {
    handleEvent(event)
}

// React to latest from two streams:
for await (filter, sortOrder) in combineLatest(filterChanges, sortChanges) {
    updateResults(filter: filter, sort: sortOrder)
}
```

### When to add these packages

- **Do not reimplement** `OrderedSet`, `OrderedDictionary`, `Deque`, `Heap`, or any async sequence combinator. The correct implementations handle edge cases (hash collision strategies, COW optimization, cancellation propagation, backpressure) that ad-hoc versions miss.
- **Add the package** when the first use case arises — don't preemptively add all three. Each is an independent SPM dependency.
- These are **not third-party dependencies** in the usual sense — they're maintained by the Swift project under the `apple/` GitHub organization, follow Swift Evolution, and are considered the staging ground for eventual standard library inclusion.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)