Claude Skill

swift-macos

Covers macOS app development with Swift 6.3, SwiftUI, SwiftData, Swift Concurrency, Foundation Models, Swift Testing, ScreenCaptureKit, and app distribution. Use when building native Mac apps - windows, scenes, navigation, menus and toolbars, SwiftData models and queries, modern

LLM Mart · 0 points · 26 views 11 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download tenequm-skills-skills_swift-macos-1ff2284.zip · 147 KB
Part of tenequm/skills — 25 skills

Install

skills CLI npx skills add https://github.com/tenequm/skills/tree/main/skills/swift-macos
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tenequm-skills@llmmart
Git git clone https://github.com/tenequm/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tenequm/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

macOS App Development - Swift 6.3

Build native macOS apps with Swift 6.3 (latest: 6.3.3, bundled in Xcode 26.6, Jun 2026), SwiftUI, SwiftData, and macOS 26 Tahoe (26.6.2 current). Target macOS 14+ for SwiftData/@Observable, macOS 15+ for latest SwiftUI, macOS 26 for Liquid Glass and Foundation Models. Note Xcode 26.6 still bundles the macOS 26.5 SDK, so 26.6-only API is not yet buildable. For the WWDC 2026 beta stack (macOS 27, Xcode 27, Swift 6.4, shipping fall 2026), see references/fall-2026-releases.md.

Quick Start

import SwiftUI
import SwiftData

@Model
final class Project {
    var name: String
    var createdAt: Date
    // Named ProjectTask, not Task - `Task` would shadow `Swift.Task`
    @Relationship(deleteRule: .cascade) var tasks: [ProjectTask] = []

    init(name: String) {
        self.name = name
        self.createdAt = .now
    }
}

@Model
final class ProjectTask {
    var title: String
    var isComplete: Bool
    var project: Project?

    init(title: String) {
        self.title = title
        self.isComplete = false
    }
}

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup("Projects") {
            ContentView()
        }
        .modelContainer(for: [Project.self, ProjectTask.self])
        .defaultSize(width: 900, height: 600)

        #if os(macOS)
        Settings { SettingsView() }

        MenuBarExtra("Status", systemImage: "circle.fill") {
            MenuBarView()
        }
        .menuBarExtraStyle(.window)
        #endif
    }
}

struct ContentView: View {
    @Query(sort: \Project.createdAt, order: .reverse)
    private var projects: [Project]

    @Environment(\.modelContext) private var context
    @State private var selected: Project?

    var body: some View {
        NavigationSplitView {
            List(projects, selection: $selected) { project in
                NavigationLink(value: project) {
                    Text(project.name)
                }
            }
            .navigationSplitViewColumnWidth(min: 200, ideal: 250)
        } detail: {
            if let selected {
                DetailView(project: selected)
            } else {
                ContentUnavailableView("Select a Project",
                    systemImage: "sidebar.left")
            }
        }
    }
}

Scenes & Windows

Scene Purpose
WindowGroup Resizable windows (multiple instances)
Window Single-instance utility window
Settings Preferences (Cmd+,)
MenuBarExtra Menu bar with .menu or .window style
DocumentGroup Document-based apps

Open windows: @Environment(\.openWindow) var openWindow; openWindow(id: "about")

For complete scene lifecycle, see references/app-lifecycle.md.

Menus & Commands

.commands {
    CommandGroup(replacing: .newItem) {
        Button("New Project") { /* ... */ }
            .keyboardShortcut("n", modifiers: .command)
    }
    CommandMenu("Tools") {
        Button("Run Analysis") { /* ... */ }
            .keyboardShortcut("r", modifiers: [.command, .shift])
    }
}

Table (macOS-native)

Table(items, selection: $selectedIDs, sortOrder: $sortOrder) {
    TableColumn("Name", value: \.name)
    TableColumn("Date") { Text($0.date, format: .dateTime) }
        .width(min: 100, ideal: 150)
}
.contextMenu(forSelectionType: Item.ID.self) { ids in
    Button("Delete", role: .destructive) { delete(ids) }
}

For forms, popovers, sheets, inspector, and macOS modifiers, see references/swiftui-macos.md.

@Observable

@Observable
final class AppState {
    var projects: [Project] = []
    var isLoading = false

    func load() async throws {
        isLoading = true
        defer { isLoading = false }
        projects = try await ProjectService.fetchAll()
    }
}

// Use: @State var state = AppState()          (owner)
// Pass: .environment(state)                    (inject)
// Read: @Environment(AppState.self) var state  (child)

SwiftData

@Query & #Predicate

@Query(filter: #Predicate<Project> { !$0.isArchived }, sort: \Project.name)
private var active: [Project]

// Dynamic predicate
func search(_ term: String) -> Predicate<Project> {
    #Predicate { $0.name.localizedStandardContains(term) }
}

// FetchDescriptor (outside views)
var desc = FetchDescriptor<Project>(predicate: #Predicate { $0.isArchived })
desc.fetchLimit = 50
let results = try context.fetch(desc)
let count = try context.fetchCount(desc)

Relationships

@Model final class Author {
    var name: String
    @Relationship(deleteRule: .cascade, inverse: \Book.author)
    var books: [Book] = []

    init(name: String) { self.name = name }
}

@Model final class Book {
    var title: String
    var author: Author?
    @Relationship var tags: [Tag] = []  // many-to-many

    init(title: String) { self.title = title }
}

Delete rules: .cascade, .nullify (default), .deny, .noAction.

Every @Model class needs an explicit initializer - the macro does not synthesize one, and omitting it fails with @Model requires an initializer be provided for '<Type>'.

Schema Migration

enum SchemaV1: VersionedSchema { /* ... */ }
enum SchemaV2: VersionedSchema { /* ... */ }

enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] { [SchemaV1.self, SchemaV2.self] }
    static var stages: [MigrationStage] {
        [.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)]
    }
}

// Apply: .modelContainer(for: Model.self, migrationPlan: MigrationPlan.self)

CloudKit Sync

Enable iCloud capability, then .modelContainer(for: Model.self) auto-syncs. Constraints: all properties need defaults/optional, no unique constraints, optional relationships.

For model attributes, background contexts, batch ops, undo/redo, and testing, see SwiftData references below.

Concurrency (Swift 6.2+)

Default MainActor Isolation

Opt entire module into main actor - all code runs on main actor by default:

// Package.swift
.executableTarget(name: "MyApp", swiftSettings: [
    .defaultIsolation(MainActor.self),
])

Or Xcode: Build Settings > Swift Compiler > Default Isolation > MainActor.

@concurrent

Mark functions for background execution:

@concurrent
func processFile(_ url: URL) async throws -> Data {
    let data = try Data(contentsOf: url)
    return try compress(data) // runs off main actor
}
// After await, automatically back on main actor
let result = try await processFile(fileURL)

Use for CPU-intensive work, I/O, anything not touching UI.

Actors

actor DocumentStore {
    private var docs: [UUID: Document] = [:]
    func add(_ doc: Document) { docs[doc.id] = doc }
    func get(_ id: UUID) -> Document? { docs[id] }
    nonisolated let name: String
}
// Requires await: let doc = await store.get(id)

Structured Concurrency

// Parallel with async let
func loadDashboard() async throws -> Dashboard {
    async let profile = fetchProfile()
    async let stats = fetchStats()
    return try await Dashboard(profile: profile, stats: stats)
}

// Dynamic with TaskGroup
func processImages(_ urls: [URL]) async throws -> [NSImage] {
    try await withThrowingTaskGroup(of: (Int, NSImage).self) { group in
        for (i, url) in urls.enumerated() {
            group.addTask { (i, try await loadImage(url)) }
        }
        var results = [(Int, NSImage)]()
        for try await r in group { results.append(r) }
        return results.sorted { $0.0 < $1.0 }.map(\.1)
    }
}

Sendable

struct Point: Sendable { var x, y: Double }              // value types: implicit
final class Config: Sendable { let apiURL: URL }          // final + immutable
actor SharedState { var count = 0 }                       // mutable: use actors
// Enable strict mode: .swiftLanguageMode(.v6) in Package.swift

AsyncSequence & Observations

// Stream @Observable changes (macOS 26+ / iOS 26+, SE-0475)
// Observations uses a closure init, not Observations(of:).
let progresses = Observations { manager.progress }
for await p in progresses { print(p) }

// Typed NotificationCenter (macOS 26+)
struct DocSaved: NotificationCenter.MainActorMessage {
    typealias Subject = Document
    static var name: Notification.Name { .init("DocSaved") }
    let id: UUID
}
NotificationCenter.default.post(DocSaved(id: document.id), subject: document)
let token = NotificationCenter.default.addObserver(of: document, for: DocSaved.self) { msg in
    refresh(msg.id)
}

For concurrency deep dives, see concurrency references below.

Foundation Models (macOS 26+)

On-device ~3B LLM. Free, offline, private:

import FoundationModels

let session = LanguageModelSession()
let response = try await session.respond(to: "Summarize: \(text)")
print(response.content)  // respond() returns Response<Content>, not Content

// Structured output
@Generable struct Summary { var title: String; var points: [String] }
let result = try await session.respond(to: prompt, generating: Summary.self)
let summary: Summary = result.content

For tool calling, streaming, and sessions, see references/foundation-models.md.

Testing

import Testing

@Suite("Project Tests")
struct ProjectTests {
    @Test("creates with defaults")
    func create() {
        let p = Project(name: "Test")
        #expect(p.name == "Test")
    }

    @Test("formats sizes", arguments: [(1024, "1 KB"), (0, "0 KB")])
    func format(bytes: Int, expected: String) {
        #expect(formatSize(bytes) == expected)
    }
}

// SwiftData testing
let container = try ModelContainer(
    for: Project.self,
    configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let ctx = ModelContext(container)
ctx.insert(Project(name: "Test"))
try ctx.save()

For exit tests, attachments, UI testing, see references/testing.md.

Distribution

Method Sandbox Notarization Review
App Store Required Automatic Yes
Developer ID Recommended Required No
Ad-Hoc No No Local only
xcodebuild archive -scheme MyApp -archivePath MyApp.xcarchive
xcodebuild -exportArchive -archivePath MyApp.xcarchive \
  -exportPath ./export -exportOptionsPlist ExportOptions.plist
xcrun notarytool submit ./export/MyApp.dmg \
  --apple-id you@example.com --team-id TEAM_ID \
  --password @keychain:AC_PASSWORD --wait
xcrun stapler staple ./export/MyApp.dmg

For complete distribution guide, see references/distribution.md.

SPM

// swift-tools-version: 6.3
let package = Package(
    name: "MyApp",
    platforms: [.macOS(.v14)],
    targets: [
        .executableTarget(name: "MyApp", swiftSettings: [
            .swiftLanguageMode(.v6),
            .defaultIsolation(MainActor.self),
        ]),
        .testTarget(name: "MyAppTests", dependencies: ["MyApp"]),
    ]
)

For build plugins, macros, and Swift Build, see references/spm-build.md.

Liquid Glass (macOS 26)

Apps rebuilt with Xcode 26 SDK get automatic Liquid Glass styling. Use .glassEffect() for custom glass surfaces, GlassEffectContainer for custom hierarchies. Opt out (Xcode 26 only): UIDesignRequiresCompatibility = YES in Info.plist keeps the legacy visual style - a temporary migration aid. Apps rebuilt with Xcode 27 (beta) can no longer opt out; the key is ignored and Liquid Glass is mandatory (see references/fall-2026-releases.md).

ScreenCaptureKit

Capture screen content, app audio, and microphone (macOS 12.3+):

import ScreenCaptureKit

let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
guard let display = content.displays.first else { return }

// Filter: specific apps only
let filter = SCContentFilter(display: display, including: [targetApp], exceptingWindows: [])

// Configure
let config = SCStreamConfiguration()
config.capturesAudio = true
config.sampleRate = 48000
config.channelCount = 2
config.excludesCurrentProcessAudio = true

// Audio-only: throttle the video pipeline (it always runs). minimumFrameInterval is a
// MINIMUM gap between frames - 1/Int32.max is ~0s, i.e. native refresh rate (60+ fps
// of discarded frames and a full-screen recomposite each). Use 1 fps.
config.width = 2; config.height = 2
config.minimumFrameInterval = CMTime(value: 1, timescale: 1)

let stream = SCStream(filter: filter, configuration: config, delegate: self)
try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: nil)
try stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: audioQueue)
try await stream.startCapture()

macOS 15+: SCRecordingOutput for simplified file recording, config.captureMicrophone for mic capture. macOS 14+: SCContentSharingPicker for system picker UI, SCScreenshotManager for single-frame capture.

For complete API reference, audio writing (AVAssetWriter/AVAudioFile), permissions, and examples, see references/screen-capture-audio.md.

AppKit Interop

struct WebViewWrapper: NSViewRepresentable {
    let url: URL
    func makeNSView(context: Context) -> WKWebView { WKWebView() }
    func updateNSView(_ v: WKWebView, context: Context) {
        v.load(URLRequest(url: url))
    }
}

For hosting SwiftUI in AppKit and advanced bridging, see references/appkit-interop.md.

Architecture

Pattern Best For Complexity
SwiftUI + @Observable Small-medium, solo Low
MVVM + @Observable Medium, teams Medium
TCA Large, strict testing High

See references/architecture.md for all patterns with examples.

References

File When to read
references/fall-2026-releases.md WWDC 2026 beta stack: macOS 27, Xcode 27, Swift 6.4, Foundation Models next-gen, Core AI, Spatial Preview, mandatory Liquid Glass
SwiftUI & macOS
references/app-lifecycle.md Window management, scenes, DocumentGroup, MenuBarExtra gotchas, async termination, LSUIElement issues
references/swiftui-macos.md Sidebar, Inspector, Table, forms, popovers, sheets, search
references/appkit-interop.md NSViewRepresentable, hosting controllers, NSHostingSceneRepresentation, sizing/scene-bridging options, AppKit Liquid Glass (NSGlassEffectView), pasteboard privacy, NSPanel/floating HUD
references/screen-capture-audio.md ScreenCaptureKit, SCStream gotchas, SCStream teardown hazards, AVAudioEngine dual pipeline, AVAssetWriter crash safety, non-interleaved stereo trap, TCC gotchas, CDHash degraded-state after reinstall, SpeechAnalyzer transcription
references/core-audio-tap.md CATap for per-process audio: tap-only aggregate (HFP-safe), drift compensation, rate-change anti-pattern, interleaved-stereo frame-count trap, IO proc isolation
references/system-integration.md Keyboard shortcuts, drag & drop, file access, App Intents (entities, Spotlight, snippets), widgets & Control Center, process monitoring, AXUIElement, CoreAudio per-process APIs, login items, XPC, LSUIElement, os.Logger & signposts, privacy usage descriptions
references/foundation-models.md On-device AI: guided generation, tool calling, streaming
references/architecture.md MVVM, TCA, dependency injection, project structure
references/testing.md Swift Testing, exit tests, attachments, UI testing, XCTest migration
references/distribution.md App Store, Developer ID, notarization gotchas, nested bundle signing, xcodebuild signing traps (silent ad-hoc builds), sandboxing, universal binaries
references/spm-build.md Package.swift, Swift Build, plugins, macros, plugin/macro validation gates, Metal toolchain download, manual .app bundle assembly, mixed ObjC targets, CLT testing
Concurrency
references/approachable-concurrency.md Default MainActor isolation, @concurrent, nonisolated async, runtime pitfalls
references/actors-isolation.md Actor model, global actors, custom executors, reentrancy
references/structured-concurrency.md Task, TaskGroup, async let, cancellation, priority, named tasks
references/sendable-safety.md Sendable protocol, data race safety, @unchecked Sendable + serial queue, @preconcurrency import
references/async-patterns.md AsyncSequence, AsyncStream, Observations, continuations, Clock
references/migration-guide.md GCD to async/await, Combine to AsyncSequence, Swift 6 migration
SwiftData
references/models-schema.md @Model, @Attribute options, Codable, transformable, external storage
references/relationships-predicates.md Advanced relationships, inverse rules, compound predicates
references/container-context.md ModelContainer, ModelContext, background contexts, undo/redo, batch ops
references/cloudkit-sync.md CloudKit setup, conflict resolution, sharing, debugging sync
references/migrations.md VersionedSchema, lightweight/custom migration, Core Data migration
Files (skills)
  • references
    • actors-isolation.md 17.1 KB
      # Actors & Isolation
      
      ## Table of Contents
      - Actor Basics
      - Global Actors
      - @MainActor
      - Custom Global Actors
      - Nonisolated
      - Actor Reentrancy
      - Custom Executors (DispatchSerialQueue pattern)
      - `assumeIsolated` Recipes
      - Reentrancy & Ordering Hazards
      - `isolated deinit` (SE-0371)
      
      ## Actor Basics
      
      Actors protect mutable state from data races via serialized access:
      
      ```swift
      actor BankAccount {
          let id: UUID
          private(set) var balance: Decimal
      
          init(id: UUID, initialBalance: Decimal) {
              self.id = id
              self.balance = initialBalance
          }
      
          func deposit(_ amount: Decimal) {
              precondition(amount > 0)
              balance += amount
          }
      
          func withdraw(_ amount: Decimal) throws {
              guard balance >= amount else {
                  throw BankError.insufficientFunds
              }
              balance -= amount
          }
      
          // Cross-actor operations
          func transfer(amount: Decimal, to other: BankAccount) async throws {
              try withdraw(amount)
              await other.deposit(amount)
          }
      }
      
      // All access from outside requires await
      let account = BankAccount(id: UUID(), initialBalance: 1000)
      await account.deposit(500)
      let balance = await account.balance
      ```
      
      ### Actor properties
      - Actor-isolated properties/methods require `await` from outside
      - `let` properties are `nonisolated` by default (immutable = safe)
      - Actors are reference types (like classes)
      - Actors implicitly conform to `Sendable`
      
      ## Global Actors
      
      Annotate types/functions to isolate them to a shared actor:
      
      ```swift
      @MainActor
      class ViewModel {
          var items: [Item] = [] // protected by MainActor
      
          func refresh() async throws {
              let data = try await api.fetch() // suspends, but resumes on MainActor
              items = data
          }
      }
      ```
      
      ### @MainActor specifics
      ```swift
      // On a function
      @MainActor
      func updateUI() {
          // Guaranteed to run on main thread
      }
      
      // On a property
      @MainActor var currentTitle: String = ""
      
      // On a closure
      let callback: @MainActor () -> Void = {
          // Runs on MainActor
      }
      
      // Opt out within MainActor type
      @MainActor
      class ViewModel {
          nonisolated var description: String {
              "ViewModel" // No actor isolation needed
          }
      
          @concurrent
          func heavyComputation() async -> Data {
              // Runs off MainActor
          }
      }
      ```
      
      ## Custom Global Actors
      
      ```swift
      @globalActor
      actor DatabaseActor {
          static let shared = DatabaseActor()
      }
      
      @DatabaseActor
      class DatabaseManager {
          private var connection: Connection?
      
          func query(_ sql: String) throws -> [Row] {
              guard let conn = connection else { throw DBError.notConnected }
              return try conn.execute(sql)
          }
      }
      
      // Usage
      @DatabaseActor
      func fetchUsers() throws -> [User] {
          let rows = try DatabaseManager.shared.query("SELECT * FROM users")
          return rows.map(User.init)
      }
      ```
      
      ## Nonisolated
      
      Opt specific members out of actor isolation:
      
      ```swift
      actor Cache {
          let name: String // implicitly nonisolated (let)
      
          nonisolated var debugDescription: String {
              "Cache(\(name))" // OK - only accesses nonisolated data
          }
      
          nonisolated func hash(into hasher: inout Hasher) {
              hasher.combine(name)
          }
      
          private var store: [String: Data] = []
      
          func get(_ key: String) -> Data? {
              store[key]
          }
      }
      ```
      
      ### nonisolated(unsafe)
      Escape hatch for when you know something is safe but compiler disagrees:
      
      ```swift
      // Use sparingly - bypasses safety checks
      nonisolated(unsafe) var legacyCallback: (() -> Void)?
      ```
      
      ## Actor Reentrancy
      
      Actors don't prevent reentrancy - state can change across await points:
      
      ```swift
      actor ImageLoader {
          private var cache: [URL: NSImage] = [:]
      
          func load(_ url: URL) async throws -> NSImage {
              // Check cache
              if let cached = cache[url] {
                  return cached
              }
      
              // DANGER: Another call to load() can execute here during await
              let image = try await downloadImage(url)
      
              // State may have changed! Check again.
              if let cached = cache[url] {
                  return cached // Another task already loaded it
              }
      
              cache[url] = image
              return image
          }
      }
      ```
      
      **Rule**: Never assume state is unchanged after an `await` inside an actor.
      
      ## Custom Executors
      
      Most apps don't need custom executors. The default executor (cooperative thread pool for actors, main thread for `@MainActor`) works well. The one pattern worth knowing is using a `DispatchSerialQueue` as an actor's serial executor - the replacement for `class X: @unchecked Sendable` + `nonisolated(unsafe) var` bookkeeping that Apple's own AVCam sample uses for audio/video capture.
      
      ### `DispatchSerialQueue` as an actor's serial executor
      
      Before - a class managing its own serial queue with roughly 28 `nonisolated(unsafe)` declarations and `@unchecked Sendable`:
      
      ```swift
      final class AudioRecorder: NSObject, @unchecked Sendable, SCStreamOutput {
          private let audioQueue = DispatchQueue(label: "com.app.audio")
          nonisolated(unsafe) private var writer: AVAssetWriter?
          nonisolated(unsafe) private var audioInput: AVAssetWriterInput?
          nonisolated(unsafe) private var sessionStarted = false
          // ... and 20+ more nonisolated(unsafe) vars
      }
      ```
      
      After - an actor whose serial executor *is* the `DispatchSerialQueue`. No `@unchecked Sendable`, no `nonisolated(unsafe)` bookkeeping. Every `func` on the actor is serialized on the audio queue.
      
      `SCStreamOutput` inherits `NSObjectProtocol`, so actors can't conform directly. The idiomatic workaround is a small NSObject adapter that forwards the callback into the actor via `assumeIsolated` (safe because the callback runs on `audioQueue`, which IS the actor's executor):
      
      ```swift
      actor AudioRecorder {
          // The backing queue. Declared DispatchSerialQueue (not DispatchQueue) so that
          // asUnownedSerialExecutor() is available.
          private let audioQueue = DispatchSerialQueue(label: "com.app.audio")
      
          // Tell the runtime: run this actor's isolated code on audioQueue, not the
          // cooperative pool.
          nonisolated var unownedExecutor: UnownedSerialExecutor {
              audioQueue.asUnownedSerialExecutor()
          }
      
          // State. Ordinary actor-isolated properties, no nonisolated(unsafe).
          private var writer: AVAssetWriter?
          private var audioInput: AVAssetWriterInput?
          private var sessionStarted = false
      
          func start(url: URL) throws { /* ... */ }
          func stop() async { /* ... */ }
      
          // SCStream adapter. Pass audioQueue as the sampleHandlerQueue so the callback
          // runs on the actor's executor and assumeIsolated is safe.
          final class StreamOutput: NSObject, SCStreamOutput {
              unowned let recorder: AudioRecorder
              init(_ recorder: AudioRecorder) { self.recorder = recorder }
              func stream(_ stream: SCStream,
                          didOutputSampleBuffer sb: CMSampleBuffer,
                          of type: SCStreamOutputType) {
                  recorder.assumeIsolated { iso in
                      iso.handleSampleBuffer(sb, type: type)
                  }
              }
          }
      
          nonisolated func makeStreamOutput() -> SCStreamOutput { StreamOutput(self) }
      
          private func handleSampleBuffer(_ sb: CMSampleBuffer, type: SCStreamOutputType) {
              // Actor-isolated, runs on audioQueue. Touches writer / audioInput directly.
          }
      }
      ```
      
      **Why not just use an actor with the default executor?** Real-time audio callbacks (CoreAudio IO procs, SCStream sample handlers) must deliver on a specific dispatch queue to meet timing. The default cooperative pool cannot guarantee that. Custom executor ties the actor's isolation to the queue the callbacks already run on, so you get data-race safety *and* preserved timing, with zero bridging code.
      
      **IO procs stay `nonisolated`.** CoreAudio's real-time IO proc must not allocate, must not call `Task {}`, must not yield to `AsyncStream`. Keep the IO-proc closure outside the actor (or as a `nonisolated` method returning an IO block), `memcpy` into a staging buffer, dispatch to `audioQueue`, and `assumeIsolated` on the other side.
      
      ## `assumeIsolated` Recipes
      
      `assumeIsolated` lets a `nonisolated` function synchronously access actor state *if* it can prove it's already running on the actor's executor. With a custom `DispatchSerialQueue` executor, "already on the queue" = "already isolated to the actor."
      
      Three correct recipes, one wrong one.
      
      ### Recipe 1: CoreAudio listener registered on the actor's queue
      
      ```swift
      extension AudioRecorder {
          nonisolated func installRateListener(on deviceID: AudioObjectID) {
              let listener: AudioObjectPropertyListenerBlock = { [weak self] _, _ in
                  // Block runs on audioQueue because we pass it below.
                  self?.assumeIsolated { iso in
                      iso.handleRateChange()   // actor-isolated, zero await
                  }
              }
              var addr = AudioObjectPropertyAddress(
                  mSelector: kAudioDevicePropertyNominalSampleRate,
                  mScope: kAudioObjectPropertyScopeGlobal,
                  mElement: kAudioObjectPropertyElementMain
              )
              AudioObjectAddPropertyListenerBlock(deviceID, &addr, audioQueue, listener)
          }
      }
      ```
      
      ### Recipe 2: Block dispatched to the actor's queue from outside
      
      ```swift
      someBackgroundWork { result in
          self.audioQueue.async {
              self.assumeIsolated { iso in
                  iso.writeFrames(result)
              }
          }
      }
      ```
      
      ### Recipe 3: Real-time IO proc - do NOT assumeIsolated on the RT thread
      
      ```swift
      nonisolated func makeIOProc() -> AudioDeviceIOBlock {
          return { [weak self] _, inputData, _, _, _ in
              guard let self else { return }
              // RT thread. No allocation, no Task, no AsyncStream, no assumeIsolated.
              let frames = stageIntoRingBuffer(inputData)
              // Hop to audioQueue for everything else.
              self.audioQueue.async {
                  self.assumeIsolated { iso in iso.writeFrames(frames) }
              }
          }
      }
      ```
      
      ## Reentrancy & Ordering Hazards
      
      ### Don't re-dispatch when the block already runs on the actor's executor
      
      When a CoreAudio property listener is registered with the actor's `audioQueue`, the block fires *on that queue* - so the block is already isolated to the actor. Wrapping its body in an extra `audioQueue.async { assumeIsolated { ... } }` is not just redundant; it creates an **ordering bug**.
      
      Consider: `AudioObjectAddPropertyListenerBlock(deviceID, &rateAddr, audioQueue, listener)`. An `AudioObjectPropertyListenerBlock` rate-change fires on `audioQueue`. The queue already has IO-proc buffers ahead of it. If the listener does:
      
      ```swift
      // WRONG: re-dispatches rate change behind already-queued IO-proc work.
      let listener: AudioObjectPropertyListenerBlock = { [weak self] _, _ in
          self?.audioQueue.async {
              self?.assumeIsolated { iso in iso.handleRateChange() }
          }
      }
      ```
      
      ...then `handleRateChange` runs *after* the IO-proc buffers queued before it. Those buffers read the stale `tapFormat` for one cycle, producing pitch/length corruption.
      
      Correct pattern: call `assumeIsolated` directly, no inner `async`:
      
      ```swift
      let listener: AudioObjectPropertyListenerBlock = { [weak self] _, _ in
          self?.assumeIsolated { iso in iso.handleRateChange() }
      }
      ```
      
      ### State can change across every `await`
      
      Standard actor reentrancy rule - restated because it bites concrete patterns:
      
      ```swift
      actor ImageLoader {
          private var cache: [URL: NSImage] = [:]
      
          func load(_ url: URL) async throws -> NSImage {
              if let cached = cache[url] { return cached }
              let image = try await downloadImage(url)
              // Another load() may have populated cache during the await.
              if let cached = cache[url] { return cached }
              cache[url] = image
              return image
          }
      }
      ```
      
      ### Don't assign the reference before awaiting a suspension that can race `stop()`
      
      ```swift
      // FRAGILE: assignment happens after the await. A concurrent stop() during
      // startCapture's suspension calls stopCapture() on a nil stream; cleanup
      // silently no-ops, and the SCStream that eventually starts is orphaned.
      func start() async throws {
          let s = SCStream(...)
          try await s.startCapture()    // <-- suspension
          self.stream = s
      }
      
      // ROBUST: assignment first, so stop() during the suspension drives cleanup.
      func start() async throws {
          let s = SCStream(...)
          self.stream = s
          try await s.startCapture()
      }
      ```
      
      SCStream tolerates `stopCapture()` on an unstarted stream; not all APIs do. Test the "stop during start" path explicitly.
      
      ## `isolated deinit` (SE-0371, Swift 6.2+)
      
      `deinit` is `nonisolated` by default — it cannot touch actor-isolated state. SE-0371 (shipped in Swift 6.2) lets actors and global-actor-isolated classes mark `deinit` as `isolated`; the runtime hops to the relevant executor (including a custom `unownedExecutor` like `DispatchSerialQueue`) before running the body, so cleanup can access isolated properties directly. No `nonisolated(unsafe)` mirrors needed for observer tokens, listener IDs, or `beginActivity` handles.
      
      Caveat: task-local values set outside are **not** visible inside an isolated deinit — SE-0371 clears them on entry. Escaping `self` from an isolated deinit still traps. Source: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0371-isolated-synchronous-deinit.md
      
      ```swift
      actor AudioRecorder {
          private let audioQueue = DispatchSerialQueue(label: "audio")
          nonisolated var unownedExecutor: UnownedSerialExecutor {
              audioQueue.asUnownedSerialExecutor()
          }
      
          // Cached at start() time so deinit can tear them down.
          private var listenerIDs: Set<AudioObjectID> = []
          private let rateListener: AudioObjectPropertyListenerBlock
          private var observerTokens: [NSObjectProtocol] = []
          private var activityToken: NSObjectProtocol?
      
          init() {
              self.rateListener = { _, _ in /* handle rate change */ }
          }
      
          private func rateAddr() -> AudioObjectPropertyAddress {
              AudioObjectPropertyAddress(
                  mSelector: kAudioDevicePropertyNominalSampleRate,
                  mScope: kAudioObjectPropertyScopeGlobal,
                  mElement: kAudioObjectPropertyElementMain
              )
          }
      
          isolated deinit {
              // Runs on audioQueue, can touch actor-isolated state directly.
              var addr = rateAddr()
              for id in listenerIDs {
                  AudioObjectRemovePropertyListenerBlock(id, &addr, audioQueue, rateListener)
              }
              observerTokens.forEach { NotificationCenter.default.removeObserver($0) }
              if let t = activityToken { ProcessInfo.processInfo.endActivity(t) }
          }
      }
      ```
      
      The "before" equivalent required `nonisolated(unsafe) var` copies of every piece of cleanup state, kept in sync with the actor-isolated originals.
      
      ## Reentrancy: a `Bool` flag does not make async start/stop safe
      
      The classic shape - an actor with `start()` and `stop()` guarded by a `stopped` flag - is unsound the moment `start()` contains an `await`. Actors are reentrant: `stop()` runs *during* the suspension, flips the flag, and the resumed `start()` walks past it and finishes building a live object that no caller holds a reference to.
      
      Observed failure: a `start()` suspended for ~12 s inside a slow `SCShareableContent` query. `stop()` arrived mid-suspension, set `stopped = true`, and returned. `start()` resumed, checked nothing further, and built a running recorder. It stayed alive for hours dropping every buffer at a `guard !stopped` in the output callback, never finalized the writer, and left a zero-byte file plus a UI stuck in "recording".
      
      ```swift
      // WRONG - one flag read, checked before the await that matters
      actor Recorder {
          private var stopped = false
          func start() async throws {
              guard !stopped else { return }
              let content = try await SCShareableContent.current   // stop() lands here
              stream = try buildStream(content)                     // built anyway
              try await stream?.startCapture()
          }
      }
      ```
      
      Two rules fix it:
      
      **Re-check cancellation after every await, not once at the top.**
      
      ```swift
      actor Recorder {
          private enum State { case idle, starting, running, stopping }
          private var state: State = .idle
          private var stream: SCStream?
      
          func start() async throws {
              guard state == .idle else { throw RecorderError.alreadyRunning }
              state = .starting
      
              let content = try await SCShareableContent.current
              guard state == .starting else {            // re-check
                  try await cleanupPartialStart()
                  throw RecorderError.cancelled
              }
      
              let built = try buildStream(content)
              guard state == .starting else {            // re-check again
                  try? await built.stopCapture()
                  throw RecorderError.cancelled
              }
      
              stream = built
              try await built.startCapture()
              state = .running
          }
      }
      ```
      
      **Make cleanup idempotent by taking ownership before awaiting.** Copy the resource to a local and nil the actor field *before* any suspension, so an interleaved `stop()` cannot double-free or act on a half-torn-down object:
      
      ```swift
      func stop() async {
          guard let stream else { state = .idle; return }
          self.stream = nil          // take ownership BEFORE the await
          state = .stopping
          try? await stream.stopCapture()
          await finalizeWriter()
          state = .idle
      }
      ```
      
      Distinct error cases (`.cancelled` vs `.alreadyRunning`) are worth the extra enum - they let tests assert the exact interleaving outcome instead of just "it threw". Delete partial artifacts on a failed start; a zero-byte output file is worse than no file.
      
    • app-lifecycle.md 16.2 KB
      # App Lifecycle & Scenes
      
      ## Table of Contents
      - App Protocol & Entry Point
      - WindowGroup
      - Window (Single Instance)
      - Settings Scene
      - MenuBarExtra
      - MenuBarExtra Gotchas
      - DocumentGroup
      - Window Management
      - Scene Phases
      - Async Termination Cleanup
      - LSUIElement Operational Issues
      
      ## App Protocol & Entry Point
      
      ```swift
      @main
      struct MyApp: App {
          // App-level state
          @State private var appState = AppState()
      
          // App delegate for system events
          @NSApplicationDelegateAdaptor private var delegate: AppDelegate
      
          var body: some Scene {
              WindowGroup("Projects", id: "projects") {
                  ContentView()
                      .environment(appState)
              }
              .defaultSize(width: 1000, height: 700)
              .defaultPosition(.center)
              .keyboardShortcut("1", modifiers: .command)
      
              Window("Activity", id: "activity") {
                  ActivityView()
              }
              .defaultSize(width: 400, height: 600)
              .windowResizability(.contentMinSize)
      
              Settings {
                  SettingsView()
                      .environment(appState)
              }
      
              MenuBarExtra("MyApp", systemImage: "app.fill") {
                  MenuBarContentView()
              }
              .menuBarExtraStyle(.window)
          }
      }
      ```
      
      ## WindowGroup
      
      Creates standard resizable windows. Multiple instances allowed by default:
      
      ```swift
      // Basic
      WindowGroup {
          ContentView()
      }
      
      // With identifier for programmatic opening
      WindowGroup("Editor", id: "editor") {
          EditorView()
      }
      
      // With data binding - open window for specific item
      WindowGroup("Detail", id: "detail", for: Item.ID.self) { $itemID in
          if let itemID {
              DetailView(itemID: itemID)
          }
      }
      
      // Open programmatically
      @Environment(\.openWindow) private var openWindow
      
      Button("Open Editor") {
          openWindow(id: "editor")
      }
      
      Button("Show Detail") {
          openWindow(value: selectedItem.id)
      }
      ```
      
      Window modifiers:
      ```swift
      WindowGroup {
          ContentView()
      }
      .defaultSize(width: 800, height: 600)
      .defaultSize(CGSize(width: 800, height: 600))
      .defaultPosition(.center)          // .leading, .trailing, .topLeading, etc.
      .windowResizability(.automatic)     // .contentSize, .contentMinSize
      .windowStyle(.automatic)            // .hiddenTitleBar, .titleBar
      .windowToolbarStyle(.unified)       // .unifiedCompact, .expanded, .automatic
      .keyboardShortcut("n", modifiers: .command)
      ```
      
      ## Window (Single Instance)
      
      For utility/auxiliary windows that should have only one instance:
      
      ```swift
      Window("Inspector", id: "inspector") {
          InspectorView()
      }
      .defaultSize(width: 300, height: 500)
      .windowResizability(.contentSize)
      .commandsRemoved()  // Remove default window commands
      ```
      
      Dismiss from within:
      ```swift
      @Environment(\.dismissWindow) private var dismissWindow
      
      Button("Close") {
          dismissWindow(id: "inspector")
      }
      ```
      
      ## Settings Scene
      
      Preferences window accessible via Cmd+,:
      
      ```swift
      Settings {
          TabView {
              GeneralSettingsView()
                  .tabItem { Label("General", systemImage: "gear") }
              AppearanceSettingsView()
                  .tabItem { Label("Appearance", systemImage: "paintpalette") }
              AdvancedSettingsView()
                  .tabItem { Label("Advanced", systemImage: "wrench") }
          }
          .frame(width: 450)
      }
      ```
      
      Use `@AppStorage` for UserDefaults-backed preferences:
      ```swift
      struct GeneralSettingsView: View {
          @AppStorage("autoSave") private var autoSave = true
          @AppStorage("fontSize") private var fontSize = 14.0
          @AppStorage("theme") private var theme = "system"
      
          var body: some View {
              Form {
                  Toggle("Auto-save documents", isOn: $autoSave)
                  Slider(value: $fontSize, in: 10...24, step: 1) {
                      Text("Font Size: \(Int(fontSize))pt")
                  }
                  Picker("Theme", selection: $theme) {
                      Text("System").tag("system")
                      Text("Light").tag("light")
                      Text("Dark").tag("dark")
                  }
              }
              .formStyle(.grouped)
              .padding()
          }
      }
      ```
      
      ## MenuBarExtra
      
      Two styles - menu or window:
      
      ```swift
      // Menu style (dropdown menu)
      MenuBarExtra("Status", systemImage: "circle.fill") {
          Button("Show Dashboard") { openWindow(id: "dashboard") }
          Divider()
          Toggle("Monitoring", isOn: $isMonitoring)
          Divider()
          Button("Quit") { NSApplication.shared.terminate(nil) }
      }
      .menuBarExtraStyle(.menu)
      
      // Window style (popover window)
      MenuBarExtra("Status", systemImage: "circle.fill") {
          VStack {
              Text("System Status")
                  .font(.headline)
              StatusDashboard()
          }
          .frame(width: 300, height: 400)
      }
      .menuBarExtraStyle(.window)
      ```
      
      Dynamic icon:
      ```swift
      MenuBarExtra {
          MenuBarContent()
      } label: {
          Image(systemName: isConnected ? "wifi" : "wifi.slash")
          if showBadge {
              Text("\(unreadCount)")
          }
      }
      ```
      
      ## MenuBarExtra Gotchas
      
      ### .menu style strips SwiftUI font modifiers
      
      With `.menuBarExtraStyle(.menu)`, content renders to native `NSMenu` items via `NSStatusBarButton`. All SwiftUI font modifiers (`.monospacedDigit()`, `.font(.system(.body, design: .monospaced))`) are silently ignored. Timer displays like "0:05" jump width as digits change.
      
      Fix options:
      1. **Fixed-width frame**: `.frame(width: 38)` on the timer text
      2. **ImageRenderer trick** (used by AeroSpace app): render `Text` with proper font into a `CGImage`, display as `Image`. Font modifiers are baked into the rendered image, bypassing the bridge.
      
      ### TimelineView doesn't work in .menu style
      
      `TimelineView(.periodic(from: .now, by: 1))` doesn't tick - `.menu` style renders to static `NSMenu` items. Use a `Timer` firing every second that updates an `@Observable` property instead:
      
      ```swift
      @Observable
      class AppState {
          var formattedElapsed = "0:00"
          private var timer: Timer?
      
          func startTimer() {
              timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
                  self?.formattedElapsed = self?.computeElapsed() ?? "0:00"
              }
          }
      }
      ```
      
      ### onAppear never fires until menu is opened
      
      With `.menuBarExtraStyle(.menu)`, the content view's `onAppear` only fires when the user clicks the menu bar icon. Never place initialization code (monitoring start, permission requests, delegate setup) in `onAppear`. Use `applicationDidFinishLaunching` instead.
      
      ### @NSApplicationDelegateAdaptor reference timing
      
      `(NSApplication.shared.delegate as? AppDelegate)?.monitor = monitor` in `App.init()` may silently fail. Use the adaptor property directly:
      
      ```swift
      @main
      struct MyApp: App {
          @NSApplicationDelegateAdaptor private var delegate: AppDelegate
      
          init() {
              delegate.monitor = monitor // Works - uses adaptor property directly
          }
      }
      ```
      
      ## DocumentGroup
      
      For document-based apps:
      
      ```swift
      @main
      struct TextEditorApp: App {
          var body: some Scene {
              DocumentGroup(newDocument: TextDocument()) { file in
                  TextEditorView(document: file.$document)
              }
          }
      }
      
      // Document type
      struct TextDocument: FileDocument {
          static var readableContentTypes: [UTType] { [.plainText] }
      
          var text: String
      
          init(text: String = "") {
              self.text = text
          }
      
          init(configuration: ReadConfiguration) throws {
              guard let data = configuration.file.regularFileContents,
                    let text = String(data: data, encoding: .utf8)
              else { throw CocoaError(.fileReadCorruptFile) }
              self.text = text
          }
      
          func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
              let data = text.data(using: .utf8)!
              return FileWrapper(regularFileWithContents: data)
          }
      }
      ```
      
      For `ReferenceFileDocument` (class-based, supports undo):
      ```swift
      class RichDocument: ReferenceFileDocument {
          static var readableContentTypes: [UTType] { [.rtf] }
      
          @Published var content: AttributedString
      
          required init(configuration: ReadConfiguration) throws { /* ... */ }
      
          func snapshot(contentType: UTType) throws -> Data { /* ... */ }
      
          func fileWrapper(snapshot: Data, configuration: WriteConfiguration) throws -> FileWrapper {
              FileWrapper(regularFileWithContents: snapshot)
          }
      }
      ```
      
      ## Window Management
      
      ### Restore behavior (macOS 15+)
      ```swift
      WindowGroup {
          ContentView()
      }
      .restorationBehavior(.enabled) // .disabled, .enabled
      ```
      
      ### Window level
      ```swift
      .windowLevel(.floating) // Keep window above others
      ```
      
      ### Presented window style
      
      `presentedWindowStyle` is a `View` modifier that styles windows this view presents. The complete set of `WindowStyle` values in the macOS 26.5 SDK is `.automatic`, `.titleBar`, `.hiddenTitleBar`, and `.plain` - there is no `.fullScreen` member.
      
      ```swift
      .presentedWindowStyle(.hiddenTitleBar) // .automatic, .titleBar, .plain
      ```
      
      Full-screen is not a `WindowStyle`. Drive it from AppKit (`NSWindow.toggleFullScreen(_:)`) or let the user use the standard green-button behavior.
      
      ## Scene Phases
      
      React to app lifecycle:
      ```swift
      @Environment(\.scenePhase) private var scenePhase
      
      var body: some View {
          ContentView()
              .onChange(of: scenePhase) { oldPhase, newPhase in
                  switch newPhase {
                  case .active:
                      // App is active and visible
                      refreshData()
                  case .inactive:
                      // App is visible but not interactive
                      break
                  case .background:
                      // App is in background
                      saveState()
                  @unknown default:
                      break
                  }
              }
      }
      ```
      
      ## NSApplicationDelegateAdaptor
      
      Bridge to AppKit delegate for system events:
      
      ```swift
      class AppDelegate: NSObject, NSApplicationDelegate {
          func applicationDidFinishLaunching(_ notification: Notification) {
              // Register URL scheme handlers, set up global state
          }
      
          func applicationWillTerminate(_ notification: Notification) {
              // Cleanup
          }
      
          func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
              false // Keep menu bar app alive
          }
      
          func application(_ application: NSApplication, open urls: [URL]) {
              // Handle URL scheme
          }
      }
      ```
      
      ## Async Termination Cleanup
      
      For apps that need async cleanup before quitting (finalizing file writers, stopping streams):
      
      ```swift
      class AppDelegate: NSObject, NSApplicationDelegate {
          var monitor: AudioMonitor?
          private var hasReplied = false
      
          func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
              hasReplied = false
      
              // Cleanup task
              Task { @MainActor in
                  await monitor?.stopAndSave()
                  guard !hasReplied else { return }
                  hasReplied = true
                  NSApplication.shared.reply(toApplicationShouldTerminate: true)
              }
      
              // Timeout task - prevents hanging forever
              Task { @MainActor in
                  try? await Task.sleep(for: .seconds(8))
                  guard !hasReplied else { return }
                  hasReplied = true
                  NSApplication.shared.reply(toApplicationShouldTerminate: true)
              }
      
              return .terminateLater
          }
      }
      ```
      
      The `hasReplied` flag prevents double-reply (undefined behavior). Both Tasks are MainActor (serial), so the flag check is race-free. Use `movieFragmentInterval` on AVAssetWriter to bound data loss to ~10s even if timeout fires.
      
      ### Prevent idle sleep during recording
      
      ```swift
      var activity: NSObjectProtocol?
      activity = ProcessInfo.processInfo.beginActivity(
          .userInitiated, reason: "Recording audio"
      )
      // ... later:
      if let a = activity { ProcessInfo.processInfo.endActivity(a) }
      ```
      
      ## LSUIElement Operational Issues
      
      ### Windows open behind other apps
      
      LSUIElement apps don't auto-activate. When opening windows from the menu:
      
      ```swift
      Button("Show Settings") {
          openWindow(id: "settings")
          NSApplication.shared.activate(ignoringOtherApps: true)
      }
      ```
      
      ### Cmd+, doesn't work
      
      LSUIElement apps have no app menu bar, so `CommandGroup(replacing: .appSettings)` has nowhere to attach. Add `.keyboardShortcut(",", modifiers: .command)` to a button in the menu dropdown (only works when menu is open).
      
      ### Crash detection
      
      When an LSUIElement menu bar app crashes, the icon silently disappears - no crash dialog. Use a watchdog helper binary that monitors the main process via `kqueue`/`kevent(EVFILT_PROC, NOTE_EXIT)` and shows an alert on crash signals (SIGTRAP, SIGABRT, SIGSEGV).
      
      ### Onboarding for LSUIElement apps
      
      Host onboarding in a manually-created `NSWindow` (not a SwiftUI `Window` scene), since the window must appear before any user interaction:
      
      ```swift
      func applicationDidFinishLaunching(_ notification: Notification) {
          guard !UserDefaults.standard.bool(forKey: "hasCompletedOnboarding") else { return }
          // Skip onboarding for existing users who already have permissions
          if CGPreflightScreenCaptureAccess() {
              UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding")
              return
          }
          let window = NSWindow(contentRect: .init(x: 0, y: 0, width: 500, height: 400),
                                styleMask: [.titled, .closable], backing: .buffered, defer: false)
          window.isReleasedWhenClosed = false // Prevent dangling reference crash
          window.contentView = NSHostingView(rootView: OnboardingView(onComplete: { ... }))
          window.center()
          window.makeKeyAndOrderFront(nil)
          NSApp.activate(ignoringOtherApps: true)
      }
      ```
      
      Use `onComplete` callback instead of `@Environment(\.dismiss)` - dismiss has no backing in a manually-created NSWindow. Set `isReleasedWhenClosed = false` and nil the window reference before calling `close()` if you also use `NSWindowDelegate.windowWillClose`.
      
      ### Restarting the app
      
      Don't use `NSWorkspace.OpenConfiguration(createsNewApplicationInstance: true)` - it spawns a duplicate before the first exits. Use `Process` + terminate:
      
      ```swift
      func restartApp() {
          let path = Bundle.main.bundlePath
          Process.launchedProcess(launchPath: "/usr/bin/open", arguments: [path])
          NSApplication.shared.terminate(nil)
      }
      ```
      
      ## Crash: high-frequency MenuBarExtra label updates
      
      A per-second timer driving the `MenuBarExtra` label (an elapsed-time readout, a live meter) can crash intermittently with `EXC_BREAKPOINT` inside `-[NSWindow _postWindowNeedsUpdateConstraints]`. The `NSHostingView` backing the status item is pushed into an AppKit constraint-update exception by the repeated re-layout. It compiles, usually runs, and crashes in the field.
      
      Keep the SwiftUI label static and push frequent text through AppKit directly:
      
      ```swift
      // Static label - SwiftUI never re-lays-out on every tick
      MenuBarExtra("Recorder", systemImage: "record.circle") { MenuBarView() }
      
      // Update the status item's title from AppKit instead
      statusItem.button?.attributedTitle = NSAttributedString(
          string: elapsed,
          attributes: [.font: NSFont.monospacedDigitSystemFont(ofSize: 13, weight: .regular)]
      )
      ```
      
      Monospaced digits stop the width from oscillating each tick, which is what drives the constraint churn. If you must render SwiftUI, pre-render with `ImageRenderer` and assign the resulting image, and lower the update frequency - a wall-clock readout rarely needs more than 1 Hz, and a meter can be throttled well below its sample rate.
      
      ## Crash visibility for menu-bar-only apps
      
      When an `LSUIElement` app crashes, macOS suppresses the "quit unexpectedly" dialog. The icon simply disappears and the user keeps believing the app is running - the worst failure mode for anything doing background recording or monitoring. An in-app `NSSetUncaughtExceptionHandler` only logs, and a crash notice shown at next launch lands in a dropdown nobody opens.
      
      Bundle a small helper that watches the main app's PID and reports unexpected exits:
      
      ```c
      int fd = kqueue();
      struct kevent change;
      EV_SET(&change, targetPID, EVFILT_PROC, EV_ADD | EV_ENABLE, NOTE_EXIT, 0, NULL);
      kevent(fd, &change, 1, NULL, 0, NULL);
      
      struct kevent event;
      kevent(fd, NULL, 0, &event, 1, NULL);   // blocks until the process exits
      // Inspect exit status, then show a dialog if it was not a clean quit
      ```
      
      Choose the hosting model deliberately:
      
      | Model | Pros | Cons |
      |---|---|---|
      | Spawned by the app at launch | No system prompt, dies with the parent, nothing in System Settings | Cannot catch very early startup crashes |
      | `SMAppService` login item in `Contents/Library/LoginItems/` | Survives reboot, catches startup crashes | Triggers a one-time "Background Items Added" notification, and the user can silently disable it in System Settings |
      
      The spawned helper is the lighter default. Reach for the registered login item only if crashes during startup are the ones you actually need to see.
      
    • appkit-interop.md 12.5 KB
      # AppKit Interop
      
      ## Table of Contents
      - NSViewRepresentable
      - NSViewControllerRepresentable
      - Hosting SwiftUI in AppKit
      - Common Bridges
      - NSWindow Access
      - NSPanel / Floating HUD
      
      ## NSViewRepresentable
      
      Wrap an AppKit view for use in SwiftUI:
      
      ```swift
      struct ColorWellView: NSViewRepresentable {
          @Binding var color: Color
      
          func makeNSView(context: Context) -> NSColorWell {
              let well = NSColorWell()
              well.target = context.coordinator
              well.action = #selector(Coordinator.colorChanged(_:))
              return well
          }
      
          func updateNSView(_ well: NSColorWell, context: Context) {
              well.color = NSColor(color)
          }
      
          func makeCoordinator() -> Coordinator {
              Coordinator(color: $color)
          }
      
          class Coordinator: NSObject {
              var color: Binding<Color>
      
              init(color: Binding<Color>) {
                  self.color = color
              }
      
              @objc func colorChanged(_ sender: NSColorWell) {
                  color.wrappedValue = Color(nsColor: sender.color)
              }
          }
      }
      ```
      
      ### Lifecycle methods
      
      ```swift
      func makeNSView(context: Context) -> NSView     // Create (called once)
      func updateNSView(_ view: NSView, context: Context)  // Update (called on state changes)
      static func dismantleNSView(_ view: NSView, coordinator: Coordinator)  // Cleanup
      
      // sizeThatFits for intrinsic sizing
      func sizeThatFits(_ proposal: ProposedViewSize, nsView: NSView, context: Context) -> CGSize? {
          nsView.intrinsicContentSize == .zero ? nil : nsView.intrinsicContentSize
      }
      ```
      
      ### Coordinator for delegates
      
      ```swift
      struct TextViewWrapper: NSViewRepresentable {
          @Binding var text: String
      
          func makeNSView(context: Context) -> NSScrollView {
              let scrollView = NSTextView.scrollableTextView()
              let textView = scrollView.documentView as! NSTextView
              textView.delegate = context.coordinator
              textView.isEditable = true
              textView.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
              return scrollView
          }
      
          func updateNSView(_ scrollView: NSScrollView, context: Context) {
              let textView = scrollView.documentView as! NSTextView
              if textView.string != text {
                  textView.string = text
              }
          }
      
          func makeCoordinator() -> Coordinator { Coordinator(text: $text) }
      
          class Coordinator: NSObject, NSTextViewDelegate {
              var text: Binding<String>
              init(text: Binding<String>) { self.text = text }
      
              func textDidChange(_ notification: Notification) {
                  guard let textView = notification.object as? NSTextView else { return }
                  text.wrappedValue = textView.string
              }
          }
      }
      ```
      
      ## NSViewControllerRepresentable
      
      Wrap entire AppKit view controllers:
      
      ```swift
      struct PDFViewWrapper: NSViewControllerRepresentable {
          let url: URL
      
          func makeNSViewController(context: Context) -> PDFViewController {
              PDFViewController(url: url)
          }
      
          func updateNSViewController(_ controller: PDFViewController, context: Context) {
              controller.loadPDF(from: url)
          }
      }
      ```
      
      ## Hosting SwiftUI in AppKit
      
      Embed SwiftUI views inside AppKit apps:
      
      ```swift
      // In an NSViewController
      class MainViewController: NSViewController {
          override func viewDidLoad() {
              super.viewDidLoad()
      
              let swiftUIView = ContentView()
              let hostingView = NSHostingView(rootView: swiftUIView)
              hostingView.translatesAutoresizingMaskIntoConstraints = false
      
              view.addSubview(hostingView)
              NSLayoutConstraint.activate([
                  hostingView.topAnchor.constraint(equalTo: view.topAnchor),
                  hostingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
                  hostingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                  hostingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
              ])
          }
      }
      
      // As a window
      let window = NSWindow(
          contentRect: NSRect(x: 0, y: 0, width: 600, height: 400),
          styleMask: [.titled, .closable, .resizable],
          backing: .buffered,
          defer: false
      )
      window.contentView = NSHostingView(rootView: ContentView())
      window.makeKeyAndOrderFront(nil)
      ```
      
      ### Sizing and window bridging options
      
      A bare `NSHostingView` has **no intrinsic content size** and does not surface its content's title or toolbar to the enclosing window - both are opt-in:
      
      ```swift
      let hosting = NSHostingView(rootView: ContentView())
      hosting.sizingOptions = [.intrinsicContentSize]              // macOS 13+
      hosting.sceneBridgingOptions = [.title, .toolbars]           // macOS 14+
      ```
      
      `NSHostingSizingOptions` controls how the hosting view reflects its content's size into Auto Layout constraints; `NSHostingSceneBridgingOptions` controls which window aspects the SwiftUI content is allowed to manage. If a hosted view "has no size" under Auto Layout, `sizingOptions` is almost always the missing piece.
      
      ### Hosting whole SwiftUI scenes (macOS 26+)
      
      Hand-rolling an `NSWindow` for a Settings screen, as above, is the old way. macOS 26 adds `NSHostingSceneRepresentation` - an AppKit type that hosts and presents SwiftUI **scenes** - registered via `NSApplication.addSceneRepresentation(_:)`. An AppKit-lifecycle app can present a SwiftUI `Settings` or `Window` scene with its real scene semantics (Cmd+, handling, restoration, single-instance behaviour) instead of reimplementing them around a manually created window. Prefer it over the manual `NSWindow` recipe when your deployment target allows.
      
      ### Gesture recognizers
      
      `NSGestureRecognizerRepresentable` (macOS 26+) is the third `Representable` protocol alongside `NSViewRepresentable` and `NSViewControllerRepresentable`. It wraps an `NSGestureRecognizer` for use in a SwiftUI hierarchy - the way to reuse an existing AppKit recognizer, or to get recognizer behaviour SwiftUI's built-in gestures do not expose.
      
      ## Liquid Glass in AppKit (macOS 26+)
      
      The SwiftUI half of Liquid Glass is `.glassEffect()` / `GlassEffectContainer`. AppKit has its own:
      
      | Type | Purpose |
      |---|---|
      | `NSGlassEffectView` | "A view that embeds its content view in a dynamic glass effect." |
      | `NSGlassEffectContainerView` | "A view that efficiently merges descendant glass effect views together when they are within a specified proximity to each other." |
      | `NSBackgroundExtensionView` | "A view that extends content to fill its own bounds" - lay it out past the safe area to run content under the titlebar, sidebar, or inspector. |
      
      The single most useful migration escape hatch is on `NSView` itself:
      
      ```swift
      view.prefersCompactControlSizeMetrics = true
      ```
      
      > When this property is `YES`, any `NSControl`s in the view or its descendants will be sized with compact metrics compatible with macOS 15.0 and earlier. Defaults to `NO`.
      
      That is the fix when Liquid Glass inflates an existing dense AppKit layout - a toolbar or inspector that was tuned for pre-26 metrics and now overflows. All four symbols are in the macOS 26.5 SDK (`AppKit.framework/Headers/NSGlassEffectView.h`, `NSBackgroundExtensionView.h`, `NSView.h`).
      
      ## Common Bridges
      
      ### NSPasteboard (Clipboard)
      ```swift
      // Copy
      NSPasteboard.general.clearContents()
      NSPasteboard.general.setString(text, forType: .string)
      
      // Paste
      if let string = NSPasteboard.general.string(forType: .string) {
          // use string
      }
      ```
      
      **Reading the general pasteboard is becoming user-visible.** macOS 15.4 added `NSPasteboard.accessBehavior`, and Apple is rolling out an alert shown when an app programmatically reads the general pasteboard - the `string(forType:)` call above is exactly what triggers it:
      
      > Prepare your app for an upcoming feature in macOS that alerts a person using a device when your app programmatically reads the general pasteboard. ... New `detect` methods in NSPasteboard and NSPasteboardItem make it possible for an app to examine the kinds of data on the pasteboard without actually reading them and showing the alert.
      
      If you only need to know *whether* the pasteboard holds something you can handle - to enable a Paste menu item, say - use `detectedValues(for:)` / `detectedPatterns(for:)` rather than reading. Read the contents only in response to an explicit user paste action, which is what the alert is designed to permit. Test the behaviour ahead of the rollout:
      
      ```bash
      defaults write <your_app_bundle_id> EnablePasteboardPrivacyDeveloperPreview -bool yes
      ```
      
      Apps using the standard responder-chain paste (`NSTextView`, `Cmd+V` through first responder) are unaffected; this hits polling and clipboard-manager patterns.
      
      ### NSWorkspace
      ```swift
      // Open file in default app
      NSWorkspace.shared.open(fileURL)
      
      // Open URL in browser
      NSWorkspace.shared.open(URL(string: "https://example.com")!)
      
      // Reveal in Finder
      NSWorkspace.shared.activateFileViewerSelecting([fileURL])
      
      // Get running applications
      let apps = NSWorkspace.shared.runningApplications
      ```
      
      ### NSSavePanel / NSOpenPanel
      ```swift
      func selectFile() async -> URL? {
          let panel = NSOpenPanel()
          panel.allowedContentTypes = [.json, .plainText]
          panel.allowsMultipleSelection = false
          panel.canChooseDirectories = false
      
          let result = await panel.begin()
          return result == .OK ? panel.url : nil
      }
      
      func saveFile() async -> URL? {
          let panel = NSSavePanel()
          panel.allowedContentTypes = [.json]
          panel.nameFieldStringValue = "export.json"
      
          let result = await panel.begin()
          return result == .OK ? panel.url : nil
      }
      ```
      
      ## NSWindow Access
      
      Access the underlying NSWindow from SwiftUI:
      
      ```swift
      struct WindowAccessor: NSViewRepresentable {
          let callback: (NSWindow) -> Void
      
          func makeNSView(context: Context) -> NSView {
              let view = NSView()
              DispatchQueue.main.async {
                  if let window = view.window {
                      callback(window)
                  }
              }
              return view
          }
      
          func updateNSView(_ nsView: NSView, context: Context) {}
      }
      
      // Usage
      ContentView()
          .background(WindowAccessor { window in
              window.titlebarAppearsTransparent = true
              window.isOpaque = false
              window.backgroundColor = .clear
          })
      ```
      
      ## NSPanel / Floating HUD
      
      For toast notifications, recording indicators, or floating widgets that must appear above all apps (including full-screen):
      
      ```swift
      class HUDPanel: NSPanel {
          init(content: some View) {
              super.init(
                  contentRect: .zero,
                  styleMask: [.nonactivatingPanel, .fullSizeContentView],
                  backing: .buffered,
                  defer: true
              )
              level = .floating
              isOpaque = false
              backgroundColor = .clear
              hidesOnDeactivate = false // CRITICAL for menu bar apps
              isReleasedWhenClosed = false // Prevent dangling reference
              collectionBehavior = [
                  .canJoinAllSpaces,        // Visible on all Spaces/desktops
                  .fullScreenAuxiliary,     // Visible over full-screen apps
                  .transient,               // Don't appear in Mission Control
              ]
      
              let hostingView = NSHostingView(rootView: content)
              hostingView.frame.size = hostingView.fittingSize
              contentView = hostingView
              setContentSize(hostingView.fittingSize)
          }
      
          func show(on screen: NSScreen? = NSScreen.main) {
              guard let screen else { return }
              let size = contentView?.fittingSize ?? .zero
              // Use visibleFrame (not frame) to avoid menu bar overlap
              let origin = NSPoint(
                  x: screen.visibleFrame.maxX - size.width - 16,
                  y: screen.visibleFrame.maxY - size.height - 16
              )
              setFrameOrigin(origin)
              orderFrontRegardless()
      
              // Auto-dismiss after delay
              DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
                  self?.close()
              }
          }
      }
      ```
      
      Key gotchas:
      - **`hidesOnDeactivate = false`**: Required for menu bar apps - the HUD must stay visible when the app isn't frontmost.
      - **Use `screen.visibleFrame`** not `screen.frame` - `frame` includes the menu bar area.
      - **`.fixedSize()`** on the SwiftUI content - without it, `fittingSize` compresses the content and text truncates.
      - **`isReleasedWhenClosed = false`**: Prevents a dangling reference crash if you hold a strong reference to the panel.
      
      ### HUD actions bridging to SwiftUI
      
      NSPanel can't access `@Environment(\.openWindow)`. Use NotificationCenter to bridge:
      
      ```swift
      // In HUD - post notification on click:
      static let showMainWindowNotification = Notification.Name("ShowMainWindow")
      NSNotificationCenter.default.post(name: Self.showMainWindowNotification, object: nil)
      
      // In SwiftUI MenuBarExtra content - receive and act:
      .onReceive(NotificationCenter.default.publisher(for: HUDPanel.showMainWindowNotification)) { _ in
          openWindow(id: "main")
          NSApp.activate(ignoringOtherApps: true)
      }
      ```
      
    • approachable-concurrency.md 13.1 KB
      # Approachable Concurrency (Swift 6.2)
      
      ## Table of Contents
      - Vision & Philosophy
      - Default MainActor Isolation
      - @concurrent Attribute
      - Nonisolated Async Changes
      - Enabling in Xcode
      - Migration Strategy
      - Runtime Pitfalls with Default Isolation
      
      ## Vision & Philosophy
      
      Swift 6.2 addresses feedback that Swift Concurrency was too difficult to adopt. The key insight: most app code doesn't need concurrency. The new model follows progressive disclosure:
      
      1. **Phase 1** - Write sequential code. Default isolation keeps everything on main actor.
      2. **Phase 2** - Add `async/await` for suspension without introducing parallelism.
      3. **Phase 3** - Opt into parallelism with `@concurrent` when you need performance.
      
      ## Default MainActor Isolation
      
      ### Enabling
      
      Package.swift:
      ```swift
      .executableTarget(
          name: "MyApp",
          swiftSettings: [
              .defaultIsolation(MainActor.self),
          ]
      )
      ```
      
      Xcode: Build Settings > Swift Compiler - Upcoming Features > Default Isolation > MainActor
      
      Default isolation is a **target-level** compiler setting - there is no documented per-file `defaultIsolation(nil)` directive. Opt out per-declaration instead:
      
      ```swift
      // At the declaration level
      nonisolated class NetworkClient { /* not MainActor-isolated even inside a MainActor-default target */ }
      
      @concurrent func heavyWork() async -> Data { /* explicitly runs off the caller actor */ }
      ```
      
      ### What changes
      
      With `-default-isolation MainActor`:
      
      ```swift
      // Before: needed explicit annotations
      @MainActor
      class ViewModel {
          @MainActor
          var items: [Item] = []
      
          @MainActor
          func refresh() async { /* ... */ }
      }
      
      // After: everything is implicitly @MainActor
      class ViewModel {
          var items: [Item] = []  // protected by MainActor
      
          func refresh() async {
              items = try await fetchItems() // suspends, resumes on main
          }
      }
      ```
      
      ### What's NOT affected
      - Types in libraries imported without default isolation
      - Protocol conformances to protocols defined outside the module
      - `@concurrent` functions (explicit opt-out)
      - Types explicitly marked `nonisolated`
      
      ### Interaction with existing code
      
      ```swift
      // This "just works" now
      struct ContentView: View {
          @State private var items: [Item] = []
      
          var body: some View {
              List(items) { item in
                  Text(item.name)
              }
              .task {
                  // No actor-isolation errors - everything on MainActor
                  items = try await loadItems()
              }
          }
      }
      ```
      
      ## @concurrent Attribute
      
      Explicitly request execution on the concurrent thread pool:
      
      ```swift
      // Heavy computation - run off main actor
      @concurrent
      func compressImage(_ data: Data, quality: Double) async throws -> Data {
          // Runs on concurrent pool, keeping main actor free
          let source = CGImageSourceCreateWithData(data as CFData, nil)!
          // ... expensive processing
          return compressedData
      }
      
      // I/O-bound work
      @concurrent
      func loadFileContents(_ url: URL) async throws -> String {
          try String(contentsOf: url, encoding: .utf8)
      }
      
      // Call from MainActor context
      func handleImport() async throws {
          // Automatically dispatched to concurrent pool
          let data = try await loadFileContents(fileURL)
          // Back on MainActor after await
          self.content = data
      }
      ```
      
      ### When to use @concurrent
      - CPU-intensive work (image processing, data parsing, compression)
      - Large file I/O
      - Expensive computations (sorting large datasets, cryptography)
      - Any work that would cause UI jank if run on the main actor
      
      ### When NOT to use @concurrent
      - Simple property access or state updates
      - UI-related operations
      - Short synchronous operations
      - Functions that primarily call other async functions (just let them suspend)
      
      ## Nonisolated Async Changes
      
      In Swift 6.2, `nonisolated async` functions behave differently:
      
      ```swift
      // Swift 6.1: nonisolated async always ran on global pool
      // Swift 6.2: nonisolated async runs in caller's context
      
      class DataProcessor {
          nonisolated func process() async -> Data {
              // Swift 6.1: This ran on the global concurrent pool
              // Swift 6.2: This runs wherever the caller is (e.g., MainActor)
              return computeData()
          }
      }
      
      // To get the old behavior (run on concurrent pool), use @concurrent:
      class DataProcessor {
          @concurrent
          func process() async -> Data {
              return computeData() // Explicitly on concurrent pool
          }
      }
      ```
      
      This change (SE-0461) is gated on the **`NonisolatedNonsendingByDefault` upcoming feature flag** — it is not on by default in Swift 6.2, not even when default isolation is set. Enable it explicitly in Package.swift:
      
      ```swift
      swiftSettings: [
          .defaultIsolation(MainActor.self),
          .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
      ]
      ```
      
      Without the flag, `nonisolated async` still runs on the global concurrent pool. Source: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md
      
      ## Migration Strategy
      
      ### From Swift 5 / no concurrency
      1. Enable `-default-isolation MainActor` on your target
      2. Build and fix errors (mostly around library boundaries)
      3. Mark expensive functions with `@concurrent`
      4. Test thoroughly - behavior change is mostly in async function execution context
      
      ### From Swift 6.0/6.1 with explicit @MainActor
      1. Enable default isolation
      2. Remove redundant `@MainActor` annotations
      3. Replace `nonisolated` background work with `@concurrent`
      4. Remove unnecessary `Sendable` annotations (compiler infers more)
      
      ### Gradual adoption
      
      Split modules at the SPM level — give the converted target `.defaultIsolation(MainActor.self)` and leave the legacy target alone. Within the converted target, use `nonisolated` / `@concurrent` to opt individual declarations out (see the "Default MainActor Isolation" section above for why there's no per-file directive).
      
      ## Runtime Pitfalls with Default Isolation
      
      These issues compile cleanly but crash at runtime. The compiler does not always catch them.
      
      ### Closure isolation inheritance (most dangerous)
      
      Closures defined inside MainActor-isolated methods inherit MainActor isolation. When passed to APIs that call them on background threads, Swift 6 runtime checks the executor and crashes with `dispatch_assert_queue_fail` / SIGTRAP.
      
      Affected APIs include `AVAudioEngine.installTap`, `NotificationCenter.addObserver` with `queue: nil`, `DispatchSource.setEventHandler`, and `NSSetUncaughtExceptionHandler`.
      
      ```swift
      // CRASHES at runtime - closure inherits MainActor isolation but runs on audio thread:
      func startCapture() {
          engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: fmt) { buffer, time in
              self.process(buffer) // dispatch_assert_queue_fail
          }
      }
      
      // FIX - extract to @Sendable typed variable to break isolation inheritance:
      func startCapture() {
          let handler: @Sendable (AVAudioPCMBuffer, AVAudioTime) -> Void = { [weak self] buffer, time in
              self?.process(buffer) // nonisolated, safe on any thread
          }
          engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: fmt, block: handler)
      }
      ```
      
      Same fix needed for NotificationCenter observers:
      ```swift
      // CRASHES - observer closure inherits MainActor, but CoreAudio fires on I/O thread:
      NotificationCenter.default.addObserver(
          forName: .AVAudioEngineConfigurationChange, object: engine, queue: nil
      ) { [weak self] _ in self?.handleConfigChange() }
      
      // FIX - either use queue: .main or extract to @Sendable:
      NotificationCenter.default.addObserver(
          forName: .AVAudioEngineConfigurationChange, object: engine, queue: .main
      ) { [weak self] _ in self?.handleConfigChange() }
      ```
      
      ### deinit is nonisolated by default; SE-0371 offers an opt-in
      
      By default, `deinit` is `nonisolated` and cannot access actor-isolated properties or call MainActor-isolated methods. For classes that can't adopt `isolated deinit` (see `actors-isolation.md`), properties needed for cleanup must be `nonisolated(unsafe)`:
      
      ```swift
      @Observable
      class ResourceManager: @unchecked Sendable {
          // Pair @ObservationIgnored with nonisolated(unsafe) for internal state
          @ObservationIgnored nonisolated(unsafe) private var listenerID: AudioObjectID?
      
          deinit {
              // Can access nonisolated(unsafe) properties directly
              if let id = listenerID {
                  AudioObjectRemovePropertyListener(id, &address, callback, nil)
              }
          }
      }
      ```
      
      In Swift 6.2+, actors (and global-actor-isolated classes) can opt into **`isolated deinit`** (SE-0371) so cleanup runs on the actor's executor and can touch isolated state directly. Task-local values are cleared on entry to an isolated deinit. See `actors-isolation.md` for the full pattern. Source: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0371-isolated-synchronous-deinit.md
      
      ### Types and enums need explicit nonisolated for cross-isolation use
      
      With default isolation, all types are MainActor. Value types and enums used in background callbacks or `Task.detached` need explicit opt-out:
      
      ```swift
      // Without this, using LevelSource in a nonisolated SCStreamOutput callback errors:
      nonisolated enum LevelSource: Sendable {
          case mic, system, both
      }
      
      // Codable structs used across isolation boundaries:
      nonisolated struct TranscriptSegment: Codable, Sendable {
          let timestamp: Double
          let text: String
      }
      ```
      
      ### Non-Sendable Apple framework types
      
      Several Apple types lack Sendable conformance. Use `@preconcurrency import` to suppress warnings:
      
      ```swift
      @preconcurrency import AVFoundation // Suppresses AVAudioPCMBuffer, AVAssetWriter Sendable warnings
      
      // KeyPath is not Sendable - Table sort with KeyPathComparator breaks:
      // Instead of: @State var sortOrder = [KeyPathComparator(\Item.date)]
      // Pre-sort data in the source and avoid KeyPathComparator entirely.
      
      // AVAssetWriter can't cross TaskGroup boundary (sending parameter).
      // Use a simple Task instead of TaskGroup for timeout patterns.
      ```
      
      ### Methods called from nonisolated contexts
      
      Functions called from background callbacks must be explicitly `nonisolated`:
      
      ```swift
      class AudioProcessor: @unchecked Sendable {
          // Called from audioQueue (background) - must be explicit nonisolated
          nonisolated private func handleConfigChange(engine: AVAudioEngine) {
              // Only access nonisolated(unsafe) state or dispatch to queue
          }
      
          // Log utilities must also be nonisolated to work from any thread
          nonisolated static func log(_ message: String) { ... }
      }
      ```
      
      ### Callbacks set after init create data races
      
      Mutable closure properties set after initialization are read from background callbacks:
      
      ```swift
      // BAD - data race between MainActor set and background read:
      class Recorder {
          var onError: ((Error) -> Void)?
      }
      
      // GOOD - immutable, passed at init:
      class Recorder {
          nonisolated let onError: (@Sendable (Error) -> Void)?
          init(onError: (@Sendable (Error) -> Void)? = nil) {
              self.onError = onError
          }
      }
      ```
      
      ## `nonisolated(nonsending)` - the spelling behind the behavior
      
      The section above describes how a `nonisolated async` function runs on the caller's actor rather than hopping to the global executor. The attribute that expresses this explicitly is `nonisolated(nonsending)` (SE-0461), and you will meet it in Xcode diagnostics and in Apple's own signatures long before you write it yourself:
      
      ```swift
      // From FoundationModels - note the annotation on Apple's API
      nonisolated(nonsending) public func respond(
          to prompt: Prompt, options: GenerationOptions
      ) async throws -> LanguageModelSession.Response<String>
      ```
      
      Three spellings, three behaviors:
      
      | Declaration | Runs on |
      |---|---|
      | `nonisolated func f() async` | caller's isolation (under `NonisolatedNonsendingByDefault`) |
      | `nonisolated(nonsending) func f() async` | caller's isolation, stated explicitly |
      | `@concurrent func f() async` | the global concurrent executor, always |
      
      Write `nonisolated(nonsending)` when you want the caller-inherits behavior to survive regardless of whether the module has the upcoming feature enabled - it is the portable way to say "do not hop".
      
      ## Isolated conformances (SE-0470)
      
      A recurring wall under default `MainActor` isolation: a `@MainActor` type needs to conform to a `nonisolated` protocol from a framework - `SCStreamOutput`, `SCStreamDelegate`, `NSObjectProtocol`-derived delegates. The conformance requirements are not main-actor-isolated, so the compiler rejects the isolated implementations.
      
      Swift 6.2 lets the conformance itself carry isolation:
      
      ```swift
      @MainActor
      final class CaptureController: NSObject, @MainActor SCStreamOutput {
          var frameCount = 0   // main-actor state, safe to touch below
      
          func stream(_ stream: SCStream, didOutputSampleBuffer buffer: CMSampleBuffer,
                      of type: SCStreamOutputType) {
              frameCount += 1
          }
      }
      ```
      
      The compiler then guarantees the conformance is only used from that actor - if something tries to use `CaptureController` as an `SCStreamOutput` from another isolation domain, that is the error, rather than every method body erroring.
      
      Caveat worth knowing before reaching for it: an isolated conformance cannot be used where the protocol is required to be `Sendable`-usable across domains. When a framework hands your delegate to a background queue, the right answer is still a `nonisolated` conformance whose body hops explicitly - isolated conformances solve the "this genuinely only ever runs on main" case, not the "I want to silence the error" case.
      
    • architecture.md 8.3 KB
      # Architecture Patterns for macOS Apps
      
      ## Table of Contents
      - SwiftUI + @Observable (Recommended Default)
      - MVVM with @Observable
      - The Composable Architecture (TCA)
      - Dependency Injection
      - Project Structure
      - Decision Guide
      
      ## SwiftUI + @Observable (Simple Apps)
      
      Best for small-medium apps, solo developers, or prototypes. Direct @Observable objects owned by views:
      
      ```swift
      @Observable
      final class ProjectManager {
          var projects: [Project] = []
          var selectedProject: Project?
          var searchText = ""
          private let store: ProjectStore
      
          var filteredProjects: [Project] {
              searchText.isEmpty ? projects : projects.filter {
                  $0.name.localizedStandardContains(searchText)
              }
          }
      
          init(store: ProjectStore = .shared) {
              self.store = store
          }
      
          func load() async throws {
              projects = try await store.fetchAll()
          }
      
          func create(name: String) async throws {
              let project = try await store.create(name: name)
              projects.append(project)
          }
      
          func delete(_ project: Project) async throws {
              try await store.delete(project)
              projects.removeAll { $0.id == project.id }
          }
      }
      
      struct ProjectsView: View {
          @State private var manager = ProjectManager()
      
          var body: some View {
              NavigationSplitView {
                  List(manager.filteredProjects, selection: $manager.selectedProject) { project in
                      Text(project.name)
                  }
                  .searchable(text: $manager.searchText)
                  .task { try? await manager.load() }
              } detail: {
                  if let project = manager.selectedProject {
                      ProjectDetailView(project: project)
                  }
              }
          }
      }
      ```
      
      ### Environment sharing
      ```swift
      // Root
      ContentView()
          .environment(manager)
      
      // Child views
      @Environment(ProjectManager.self) private var manager
      ```
      
      ## MVVM with @Observable (Medium Apps)
      
      Separate ViewModel per view for clearer responsibilities:
      
      ```swift
      @Observable
      final class ProjectListViewModel {
          var projects: [Project] = []
          var searchText = ""
          var isLoading = false
          var error: Error?
      
          private let service: ProjectServiceProtocol
      
          init(service: ProjectServiceProtocol = ProjectService()) {
              self.service = service
          }
      
          var filteredProjects: [Project] {
              guard !searchText.isEmpty else { return projects }
              return projects.filter { $0.name.localizedStandardContains(searchText) }
          }
      
          func load() async {
              isLoading = true
              defer { isLoading = false }
              do {
                  projects = try await service.fetchAll()
              } catch {
                  self.error = error
              }
          }
      
          func delete(_ project: Project) async {
              do {
                  try await service.delete(project.id)
                  projects.removeAll { $0.id == project.id }
              } catch {
                  self.error = error
              }
          }
      }
      
      struct ProjectListView: View {
          @State private var viewModel: ProjectListViewModel
      
          init(service: ProjectServiceProtocol = ProjectService()) {
              _viewModel = State(initialValue: ProjectListViewModel(service: service))
          }
      
          var body: some View {
              List(viewModel.filteredProjects) { project in
                  ProjectRow(project: project)
                      .swipeActions {
                          Button("Delete", role: .destructive) {
                              Task { await viewModel.delete(project) }
                          }
                      }
              }
              .searchable(text: $viewModel.searchText)
              .overlay {
                  if viewModel.isLoading { ProgressView() }
              }
              .task { await viewModel.load() }
          }
      }
      ```
      
      ## The Composable Architecture (TCA)
      
      For large apps requiring strict testability, modular composition, and predictable state management. Uses `pointfreeco/swift-composable-architecture`:
      
      ```swift
      import ComposableArchitecture
      
      @Reducer
      struct ProjectListFeature {
          @ObservableState
          struct State: Equatable {
              var projects: IdentifiedArrayOf<Project> = []
              var searchText = ""
              var isLoading = false
              @Presents var destination: Destination.State?
          }
      
          enum Action: BindableAction {
              case binding(BindingAction<State>)
              case onAppear
              case projectsLoaded([Project])
              case deleteProject(Project.ID)
              case destination(PresentationAction<Destination.Action>)
          }
      
          @Dependency(\.projectClient) var projectClient
      
          var body: some ReducerOf<Self> {
              BindingReducer()
              Reduce { state, action in
                  switch action {
                  case .onAppear:
                      state.isLoading = true
                      return .run { send in
                          let projects = try await projectClient.fetchAll()
                          await send(.projectsLoaded(projects))
                      }
      
                  case let .projectsLoaded(projects):
                      state.isLoading = false
                      state.projects = IdentifiedArray(uniqueElements: projects)
                      return .none
      
                  case let .deleteProject(id):
                      state.projects.remove(id: id)
                      return .run { _ in try await projectClient.delete(id) }
      
                  case .binding, .destination:
                      return .none
                  }
              }
          }
      
          @Reducer
          enum Destination {
              case detail(ProjectDetailFeature)
          }
      }
      ```
      
      ### Testing TCA
      ```swift
      @Test func loadProjects() async {
          let store = TestStore(initialState: ProjectListFeature.State()) {
              ProjectListFeature()
          } withDependencies: {
              $0.projectClient.fetchAll = { [Project(name: "Test")] }
          }
      
          await store.send(.onAppear) {
              $0.isLoading = true
          }
          await store.receive(\.projectsLoaded) {
              $0.isLoading = false
              $0.projects = [Project(name: "Test")]
          }
      }
      ```
      
      ## Dependency Injection
      
      ### Protocol-based (for MVVM)
      ```swift
      protocol ProjectServiceProtocol: Sendable {
          func fetchAll() async throws -> [Project]
          func create(name: String) async throws -> Project
          func delete(_ id: Project.ID) async throws
      }
      
      struct ProjectService: ProjectServiceProtocol { /* real impl */ }
      struct MockProjectService: ProjectServiceProtocol { /* test impl */ }
      ```
      
      ### TCA Dependencies
      ```swift
      struct ProjectClient: Sendable {
          var fetchAll: @Sendable () async throws -> [Project]
          var create: @Sendable (String) async throws -> Project
          var delete: @Sendable (Project.ID) async throws -> Void
      }
      
      extension ProjectClient: DependencyKey {
          static let liveValue = ProjectClient(
              fetchAll: { try await APIClient.shared.get("/projects") },
              create: { try await APIClient.shared.post("/projects", body: ["name": $0]) },
              delete: { try await APIClient.shared.delete("/projects/\($0)") }
          )
      
          static let testValue = ProjectClient(
              fetchAll: { [] },
              create: { Project(name: $0) },
              delete: { _ in }
          )
      }
      ```
      
      ## Project Structure
      
      ### Small-medium app
      ```
      MyApp/
      ├── MyAppApp.swift           # @main, scenes
      ├── Models/                  # Data models
      ├── Views/                   # SwiftUI views
      ├── ViewModels/              # @Observable view models (MVVM only)
      ├── Services/                # API clients, persistence
      └── Utilities/               # Extensions, helpers
      ```
      
      ### Large app (modular)
      ```
      MyApp/
      ├── App/                     # Entry point, scenes, commands
      ├── Features/
      │   ├── Projects/
      │   │   ├── ProjectListView.swift
      │   │   ├── ProjectDetailView.swift
      │   │   └── ProjectListViewModel.swift
      │   ├── Settings/
      │   └── Dashboard/
      ├── Core/
      │   ├── Models/
      │   ├── Services/
      │   └── Networking/
      ├── Shared/
      │   ├── Components/          # Reusable views
      │   └── Extensions/
      └── Resources/
      ```
      
      ## Decision Guide
      
      | Factor | @Observable | MVVM | TCA |
      |--------|-------------|------|-----|
      | Team size | 1-2 | 2-5 | 5+ |
      | Testability | Basic | Good | Excellent |
      | Boilerplate | Minimal | Low | Medium |
      | Learning curve | Low | Low | High |
      | State predictability | Good | Good | Excellent |
      | Navigation handling | SwiftUI native | SwiftUI native | Tree-based |
      | Dependencies | Protocol injection | Protocol injection | Built-in DI |
      
      **Rule of thumb**: Start with @Observable. Graduate to MVVM when you need testable view models. Use TCA when you need strict state management and modular composition.
      
    • async-patterns.md 8 KB
      # Async Patterns
      
      ## Table of Contents
      - AsyncSequence
      - AsyncStream
      - Observations (Swift 6.2)
      - Continuations
      - Clock & Duration
      - Debouncing & Throttling
      - Error Handling
      
      ## AsyncSequence
      
      Protocol for asynchronous iteration:
      
      ```swift
      // Consuming
      for await line in fileURL.lines {
          process(line)
      }
      
      // With transformations
      let validItems = items
          .compactMap { try? parse($0) }
          .filter { $0.isActive }
      
      for try await item in validItems {
          display(item)
      }
      
      // First element
      let first = await stream.first(where: { $0.isImportant })
      
      // Collect into array
      let all = try await stream.reduce(into: []) { $0.append($1) }
      ```
      
      ### Built-in AsyncSequences
      - `URL.lines` - Lines from a file
      - `URLSession.bytes(from:)` - Bytes from network
      - `NotificationCenter.notifications(named:)` - Notifications
      - `FileHandle.bytes` - Bytes from file handle
      
      ## AsyncStream
      
      Create custom async sequences:
      
      ```swift
      // Yield-based
      func priceUpdates(for symbol: String) -> AsyncStream<Price> {
          AsyncStream { continuation in
              let connection = WebSocket(url: priceURL(for: symbol))
      
              connection.onMessage = { data in
                  if let price = try? JSONDecoder().decode(Price.self, from: data) {
                      continuation.yield(price)
                  }
              }
      
              connection.onClose = {
                  continuation.finish()
              }
      
              continuation.onTermination = { _ in
                  connection.close()
              }
      
              connection.connect()
          }
      }
      
      // Usage
      for await price in priceUpdates(for: "AAPL") {
          updateChart(price)
      }
      ```
      
      ### Throwing variant
      ```swift
      func monitorSystem() -> AsyncThrowingStream<SystemEvent, Error> {
          AsyncThrowingStream { continuation in
              let monitor = SystemMonitor()
      
              monitor.onEvent = { event in
                  continuation.yield(event)
              }
      
              monitor.onError = { error in
                  continuation.finish(throwing: error)
              }
      
              monitor.onComplete = {
                  continuation.finish()
              }
      
              continuation.onTermination = { _ in
                  monitor.stop()
              }
      
              monitor.start()
          }
      }
      ```
      
      ### Buffering policy
      ```swift
      AsyncStream(bufferingPolicy: .bufferingNewest(10)) { continuation in
          // Only keeps latest 10 values if consumer is slow
      }
      
      // Options:
      // .unbounded - No limit (default)
      // .bufferingOldest(N) - Keep first N, drop new
      // .bufferingNewest(N) - Keep latest N, drop old
      ```
      
      ## Observations (Swift 6.2)
      
      Stream transactional state changes from `@Observable` types:
      
      ```swift
      import Observation
      
      @Observable
      class DownloadState {
          var bytesReceived: Int = 0
          var totalBytes: Int = 0
          var isComplete = false
      
          var progress: Double {
              totalBytes > 0 ? Double(bytesReceived) / Double(totalBytes) : 0
          }
      }
      
      // Stream as AsyncSequence. `Observations` uses a closure initializer;
      // there is NO `Observations(of:)` factory and NO variadic-keypath tracking
      // form. SE-0475 rejected both. Apple platforms: macOS 26+ / iOS 26+.
      let state = DownloadState()
      let progressStream = Observations { state.progress }
      for await value in progressStream {
          progressBar.value = value
          if state.isComplete { break }
      }
      ```
      
      Key behavior:
      - Groups synchronous changes into transactions
      - Transaction ends at next `await` that suspends
      - Avoids redundant updates (if you change 3 properties synchronously, one update fires)
      - Works with any `@Observable` type
      
      ### Observing multiple properties
      ```swift
      // Project the values you want into a tuple inside the closure.
      let progress = Observations { (state.bytesReceived, state.totalBytes) }
      for await (bytes, total) in progress {
          updateProgress(bytes: bytes, total: total)
      }
      ```
      
      Source: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0475-observed.md. Docs: https://developer.apple.com/documentation/observation/observations
      
      ### Advanced Observation Tracking (SE-0506, Swift 6.4)
      
      SE-0506 moved to **Implemented (Swift 6.4)** on 2026-07-28 - not in the pinned 6.3.3 toolchain, but it changes what the right answer will be. It adds two top-level interfaces:
      
      - `withObservationTracking(options:)` - the existing function plus an options parameter, so you can control tracking behaviour instead of getting exactly one fire-once callback.
      - `withContinuousObservationTracking` - a callback-based form with behaviour similar to `Observations`, for the common case where you want to keep observing rather than re-arming the tracker by hand.
      
      Until 6.4 ships, `Observations { }` remains the way to stream changes, and plain `withObservationTracking` remains fire-once - if you are re-registering it inside its own `onChange` closure today, that is the pattern SE-0506 replaces. Source: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0506-advanced-observation-tracking.md
      
      ## Continuations
      
      Bridge callback-based APIs to async/await:
      
      ```swift
      // Checked continuation (with runtime checks for misuse)
      func fetchLocation() async throws -> CLLocation {
          try await withCheckedThrowingContinuation { continuation in
              locationManager.requestLocation { result in
                  switch result {
                  case .success(let location):
                      continuation.resume(returning: location)
                  case .failure(let error):
                      continuation.resume(throwing: error)
                  }
                  // WARNING: Must resume exactly once. Double-resume crashes.
              }
          }
      }
      
      // Unsafe continuation (no runtime checks, slightly faster)
      func readSensor() async -> SensorData {
          await withUnsafeContinuation { continuation in
              sensor.read { data in
                  continuation.resume(returning: data)
              }
          }
      }
      ```
      
      **Rules:**
      - Must resume exactly once
      - `withCheckedContinuation` catches double-resume at runtime
      - `withUnsafeContinuation` for performance-critical paths
      
      ## Clock & Duration
      
      ```swift
      // Sleep
      try await Task.sleep(for: .seconds(2))
      try await Task.sleep(for: .milliseconds(500))
      try await Task.sleep(until: .now + .seconds(5), clock: .continuous)
      
      // Measure
      let clock = ContinuousClock()
      let elapsed = try await clock.measure {
          try await performWork()
      }
      print("Took \(elapsed)") // e.g., "1.234 seconds"
      
      // Timeout pattern
      func withTimeout<T>(
          _ duration: Duration,
          operation: @Sendable () async throws -> T
      ) async throws -> T {
          try await withThrowingTaskGroup(of: T.self) { group in
              group.addTask { try await operation() }
              group.addTask {
                  try await Task.sleep(for: duration)
                  throw TimeoutError()
              }
      
              let result = try await group.next()!
              group.cancelAll()
              return result
          }
      }
      ```
      
      ## Debouncing & Throttling
      
      ```swift
      // Debounce search input
      actor SearchDebouncer {
          private var currentTask: Task<Void, Never>?
      
          func debounce(delay: Duration = .milliseconds(300), action: @Sendable @escaping () async -> Void) {
              currentTask?.cancel()
              currentTask = Task {
                  try? await Task.sleep(for: delay)
                  guard !Task.isCancelled else { return }
                  await action()
              }
          }
      }
      
      // Usage in SwiftUI
      struct SearchView: View {
          @State private var query = ""
          @State private var results: [Item] = []
          private let debouncer = SearchDebouncer()
      
          var body: some View {
              TextField("Search", text: $query)
                  .onChange(of: query) { _, newValue in
                      Task {
                          await debouncer.debounce {
                              let items = try? await search(newValue)
                              await MainActor.run { results = items ?? [] }
                          }
                      }
                  }
          }
      }
      ```
      
      ## Error Handling
      
      ```swift
      // Typed throws (Swift 6.0+)
      func parse(_ input: String) throws(ParseError) -> AST {
          guard !input.isEmpty else {
              throw .emptyInput
          }
          // ...
      }
      
      // Catch specific typed errors
      do throws(ParseError) {
          let ast = try parse(input)
      } catch .emptyInput {
          showEmptyMessage()
      } catch .invalidSyntax(let line) {
          highlightError(at: line)
      }
      
      // In async context
      func fetchAndParse() async throws(AppError) -> Model {
          let data = try await fetch()
          return try parse(data)
      }
      ```
      
    • cloudkit-sync.md 4.9 KB
      # CloudKit Sync
      
      ## Table of Contents
      - Setup
      - Configuration Options
      - Model Constraints
      - Conflict Resolution
      - Debugging Sync
      - Sharing
      
      ## Setup
      
      ### 1. Enable capabilities in Xcode
      - Signing & Capabilities > + Capability > iCloud
      - Check "CloudKit"
      - Select or create a container (e.g., `iCloud.com.yourapp`)
      - Also add "Background Modes" > "Remote notifications" for push-based sync
      
      ### 2. Configure container
      ```swift
      // Default: auto-syncs all models to private CloudKit database
      @main
      struct MyApp: App {
          var body: some Scene {
              WindowGroup {
                  ContentView()
              }
              .modelContainer(for: [Project.self, Task.self])
              // CloudKit sync happens automatically if iCloud capability is enabled
          }
      }
      ```
      
      ### 3. Explicit configuration
      ```swift
      let config = ModelConfiguration(
          cloudKitDatabase: .automatic // Uses default container
      )
      
      // Or specify database type
      let privateConfig = ModelConfiguration(
          cloudKitDatabase: .private("iCloud.com.yourapp")
      )
      
      // Disable sync for specific config
      let localConfig = ModelConfiguration(
          cloudKitDatabase: .none
      )
      
      // Mix synced and local storage
      let syncedConfig = ModelConfiguration(
          "Synced",
          schema: Schema([Project.self]),
          cloudKitDatabase: .automatic
      )
      let localConfig = ModelConfiguration(
          "Local",
          schema: Schema([CacheItem.self]),
          cloudKitDatabase: .none
      )
      
      let container = try ModelContainer(
          for: Project.self, CacheItem.self,
          configurations: syncedConfig, localConfig
      )
      ```
      
      ## Model Constraints for CloudKit
      
      CloudKit imposes stricter requirements on models:
      
      ```swift
      @Model
      final class SyncableProject {
          // All properties must have default values or be optional
          var name: String = ""
          var description: String?
          var createdAt: Date = .now
          var isArchived: Bool = false
      
          // Relationships must be optional
          @Relationship(deleteRule: .nullify)
          var tasks: [SyncableTask]? // Optional array
      
          // NO unique constraints (not supported with CloudKit)
          // @Attribute(.unique) var slug: String // WRONG
      
          init(name: String) {
              self.name = name
          }
      }
      ```
      
      ### Rules
      - All properties must have defaults or be optional
      - No `@Attribute(.unique)` constraints
      - Relationships must be optional
      - Delete rule `.deny` not recommended (can fail silently)
      - Avoid very large `Data` properties (CloudKit has size limits)
      - Model class name becomes the CloudKit record type
      
      ## Conflict Resolution
      
      CloudKit uses last-writer-wins by default, and SwiftData handles most merge work transparently. There is **no public SwiftData API** that surfaces conflicts as `NSMergeConflict` objects on `ModelContext.didSave` — that's a Core Data pattern that does not apply here. If you need programmatic conflict inspection and resolution policies today, you must drop down to `NSPersistentCloudKitContainer` + Core Data.
      
      For SwiftData + CloudKit apps, design models to minimize conflicts (see Best Practices below) rather than relying on post-hoc conflict handlers.
      
      ### Best practices
      - Use server timestamps for ordering (`createdAt`, `updatedAt`)
      - Design models to minimize conflicts (separate frequently-changed properties)
      - Expect eventual consistency (sync is not instant)
      - Test with multiple devices/simulators
      
      ## Debugging Sync
      
      ### Enable CloudKit logging
      ```bash
      # In Xcode scheme, add launch argument:
      -com.apple.CoreData.CloudKitDebug 1
      
      # Verbose logging:
      -com.apple.CoreData.CloudKitDebug 3
      ```
      
      ### CloudKit Dashboard
      1. Go to https://icloud.developer.apple.com
      2. Select your container
      3. View records, zones, subscriptions
      4. Check for errors in the "Logs" section
      
      ### Common issues
      
      **Sync not starting:**
      - Verify iCloud is signed in (System Settings > Apple Account)
      - Check CloudKit container identifier matches capability
      - Ensure network connectivity
      - Check Console.app for CloudKit errors
      
      **Data not appearing on other devices:**
      - Sync is eventually consistent (can take seconds to minutes)
      - Verify both devices use the same iCloud account
      - Check that the model schema matches on both versions
      - Look for CKError codes in logs
      
      **Schema mismatch:**
      - After adding new properties, deploy schema to CloudKit Dashboard
      - Development environment: auto-deployed
      - Production: must manually deploy via Dashboard
      
      ## Sharing (CloudKit Sharing)
      
      Share records with other iCloud users:
      
      ```swift
      // Note: Direct SwiftData sharing APIs are limited.
      // For advanced sharing, bridge to Core Data's NSPersistentCloudKitContainer.
      
      // Basic approach: Share via a unique code/link
      @Model
      final class SharedProject {
          var name: String
          var shareCode: String? // Generate unique code for sharing
          var ownerID: String    // iCloud user record ID
      
          init(name: String, ownerID: String) {
              self.name = name
              self.ownerID = ownerID
          }
      }
      ```
      
      For full CloudKit sharing with CKShare records, consider using Core Data's `NSPersistentCloudKitContainer` alongside SwiftData, or build a custom sharing layer using CloudKit framework directly.
      
    • container-context.md 10.2 KB
      # ModelContainer & ModelContext
      
      ## Table of Contents
      - ModelContainer Configuration
      - ModelContext Operations
      - Background Contexts
      - Batch Operations
      - Undo/Redo
      - Previews & Testing
      
      ## ModelContainer Configuration
      
      ### Basic setup
      ```swift
      // Default (SQLite, app's default location)
      .modelContainer(for: [Project.self, Task.self])
      
      // With configuration
      let config = ModelConfiguration(
          "MyDB",
          schema: Schema([Project.self, Task.self]),
          url: .applicationSupportDirectory.appending(path: "myapp.store"),
          allowsSave: true
      )
      let container = try ModelContainer(
          for: Project.self, Task.self,
          configurations: config
      )
      ```
      
      ### Multiple stores
      ```swift
      // User data in one store, reference data in another
      let userData = ModelConfiguration(
          "UserData",
          schema: Schema([Project.self, Task.self])
      )
      let referenceData = ModelConfiguration(
          "ReferenceData",
          schema: Schema([Template.self, Category.self]),
          url: bundledDBURL,
          allowsSave: false
      )
      
      let container = try ModelContainer(
          for: Project.self, Task.self, Template.self, Category.self,
          configurations: userData, referenceData
      )
      ```
      
      ### In-memory (previews/testing)
      ```swift
      let config = ModelConfiguration(isStoredInMemoryOnly: true)
      let container = try ModelContainer(
          for: Project.self,
          configurations: config
      )
      ```
      
      ### Injecting container
      ```swift
      // Scene level
      WindowGroup {
          ContentView()
      }
      .modelContainer(container)
      
      // Or in a view
      ContentView()
          .modelContainer(for: Project.self)
      ```
      
      ## ModelContext Operations
      
      ### CRUD
      ```swift
      @Environment(\.modelContext) private var context
      
      // Create
      let project = Project(name: "New Project")
      context.insert(project)
      
      // Read
      let descriptor = FetchDescriptor<Project>(
          predicate: #Predicate { !$0.isArchived },
          sortBy: [SortDescriptor(\.name)]
      )
      let projects = try context.fetch(descriptor)
      
      // Update (just modify properties - auto-tracked)
      project.name = "Updated Name"
      project.updatedAt = .now
      
      // Delete
      context.delete(project)
      
      // Save explicitly
      try context.save()
      
      // Check for unsaved changes
      if context.hasChanges {
          try context.save()
      }
      ```
      
      ### Auto-save
      By default, ModelContext auto-saves on the next run loop iteration. To control:
      ```swift
      // Disable auto-save
      context.autosaveEnabled = false
      
      // Manual save
      try context.save()
      
      // Rollback unsaved changes
      context.rollback()
      ```
      
      ### Fetch count
      ```swift
      let count = try context.fetchCount(
          FetchDescriptor<Project>(predicate: #Predicate { $0.isArchived })
      )
      ```
      
      ### Fetch with pagination
      ```swift
      var descriptor = FetchDescriptor<Project>(sortBy: [SortDescriptor(\.name)])
      descriptor.fetchLimit = 20
      descriptor.fetchOffset = page * 20
      
      let page = try context.fetch(descriptor)
      ```
      
      ## Background Contexts
      
      Perform heavy operations off the main thread:
      
      ```swift
      // Create background context from container
      let container = // your ModelContainer
      let backgroundContext = ModelContext(container)
      
      // Use in a detached task
      @concurrent
      func importData(_ data: [ImportRow]) async throws {
          let context = ModelContext(container)
          context.autosaveEnabled = false
      
          for row in data {
              let item = Item(from: row)
              context.insert(item)
          }
      
          try context.save()
      }
      ```
      
      ### ModelActor (actor-isolated context)
      ```swift
      @ModelActor
      actor DataImporter {
          // modelContext and modelContainer are auto-provided
      
          func importItems(_ items: [ImportData]) throws {
              for item in items {
                  let model = Project(name: item.name)
                  modelContext.insert(model)
              }
              try modelContext.save()
          }
      
          func countProjects() throws -> Int {
              try modelContext.fetchCount(FetchDescriptor<Project>())
          }
      }
      
      // Usage
      let importer = DataImporter(modelContainer: container)
      try await importer.importItems(data)
      ```
      
      ## Batch Operations
      
      ### Batch delete
      ```swift
      // Delete all archived projects
      try context.delete(
          model: Project.self,
          where: #Predicate { $0.isArchived }
      )
      ```
      
      ### Enumerate for bulk processing
      
      `FetchDescriptor` has **no** `fetchBatchSize` property - batching is a parameter on `enumerate` itself (default 5000):
      
      ```swift
      let descriptor = FetchDescriptor<Project>()
      
      try context.enumerate(descriptor, batchSize: 100) { project in
          project.lastSyncedAt = .now
      }
      try context.save()
      ```
      
      Full signature: `enumerate(_:batchSize:allowEscapingMutations:block:)`. Set `allowEscapingMutations: true` only when the block intentionally mutates objects outside the enumerated set - otherwise SwiftData traps on escaping mutations, which is the behavior you want as a safety net.
      
      ## Undo/Redo
      
      ```swift
      // Enable undo manager
      let container = try ModelContainer(for: Project.self)
      container.mainContext.undoManager = UndoManager()
      
      // In SwiftUI
      @Environment(\.undoManager) private var undoManager
      
      // Undo/redo
      undoManager?.undo()
      undoManager?.redo()
      
      // Check availability
      undoManager?.canUndo
      undoManager?.canRedo
      ```
      
      Toolbar integration:
      ```swift
      .toolbar {
          ToolbarItem {
              Button(action: { undoManager?.undo() }) {
                  Label("Undo", systemImage: "arrow.uturn.backward")
              }
              .disabled(!(undoManager?.canUndo ?? false))
          }
          ToolbarItem {
              Button(action: { undoManager?.redo() }) {
                  Label("Redo", systemImage: "arrow.uturn.forward")
              }
              .disabled(!(undoManager?.canRedo ?? false))
          }
      }
      ```
      
      ## Previews & Testing
      
      ### Preview container
      ```swift
      struct ProjectListView_Previews: PreviewProvider {
          static var previews: some View {
              let config = ModelConfiguration(isStoredInMemoryOnly: true)
              let container = try! ModelContainer(for: Project.self, configurations: config)
      
              // Seed data
              let context = container.mainContext
              let project = Project(name: "Sample Project")
              context.insert(project)
      
              return ProjectListView()
                  .modelContainer(container)
          }
      }
      
      // Or with @Previewable macro
      #Preview {
          @Previewable @State var container = {
              let config = ModelConfiguration(isStoredInMemoryOnly: true)
              let container = try! ModelContainer(for: Project.self, configurations: config)
              let project = Project(name: "Preview Project")
              container.mainContext.insert(project)
              return container
          }()
      
          ProjectListView()
              .modelContainer(container)
      }
      ```
      
      ### Testing
      ```swift
      @Suite("Project Repository")
      struct ProjectRepositoryTests {
          let container: ModelContainer
      
          init() throws {
              container = try ModelContainer(
                  for: Project.self, Task.self,
                  configurations: ModelConfiguration(isStoredInMemoryOnly: true)
              )
          }
      
          @Test func fetchActiveProjects() throws {
              let context = ModelContext(container)
              context.insert(Project(name: "Active"))
      
              let archived = Project(name: "Old")
              archived.isArchived = true
              context.insert(archived)
              try context.save()
      
              let descriptor = FetchDescriptor<Project>(
                  predicate: #Predicate { !$0.isArchived }
              )
              let active = try context.fetch(descriptor)
              #expect(active.count == 1)
              #expect(active.first?.name == "Active")
          }
      }
      ```
      
      ## History tracking
      
      SwiftData records a transaction history you can replay - the basis for "what changed since I last looked" across processes, app relaunches, or CloudKit pulls. This is a whole API surface distinct from `@Query` observation.
      
      ```swift
      import SwiftData
      
      // Fetch every transaction newer than a stored token
      func changes(since token: (any HistoryToken)?, context: ModelContext) throws -> [DefaultHistoryTransaction] {
          var descriptor = HistoryDescriptor<DefaultHistoryTransaction>()
          if let token {
              descriptor.predicate = #Predicate { $0.token > token }
          }
          return try context.fetchHistory(descriptor)
      }
      
      for transaction in try changes(since: lastToken, context: context) {
          for change in transaction.changes {
              switch change {
              case .insert(let inserted):  handleInsert(inserted.changedPersistentIdentifier)
              case .update(let updated):   handleUpdate(updated.changedPersistentIdentifier)
              case .delete(let deleted):   handleDelete(deleted.tombstone)
              }
          }
          lastToken = transaction.token   // persist this
      }
      ```
      
      Key types: `HistoryDescriptor`, `HistoryToken` (Comparable + Codable - persist it between launches), `HistoryTransaction`, and the change types `HistoryInsert` / `HistoryUpdate` / `HistoryDelete` with their `Default*` concrete variants.
      
      A delete leaves a **tombstone** rather than the object - the row is gone, so only the values you marked `@Attribute(.preserveValueOnDeletion)` survive in the tombstone. If you need to know *which* record was deleted (to mirror the delete to a server, say), mark that identifier with `.preserveValueOnDeletion` up front or the tombstone will not carry it.
      
      Prune consumed history so the store does not grow without bound:
      
      ```swift
      try context.deleteHistory(HistoryDescriptor<DefaultHistoryTransaction>(
          predicate: #Predicate { $0.token < cutoffToken }
      ))
      ```
      
      ## Custom data stores
      
      `DataStore` lets you back SwiftData with something other than its default SQLite store - a JSON file, a remote service, an in-memory fixture - while keeping `@Model`, `@Query`, and `ModelContext` unchanged.
      
      ```swift
      final class JSONStore: DataStore {
          typealias Configuration = JSONStoreConfiguration
          typealias Snapshot = DefaultSnapshot
      
          var identifier: String
          var schema: Schema
          var configuration: JSONStoreConfiguration
      
          func fetch<T>(_ request: DataStoreFetchRequest<T>) throws -> DataStoreFetchResult<T, DefaultSnapshot>
          where T: PersistentModel { /* read + decode */ }
      
          func save(_ request: DataStoreSaveChangesRequest<DefaultSnapshot>) throws
              -> DataStoreSaveChangesResult<DefaultSnapshot> { /* encode + write */ }
      }
      
      // Wire it up
      let container = try ModelContainer(
          for: Project.self,
          configurations: JSONStoreConfiguration(name: "Local", schema: schema, fileURL: url)
      )
      ```
      
      Adopt `DataStoreBatching` to support batched fetches, and `HistoryProviding` if the store should serve the history API above. Related: `DataStoreError`, `DataStoreSnapshotCodingKey`.
      
      This is the supported route for a read-only or remote-backed store. It is a substantial amount of code - reach for it only when the default store genuinely cannot work, not to avoid a migration.
      
    • core-audio-tap.md 18.5 KB
      # CoreAudio Process Tap (CATap)
      
      `CATapDescription` + `AudioHardwareCreateProcessTap` + aggregate device (macOS 14.2+) captures a specific process's audio output directly from CoreAudio, without ScreenCaptureKit. Lower latency and a separate TCC permission from Screen Recording. Used by RecordKit, Chromium's `CatapAudioInputStream`, AudioCap, audiotee, and others.
      
      > **Production reality check before you migrate.** An internal call-recorder shipped CATap and reverted to display-wide SCStream after three distinct silent-recording bugs in five days. The root cause is **structural to CATap**: the aggregate-device IO proc's time base is tied to its subdevice clocks — Apple's `AudioHardwareAggregateDevice` reference states it "synchronizes the clocks of its subdevices and subtaps when running IO" ([docs](https://developer.apple.com/documentation/coreaudio/audiohardwareaggregatedevice)) — so when the default output clock is idle, rate-pinned (Bluetooth HFP), or stalled, the tap has audio to deliver but no ticks to deliver it on. A buffer-arrival watchdog restart hits the same idle clock and exhausts its restart budget. These failure modes are not Apple-documented as such, but the **Chromium audio team has independently shipped listeners for all three**: alive-state + default-output-change listeners ([crbug 436110597](https://chromium.googlesource.com/chromium/src/+/a4545b03c738f0a84468a7f70066c85f80d6b23d)), sample-rate-change error propagation ([crbug 441729516](https://chromium.googlesource.com/chromium/src/+/e17d528fa243625faeebf4ec1ff500b628f1fd74)), and device-change stream-restart ([crbug 442993607](https://chromium.googlesource.com/chromium/src/+/0cf5a46b9db9820785be8ddaf6ecd62830775501%5E%21), which notes "*CatapAudioInputStream will ignore the change and continue to capture the original default device*"). Display-wide SCStream's clock comes from the OS-composited mix, decoupled from any specific hardware output device, and has a multi-week production track record on the same workload with zero silent-recording reports. Read "When NOT to use CATap" below before choosing CATap for long-running recordings.
      
      ## Table of Contents
      - When NOT to Use CATap
      - When to Use CATap vs SCStream
      - Tap-Only Aggregate Pattern (HFP-safe)
      - Clock Fragility: What the Tap-Only Pattern Does NOT Fix
      - The Rate-Change Listener Anti-Pattern
      - Interleaved-Stereo Frame-Count Trap
      - IO Proc Isolation and `assumeIsolated`
      - TCC / Permissions for CATap
      
      ## When NOT to Use CATap
      
      Specifically these scenarios have produced silent recordings in production:
      
      1. **Bluetooth HFP path** (AirPods on a call, Mac as the audio device). Aggregate pins to 24 kHz; unless your aggregate is tap-only (see below) the IO proc never fires. Tap-only aggregate fixes *this* specific symptom but not the broader class.
      2. **Idle default output device.** IO proc stops emitting during long silences (observed ~18 s post-call tail drops).
      3. **In-page audio routing** (Chrome Meet / Zoom web picker sending call audio to a non-default output while the default is idle). IO proc on the default device never ticks; the hour-long recording ends up mic-only.
      4. **Long-duration unattended recordings** where any of the above can happen and a silent file is worse than a failed start.
      
      For these workloads, display-wide `SCStream` is more robust even with the cosmetic overhead of a still-running video pipeline (see `screen-capture-audio.md`). macOS 26.1+ also grants audio capture with the Screen Recording TCC grant, neutralizing CATap's permission-UX advantage.
      
      ## When to Use CATap vs SCStream
      
      | Need | CATap | Display-wide SCStream |
      |------|-------|-----------------------|
      | Per-process audio isolation | Yes (target PID) | No (OS-composited mix, exclude by app via filter) |
      | Survives idle default-output clock | **No** (structural) | Yes |
      | Survives Bluetooth HFP on default output | Only with tap-only aggregate | Yes |
      | Survives in-page audio re-routing | **No** (structural) | Yes |
      | Cosmetic Console.app noise | None | `stream output NOT found. Dropping frame` (cosmetic) |
      | Hidden video-pipeline CPU cost | None | Minor (mitigate with 2x2, `timescale: .max`) |
      | TCC permission on macOS 26.1+ | Screen Recording (unified) | Screen Recording |
      | TCC permission on macOS 14/15 | Separate "System Audio Recording" | Screen Recording |
      | Lower capture latency | Yes (~few ms) | No (SCStream sample-handler buffering ~20-50 ms) |
      
      **Short take**: if you need sub-20 ms capture latency for real-time AEC or live analysis, CATap is the only option and you accept the clock-fragility - pair it with a buffer-arrival watchdog and validate against HFP / idle-output / in-page-routing scenarios before shipping. If you're writing to disk for later playback/transcription (call recording, lecture capture, meeting archival), the latency argument doesn't apply and display-wide SCStream is the safer default.
      
      ## Tap-Only Aggregate Pattern (HFP-safe)
      
      The canonical recipe: build an aggregate device that contains **only the tap** (no physical output subdevice). When the output device changes - headphones plug in, AirPods enter HFP at 24 kHz - the tap stays at its own sample rate and keeps delivering frames.
      
      **Why**: including the output device as a subdevice (`kAudioAggregateDeviceMainSubDeviceKey` + `kAudioAggregateDeviceSubDeviceListKey`) locks the aggregate to the output's rate. When AirPods switch to 24 kHz mono HFP, the 48 kHz tap and the aggregate disagree, IO proc stops firing, and `AudioObjectSetPropertyData` will report `noErr` while silently doing nothing.
      
      ```swift
      import CoreAudio
      import AudioToolbox
      
      func makeAggregate(tapID: AudioObjectID, name: String, uid: String) throws -> AudioObjectID {
          let description: [String: Any] = [
              kAudioAggregateDeviceNameKey: name,
              kAudioAggregateDeviceUIDKey: uid,
              kAudioAggregateDeviceIsPrivateKey: 1,
              kAudioAggregateDeviceIsStackedKey: 0,
              // Tap-only. DO NOT include kAudioAggregateDeviceMainSubDeviceKey
              // or kAudioAggregateDeviceSubDeviceListKey - they lock the aggregate
              // to the output device's rate and break under HFP.
              kAudioAggregateDeviceTapListKey: [[
                  kAudioSubTapUIDKey: tapUID(for: tapID),
                  // Must be true: compensates for tap-vs-device clock drift.
                  kAudioSubTapDriftCompensationKey: true,
              ]],
          ]
      
          var aggregateID: AudioObjectID = 0
          let status = AudioHardwareCreateAggregateDevice(
              description as CFDictionary, &aggregateID
          )
          guard status == noErr else { throw CATapError.create(status) }
          return aggregateID
      }
      ```
      
      Reference: [graphaelli/audiotap](https://github.com/graphaelli/audiotap), RecordKit v0.78.0.
      
      ### Don't omit `kAudioSubTapDriftCompensationKey: true`
      
      Without drift compensation, CoreAudio resamples on every IO cycle to reconcile the tap's clock with the aggregate's, producing **periodic artifacts in all system audio** — audible as crackling during normal playback (music, calls) whenever the tap is running, and as pitch drift in long recordings. Confirmed in production by [Omi PR #6489](https://github.com/BasedHardware/omi/pull/6489): "*setting `kAudioSubTapDriftCompensationKey` … tells CoreAudio to reconcile clocks at the sub-tap level … the workaround is no longer needed.*" Keep it on.
      
      ## Clock Fragility: What the Tap-Only Pattern Does NOT Fix
      
      The tap-only aggregate fixes the HFP rate-pinning symptom (IO proc never fires when AirPods go to 24 kHz and the aggregate was locked to 48 kHz). It does **not** fix the underlying architectural issue:
      
      > Apple's [`AudioHardwareAggregateDevice`](https://developer.apple.com/documentation/coreaudio/audiohardwareaggregatedevice) "synchronizes the clocks of its subdevices and subtaps when running IO." When the system default output device's hardware clock is idle (no app is actively playing audio), rate-pinned (HFP transport flip), or stalled (DeviceIsAlive flapping, USB hub hiccup), the aggregate's IO proc receives no ticks and no buffer-delivery callbacks. The tap has samples to deliver; the scheduler has no timing source to hand them over on. Apple does not document this specific failure mode, but **Chromium has shipped listeners for all three transitions** (alive, default-output-change, sample-rate-change): [crbug 436110597](https://chromium.googlesource.com/chromium/src/+/a4545b03c738f0a84468a7f70066c85f80d6b23d), [441729516](https://chromium.googlesource.com/chromium/src/+/e17d528fa243625faeebf4ec1ff500b628f1fd74), [442993607](https://chromium.googlesource.com/chromium/src/+/0cf5a46b9db9820785be8ddaf6ecd62830775501%5E%21).
      
      Observed production failures (internal recorder + Chromium-mirrored classes):
      
      - Post-call tail drops of ~18 s when nothing plays after the call ends.
      - Full-hour mic-only recordings when a web app routes call audio to a non-default output while the default output stays silent. Chromium confirms the class verbatim: "*CatapAudioInputStream will ignore the change and continue to capture the original default device*" ([commit 0cf5a46b](https://chromium.googlesource.com/chromium/src/+/0cf5a46b9db9820785be8ddaf6ecd62830775501%5E%21)). Chrome's `kMacCatapCaptureAllDevices` feature flag exists specifically to work around this by capturing all outputs rather than just the default.
      - Watchdog restart + reinstall hits the same idle clock, burns the restart budget (e.g. 3 restarts / 30 s), and converts a recoverable stall into a terminated recording.
      
      If you ship CATap for long-running recordings, mandatory:
      1. **Buffer-arrival watchdog** on the IO-proc delivery path with a trip threshold appropriate for your domain (2 s is reasonable for calls; longer for music). Treat buffer absence as the failure mode.
      2. **Layered signal sources** — Chromium's pattern: listen on `kAudioDevicePropertyDeviceIsAlive` (tap), `kAudioHardwarePropertyDefaultOutputDevice` (system), and `kAudioDevicePropertyNominalSampleRate` (aggregate). Still run the watchdog as the universal safety net — the listeners can miss same-format / same-rate transitions.
      3. **Validation scenarios** before shipping: AirPods HFP on a call, idle default output for >30 s, in-page audio router on Chrome Meet / Zoom web, USB mic unplug-replug, virtual audio device (BlackHole / Loopback) crashing mid-recording.
      4. **Fallback path** — decide in advance what happens when the watchdog trips: restart the aggregate (may re-trip immediately), switch to SCStream, or fail loudly. Silently losing audio is the worst outcome.
      
      ## The Rate-Change Listener Anti-Pattern
      
      Tempting: subscribe to `kAudioDevicePropertyNominalSampleRate` on the tap and re-configure when the rate changes.
      
      Don't. Calling `AudioObjectSetPropertyData` from inside the listener re-fires the listener, producing an infinite notification storm. Observed in practice: 49 iterations before CoreAudio throttled, with no rate ever converging.
      
      ```swift
      // DO NOT DO THIS
      let listener: AudioObjectPropertyListenerBlock = { _, _ in
          // This call re-fires the listener.
          AudioObjectSetPropertyData(tapID, &addr, 0, nil, size, &newRate)
      }
      AudioObjectAddPropertyListenerBlock(tapID, &rateAddr, queue, listener)
      ```
      
      Two safe alternatives:
      1. **Linear resampling inside the IO proc** - each buffer is already annotated with its source format; resample to the writer's fixed target rate (e.g. 48 kHz) on the hot path. Same technique used for mic input.
      2. **Tap-only aggregate with drift compensation** (above) - the compensation key handles small drift automatically, so the rate-change handler is unnecessary.
      
      ## Interleaved-Stereo Frame-Count Trap
      
      CATap delivers 2-channel audio as **interleaved** (`interleaved: true`, `mFormatFlags & kAudioFormatFlagIsNonInterleaved == 0`). When wrapping into a `CMSampleBuffer` via a helper like `asSampleBuffer(sampleCount:)`, passing `pcmBuffer.frameLength` as the sample count produces a track that plays back at **half the expected duration** - `CMSampleTimingInfo` misinterprets interleaved pairs as two samples.
      
      ```swift
      // WRONG for interleaved stereo: resulting track is 50% of real duration
      let sb = pcmBuffer.asSampleBuffer(sampleCount: Int(pcmBuffer.frameLength))
      
      // RIGHT: for interleaved stereo, frame count is bytes / bytesPerFrame / channels
      let bytesPerFrame = Int(pcmBuffer.format.streamDescription.pointee.mBytesPerFrame)
      let frameCount = pcmBuffer.byteLength / bytesPerFrame
      // frameCount is now the true sample (time-domain) count.
      ```
      
      Rule of thumb: if the source format is interleaved, compute frame count from the byte length of the block buffer divided by `mBytesPerFrame`, not from `frameLength`.
      
      ## CFString Property Values Follow the Create Rule
      
      Reading a `CFString`-typed property (e.g. `kAudioDevicePropertyDeviceUID`, `kAudioObjectPropertyName`) with `AudioObjectGetPropertyData` returns an object with a **+1 retain count** - despite "Get" in the function name, the value follows the Create Rule and the caller owns it. Apple's `AudioHardware.h` states "*The caller is responsible for releasing the returned CFObject*". In Swift, take it with `takeRetainedValue()`, not `takeUnretainedValue()`; the latter leaks nothing but under-retains, so the string can be freed out from under you.
      
      ```swift
      var cfName: Unmanaged<CFString>?
      var size = UInt32(MemoryLayout<Unmanaged<CFString>?>.size)
      let status = AudioObjectGetPropertyData(deviceID, &addr, 0, nil, &size, &cfName)
      let name = cfName?.takeRetainedValue() as String?  // Create Rule: retained
      ```
      
      ## IO Proc Isolation and `assumeIsolated`
      
      The IO proc closure returned from `AudioHardwareCreateProcessTapWithAggregateDevice` / `makeIOProcBlock` runs on CoreAudio's **real-time thread**. It must stay `nonisolated` - do not make it actor-isolated or `@MainActor`. Allocation, locks, `Task {}`, and `AsyncStream.yield()` are all unsafe there.
      
      Pattern: IO proc `memcpy`s into a pre-allocated staging buffer, then dispatches (via `audioQueue.async`) for off-RT processing. The dispatched block can then synchronously `assumeIsolated` back into the actor to touch actor state.
      
      ```swift
      actor AudioRecorder {
          let audioQueue = DispatchSerialQueue(label: "audio")
          nonisolated var unownedExecutor: UnownedSerialExecutor {
              audioQueue.asUnownedSerialExecutor()
          }
      
          // IO proc: nonisolated, no allocation, no Task, no AsyncStream.
          nonisolated func makeIOProc() -> AudioDeviceIOBlock {
              return { [weak self] _, inputData, _, _, _ in
                  guard let self else { return }
                  // Copy out. No actor state access here.
                  let frames = stageBuffer(from: inputData)
                  // Hand off to audio queue - assumeIsolated on the other side.
                  audioQueue.async {
                      self.assumeIsolated { iso in
                          iso.writeFrames(frames)
                      }
                  }
              }
          }
      }
      ```
      
      See `actors-isolation.md` for the full `assumeIsolated` recipe set, including why `audioQueue.async { assumeIsolated { ... } }` is correct but nesting it inside a CoreAudio listener that already dispatches on the same queue causes an ordering bug.
      
      ## TCC / Permissions for CATap
      
      The Privacy & Security pane has been labeled **"Screen & System Audio Recording"** since macOS 14 Sonoma. On macOS 26 it gains a "System Audio Recording Only" subsection and the Screen Recording grant implicitly covers system-audio capture. Under the hood, `kTCCServiceAudioCapture` is a distinct TCC service from `kTCCServiceScreenCapture` — preflighting one does not authoritatively answer for the other, and the two can drift (particularly on macOS 14/15 where they are separately toggleable).
      
      **There is no public `CGPreflightScreenCaptureAccess` equivalent for CATap.** Apple does not expose a documented live "is System Audio Recording authorized?" API. Common fallback patterns:
      
      1. Attempt to create the tap + aggregate. Failure (`kAudioHardwareIllegalOperationError` or `kAudioHardwareBadObjectError` when the TCC prompt is declined) strongly correlates with lack of permission; branch to "request access" UX.
      2. Mirror the `CGPreflightScreenCaptureAccess` check as a **proxy only** — the two services share a UI pane on macOS 14+, so Screen Recording authorization is a decent hint, but do not treat it as authoritative for CATap specifically.
      3. `insidegui/AudioCap` uses the private SPI `TCCAccessPreflight(kTCCServiceAudioCapture, nil)`. Not App-Store-safe but useful for Developer ID builds.
      4. Never rely on a `UserDefaults`-cached "granted" flag — the user can revoke permission in System Settings between launches, and reading stale state leads to silent recordings (buffers flow, RMS stays at -inf).
      
      ```swift
      // Preferred pattern: live-check on every permission-sensitive UI refresh.
      // macOS 26: Screen Recording TCC covers system audio too.
      func audioCapturePermissionGranted() -> Bool {
          CGPreflightScreenCaptureAccess()
      }
      ```
      
      For the "granted but silent" failure mode after a force-recursive replace (rm&#8209;rf + cp&#8209;R) reinstall with the same Developer ID, see `distribution.md` - TCC is keyed by CDHash; a same-CDHash reinstall can leave the entry in a degraded state where it reads authorized but delivers no audio. Fix: toggle off/on in System Settings.
      
      ## Another app's Voice Processing reshapes your microphone tap
      
      Voice Processing (VPIO) is a property of the shared input path, not of your process alone. When a communications app enables it during a call, your own raw mic tap can be handed a layout you did not ask for - more than two channels - and naive stereo handling produces a nearly inaudible track.
      
      ```swift
      let format = inputNode.inputFormat(forBus: 0)
      if format.channelCount > 2 {
          // Take channel 0 rather than downmixing an unknown layout
          mono = extractChannel(0, from: buffer)
          mono = applyMakeupGain(mono, db: calibratedGainDB)
      } else {
          mono = downmixToMono(buffer)   // branch on isInterleaved - see screen-capture-audio.md
      }
      logRMS(mono)   // per-session RMS makes a wrong gain diagnosable on other hardware
      ```
      
      Log per-session RMS. A makeup gain calibrated on one machine is a guess on another, and RMS in the log is the difference between "the user reports it is quiet" and knowing by how much.
      
      ### Enabling VPIO in your own engine ducks all other system audio
      
      Beyond the incompatibility already documented above, turning on `setVoiceProcessingEnabled(true)` makes macOS classify your process as a VoIP app and duck every other audio source by roughly 20 dB system-wide. If you are concurrently capturing system audio, you have just attenuated your own recording's content by 20 dB.
      
      Treat "enable VPIO for AEC/AGC" as an experiment that must be measured, not a default:
      
      - Measure the ducking effect on a concurrent system-audio capture before shipping it.
      - Keep a raw-mode fallback: enabling voice processing can fail and leave the input node in a corrupted state, so the code path that runs without it must stay working.
      
    • distribution.md 21.8 KB
      # macOS App Distribution
      
      ## Table of Contents
      - Distribution Methods Overview
      - Code Signing
      - App Store Distribution
      - Developer ID Distribution
      - Notarization
      - Notarization Gotchas
      - Sandboxing
      - Hardened Runtime
      - Universal Binaries
      - Sparkle (Auto-Update)
      
      ## Distribution Methods
      
      | Method | Audience | Signing | Notarization | Sandbox | Review |
      |--------|----------|---------|-------------|---------|--------|
      | Mac App Store | Public | Apple Distribution | Automatic | Required | Yes |
      | Developer ID | Public (outside store) | Developer ID | Required | Recommended | No |
      | Ad-Hoc | Internal/testing | None or Dev | Not required | No | No |
      | TestFlight | Testers | Apple Distribution | Automatic | Required | Beta review |
      
      ## Code Signing
      
      ### Certificates
      - **Apple Development** - For running on local devices during development
      - **Apple Distribution** - For App Store and TestFlight
      - **Developer ID Application** - For distribution outside the App Store
      - **Developer ID Installer** - For signed `.pkg` installers
      
      ### Automatic signing (recommended)
      In Xcode: Signing & Capabilities > check "Automatically manage signing". Select your team.
      
      ### Manual signing
      ```bash
      # List identities
      security find-identity -v -p codesigning
      
      # Sign app
      codesign --force --options runtime \
        --sign "Developer ID Application: Your Name (TEAM_ID)" \
        --timestamp \
        MyApp.app
      
      # Verify
      codesign --verify --deep --strict MyApp.app
      spctl --assess --type execute MyApp.app
      ```
      
      ### Dev-loop gotcha: `SIGKILL (Code Signature Invalid)`
      
      If a running signed app is killed at idle right after a rebuild (e.g. a `make run` loop that replaces the `.app` under the running process), the crash is `SIGKILL` with `EXC_CRASH (Code Signature Invalid)` - not a bug in your code. macOS validates memory-mapped code pages against the on-disk signature lazily; overwriting the bundle invalidates the pages the kernel later faults in, so it terminates the process. Quit the old instance before replacing the bundle, or launch from a copied path.
      
      ## Building an existing `.xcodeproj` from the command line
      
      Signing an `.xcodeproj` via `xcodebuild` fails in ways that still report `BUILD SUCCEEDED`. These are the traps that cost the most time.
      
      **`BUILD SUCCEEDED` does not mean "signed as you asked."** When the pbxproj declares an SDK-conditional identity - `CODE_SIGN_IDENTITY[sdk=macosx*] = "Apple Development"` - a command-line override can be *accepted, logged, and then ignored*. The log prints `note: Using codesigning identity override: <hash>` and the build still emits an ad-hoc-signed bundle. An `.xcconfig` setting `CODE_SIGN_IDENTITY = -` outranks both the pbxproj conditional and the command line. Two defences:
      
      ```bash
      # 1. Refuse to silently fall back to ad-hoc
      xcodebuild -scheme MyApp CODE_SIGNING_REQUIRED=YES ...
      
      # 2. Never trust the build log - verify the artifact
      codesign -dv --verbose=4 build/MyApp.app 2>&1 | grep -E 'Authority|Signature|flags'
      ```
      
      `Signature=adhoc` in that output means unsigned for every practical purpose: no Developer ID, no notarization, no stable TCC identity.
      
      **Bracketed build settings cannot be passed on the command line at all.** `xcodebuild` splits each `SETTING=VALUE` argument on the *first* `=`, so `CODE_SIGN_IDENTITY[sdk=macosx*]=X` parses as the setting name `CODE_SIGN_IDENTITY[sdk` with the value `macosx*]=X`. To change a conditional identity, edit the pbxproj or supply an `.xcconfig` - there is no CLI form.
      
      **Match the identity by name, not by prefix.** Scripts that auto-detect a certificate with `security find-identity | grep "Apple Development: "` can never match a `Developer ID Application` certificate - different certificate class, and no environment variable or rename fixes it. Match on the exact keychain name you intend to use, or build ad-hoc and re-sign the bundle inside-out (frameworks -> dylibs -> XPC services -> app).
      
      **Build from the resolved real path.** Building through a symlinked source directory corrupts Xcode's build database: you get `Stale file ... outside of the allowed root paths` and a module-emit failure, often with **no `error:` line anywhere in the log**. Delete the derived-data directory and rebuild from the fully resolved path (`cd "$(realpath .)"`).
      
      **Signature-class changes cost one round of TCC re-prompts.** Switching a bundle between ad-hoc and Developer ID changes its code identity, so the system asks for screen-recording and microphone consent once more. That is expected. Also expected: `spctl -a -vv` reporting `rejected / Unnotarized Developer ID` for a locally signed build - Gatekeeper acts on the quarantine attribute, which `xattr -cr MyApp.app` clears.
      
      **Moving a signed `.app` preserves its TCC grants and preferences.** TCC keys off bundle identifier plus signature, not path, so relocating a bundle does not reset permissions. The safe replace idiom is quit the app, `ditto` the new bundle into place, then `xattr -cr` it.
      
      ## App Store Distribution
      
      ### Requirements
      - Active Apple Developer Program membership ($99/year)
      - App Store Connect listing with metadata, screenshots
      - **Since April 28, 2026**: uploads to App Store Connect must be built with **Xcode 26 and the iOS 26 / iPadOS 26 / tvOS 26 / visionOS 26 / watchOS 26 SDK**. Pure macOS apps are **not** in Apple's list as of 2026-04-24; Mac Catalyst and Designed-for-iPad builds inherit the iOS 26 SDK requirement. Source: https://developer.apple.com/news/?id=ueeok6yw and https://developer.apple.com/news/upcoming-requirements
      - Sandbox entitlement required
      - App Review compliance
      - Audit `PrivacyInfo.xcprivacy` against the current required-reason APIs list (https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api) — Apple updates it periodically
      
      ### Workflow
      1. Archive: Product > Archive in Xcode
      2. Validate: Window > Organizer > Validate App
      3. Upload: Distribute App > App Store Connect
      4. Submit for review in App Store Connect
      
      ### ExportOptions.plist (for CI)
      ```xml
      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
        "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
      <plist version="1.0">
      <dict>
          <key>method</key>
          <string>app-store-connect</string>
          <key>teamID</key>
          <string>YOUR_TEAM_ID</string>
          <key>destination</key>
          <string>upload</string>
          <key>signingStyle</key>
          <string>automatic</string>
      </dict>
      </plist>
      ```
      
      ### CI build & upload
      ```bash
      # Build archive
      xcodebuild archive \
        -project MyApp.xcodeproj \
        -scheme MyApp \
        -archivePath build/MyApp.xcarchive \
        -destination "generic/platform=macOS"
      
      # Export and upload
      xcodebuild -exportArchive \
        -archivePath build/MyApp.xcarchive \
        -exportPath build/export \
        -exportOptionsPlist ExportOptions.plist
      
      # Or upload directly. Use notarytool - NOT `altool --upload-app`:
      # the notary service stopped accepting altool uploads on 2023-11-01,
      # and the flag is absent from Xcode 26.6's altool --help.
      xcrun notarytool submit build/export/MyApp.pkg \
        --key AuthKey_KEYID.p8 \
        --key-id KEY_ID \
        --issuer ISSUER_ID \
        --wait
      ```
      
      ### Notarize-then-export in one archive flow
      
      `xcodebuild` can export an already-notarized archive directly, which avoids re-signing between notarization and distribution:
      
      ```bash
      xcodebuild -exportNotarizedApp \
        -archivePath build/MyApp.xcarchive \
        -exportPath build/notarized
      ```
      
      ## Developer ID Distribution
      
      For distributing outside the Mac App Store:
      
      ### Build and sign
      ```bash
      xcodebuild archive \
        -scheme MyApp \
        -archivePath MyApp.xcarchive
      
      xcodebuild -exportArchive \
        -archivePath MyApp.xcarchive \
        -exportPath ./export \
        -exportOptionsPlist DevIDExport.plist
      ```
      
      DevIDExport.plist:
      ```xml
      <dict>
          <key>method</key>
          <string>developer-id</string>
          <key>teamID</key>
          <string>YOUR_TEAM_ID</string>
          <key>signingStyle</key>
          <string>automatic</string>
      </dict>
      ```
      
      ### Create DMG
      ```bash
      # Create DMG with hdiutil
      hdiutil create -volname "MyApp" -srcfolder ./export/MyApp.app \
        -ov -format UDZO MyApp.dmg
      
      # Or use create-dmg for pretty DMGs
      # brew install create-dmg
      create-dmg \
        --volname "MyApp" \
        --window-pos 200 120 \
        --window-size 600 400 \
        --icon-size 100 \
        --icon "MyApp.app" 175 120 \
        --hide-extension "MyApp.app" \
        --app-drop-link 425 120 \
        MyApp.dmg ./export/MyApp.app
      ```
      
      ## Notarization
      
      Required for all Developer ID-signed apps (since macOS 10.15):
      
      ```bash
      # Submit for notarization
      xcrun notarytool submit MyApp.dmg \
        --apple-id your@email.com \
        --team-id TEAM_ID \
        --password @keychain:AC_PASSWORD \
        --wait
      
      # Check status
      xcrun notarytool info SUBMISSION_ID \
        --apple-id your@email.com \
        --team-id TEAM_ID \
        --password @keychain:AC_PASSWORD
      
      # View log on failure
      xcrun notarytool log SUBMISSION_ID \
        --apple-id your@email.com \
        --team-id TEAM_ID \
        --password @keychain:AC_PASSWORD
      
      # Staple ticket to app/DMG
      xcrun stapler staple MyApp.dmg
      
      # Verify
      xcrun stapler validate MyApp.dmg
      spctl --assess --type open --context context:primary-signature MyApp.dmg
      ```
      
      ### Store credentials in keychain
      ```bash
      xcrun notarytool store-credentials "AC_PASSWORD" \
        --apple-id your@email.com \
        --team-id TEAM_ID \
        --password "app-specific-password"
      
      # Then use:
      xcrun notarytool submit MyApp.dmg \
        --keychain-profile "AC_PASSWORD" --wait
      ```
      
      ### Using API keys (recommended for CI)
      ```bash
      xcrun notarytool submit MyApp.dmg \
        --key AuthKey_KEYID.p8 \
        --key-id KEY_ID \
        --issuer ISSUER_UUID \
        --wait
      ```
      
      ## Notarization Gotchas
      
      ### notarytool 403 "A required agreement is missing or has expired"
      
      A 403 from `xcrun notarytool submit` is almost never a code-signing problem - it's an Apple agreement that needs acceptance. Two flavors, both easy to miss:
      
      **(a) Apple Developer Program License Agreement.** Must be accepted by the **Account Holder** (not team admin, not developer). Log in at `developer.apple.com/account` with the Account Holder's Apple ID; a banner prompts acceptance.
      
      **(b) Digital Services Act (DSA) banner on App Store Connect.** Even when distributing via Developer ID + GitHub Releases (nothing on the App Store), an unanswered DSA compliance banner at `appstoreconnect.apple.com` can hold the account in incomplete state and cause notarytool to 403. Click "Business → Agreements, Tax, and Banking" and complete any outstanding prompts. For non-App-Store distribution, "I'm not a trader under the DSA or I don't plan to distribute in the EU" is the appropriate answer.
      
      **Propagation lag**: after accepting, notarytool may still 403 for several minutes. Don't retry in a tight loop - check both `developer.apple.com/account` and the "Agreements, Tax, and Banking" page for any remaining unsigned items, then wait 5-10 minutes before resubmitting.
      
      ### TCC is keyed by code-signature CDHash - reinstalls can degrade without error
      
      TCC grants are keyed by the app's CDHash + bundle ID + Developer ID. When a release-channel app is replaced via a force-recursive delete of `/Applications/App.app` followed by `cp -R ./export/App.app /Applications/` (common in `make install` / CI workflows), TCC may remember the grant **but deliver degraded content** - permission reads as authorized, capture APIs start without error, buffers flow at zero amplitude. Direct-reading `~/Library/Application Support/com.apple.TCC/TCC.db` is blocked by SIP, so there is no programmatic recovery.
      
      The user-visible remediation: System Settings → Privacy & Security → toggle the permission off, then on again. To prevent the issue:
      - Prefer in-place overwrite (let the OS handle inode swap) over force-recursive replace (rm&#8209;rf + cp&#8209;R).
      - `killall App` before replacing the bundle - otherwise the still-running process keeps executing from its unlinked inode and `open -a` activates the stale instance instead of launching the new one.
      - For dev scripts, consider `tccutil reset ScreenCapture $BUNDLE_ID` after install so the user gets a fresh prompt rather than a degraded cached grant.
      
      ### Nested framework bundles must be deep-signed
      
      Frameworks like Sparkle contain nested bundles (`Updater.app`, `Installer.xpc`, `Downloader.xpc`). Notarization rejects if these aren't individually signed with Developer ID + secure timestamp. Sign from inside out:
      
      ```bash
      # Sign all nested bundles inside Sparkle
      find "MyApp.app/Contents/Frameworks/Sparkle.framework" \
          -type d \( -name "*.app" -o -name "*.xpc" \) | while read f; do
          codesign --force --options runtime --sign "$SIGN_ID" --timestamp "$f"
      done
      # Then the framework itself
      codesign --force --options runtime --sign "$SIGN_ID" --timestamp \
          "MyApp.app/Contents/Frameworks/Sparkle.framework"
      # Then the main app
      codesign --force --options runtime --sign "$SIGN_ID" --timestamp \
          --entitlements entitlements.plist "MyApp.app"
      
      # Verify before submitting
      codesign --verify --deep --strict MyApp.app
      ```
      
      ### Missing intermediate certificate
      
      After importing a Developer ID Application certificate, `security find-identity -v -p codesigning` may show no valid identity. Apple's G2 intermediate certificate must be downloaded and installed separately into the keychain.
      
      ### First-time Developer ID accounts may stall
      
      First notarization submissions from a new Developer ID can sit "In Progress" for 72+ hours. If signing were wrong, Apple rejects within minutes as `Invalid`. "In Progress" for days means the submission passed validation and is in Apple's review queue. File a Technical Support Incident (TSI) if this happens.
      
      ### Gatekeeper bypass for non-notarized builds
      
      Developer ID-signed but non-notarized apps show Gatekeeper warnings. **The Control-click (right-click) > Open override was removed in macOS Sequoia and is still gone in Tahoe 26** - do not tell users to use it.
      
      The only supported path now: try to open the app once (so the block is registered), then System Settings > Privacy & Security > scroll down > **Open Anyway** > confirm. Apple's [current documentation](https://support.apple.com/en-us/102445) describes this flow exclusively. The app is then saved as an exception and opens by double-click thereafter.
      
      ### Sparkle EdDSA signing
      
      Sparkle uses its own EdDSA key pair (separate from Apple code signing) to verify update integrity. Sign DMGs with Sparkle's `sign_update` tool:
      
      ```bash
      .build/artifacts/sparkle/Sparkle/bin/sign_update MyApp-1.0.0.dmg
      ```
      
      The appcast.xml contains the EdDSA signature and is generated per-release.
      
      ## Sandboxing
      
      Required for Mac App Store. Configured via entitlements:
      
      ```xml
      <!-- MyApp.entitlements -->
      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
        "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
      <plist version="1.0">
      <dict>
          <key>com.apple.security.app-sandbox</key>
          <true/>
          <key>com.apple.security.files.user-selected.read-write</key>
          <true/>
          <key>com.apple.security.network.client</key>
          <true/>
          <key>com.apple.security.network.server</key>
          <false/>
      </dict>
      </plist>
      ```
      
      Common sandbox entitlements:
      - `files.user-selected.read-write` - Access user-selected files
      - `files.downloads.read-write` - Access Downloads folder
      - `network.client` - Outbound network connections
      - `network.server` - Incoming connections (server)
      - `device.camera` - Camera access
      - `device.microphone` - Microphone access (sandbox side)
      - `personal-information.calendars` - Calendar access
      - `personal-information.contacts` - Contacts access
      
      ## Hardened Runtime
      
      Required for notarization. Enable in Xcode: Signing & Capabilities > + Capability > Hardened Runtime.
      
      Resource access entitlements (only enable if needed):
      - `com.apple.security.device.audio-input` - Microphone and audio input via Core Audio
      - `com.apple.security.device.camera` - Camera access
      
      Runtime exception entitlements (weaken security - use sparingly):
      - `cs.disable-library-validation` - Load third-party frameworks/plugins with different Team ID
      - `cs.allow-unsigned-executable-memory` - Allow unsigned executable memory
      - `cs.allow-jit` - Allow JIT compilation
      
      **For capture apps**: microphone access needs BOTH sandbox (`device.microphone`) AND hardened runtime (`device.audio-input`) entitlements if sandboxed. Screen recording needs NO entitlement - it's governed by TCC runtime permission only.
      
      ### Capture app entitlements
      
      Non-sandboxed (Developer ID):
      
      ```xml
      <dict>
          <key>com.apple.security.device.audio-input</key>
          <true/>
      </dict>
      ```
      
      Sandboxed (App Store):
      
      ```xml
      <dict>
          <key>com.apple.security.app-sandbox</key>
          <true/>
          <key>com.apple.security.device.microphone</key>
          <true/>
          <key>com.apple.security.device.audio-input</key>
          <true/>
          <key>com.apple.security.files.user-selected.read-write</key>
          <true/>
      </dict>
      ```
      
      Info.plist (required for microphone):
      
      ```xml
      <key>NSMicrophoneUsageDescription</key>
      <string>Record audio alongside screen capture.</string>
      ```
      
      ### Persistent Content Capture
      
      `com.apple.developer.persistent-content-capture` bypasses macOS 15+ recurring screen recording prompts. Intended for VNC/remote desktop apps. Requires Apple approval and provisioning profile - request through Apple Developer entitlement request form.
      
      ## Universal Binaries
      
      Support both Apple Silicon and Intel:
      
      ```bash
      # Build universal
      xcodebuild -scheme MyApp \
        -destination "generic/platform=macOS" \
        ARCHS="arm64 x86_64" \
        ONLY_ACTIVE_ARCH=NO
      
      # Verify architectures
      lipo -info MyApp.app/Contents/MacOS/MyApp
      # Output: Architectures in the fat file: arm64 x86_64
      
      # Create universal from two builds
      lipo -create MyApp-arm64 MyApp-x86_64 -output MyApp-universal
      ```
      
      In Xcode: Build Settings > Architectures > Standard Architectures (Apple Silicon, Intel)
      
      ## Sparkle (Auto-Update)
      
      For Developer ID apps, use Sparkle for auto-updates:
      
      ```swift
      // Package.swift dependency
      .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0")
      
      // In App
      import Sparkle
      
      @main
      struct MyApp: App {
          private let updaterController = SPUStandardUpdaterController(
              startingUpdater: true,
              updaterDelegate: nil,
              userDriverDelegate: nil
          )
      
          var body: some Scene {
              WindowGroup {
                  ContentView()
              }
              .commands {
                  CommandGroup(after: .appInfo) {
                      CheckForUpdatesView(updater: updaterController.updater)
                  }
              }
          }
      }
      ```
      
      ## Notarization rejections: check these first
      
      `com.apple.security.get-task-allow` is the single most common cause. It is the debug entitlement Xcode injects into Debug builds; if it survives into the archive you submit, the notary service rejects the upload. Verify before submitting:
      
      ```bash
      codesign -d --entitlements - --xml build/export/MyApp.app | plutil -p -
      ```
      
      If `get-task-allow` appears with value `true`, you archived a Debug configuration or a custom entitlements file carries it. Strip it from the Release entitlements.
      
      Other frequent blockers:
      
      - Hardened Runtime not enabled on every executable in the bundle (not just the main one).
      - A nested binary signed with a different Team ID, or left unsigned.
      - **Plug-in entitlement inheritance**: "Shared libraries, frameworks, and in-process plug-ins inherit the entitlements of their host executable." The host app must declare every entitlement its plug-ins need - a plug-in cannot add its own.
      
      Reference: [Resolving common notarization issues](https://developer.apple.com/documentation/security/resolving-common-notarization-issues).
      
      ## Enhanced Security capability (Xcode 26+)
      
      Xcode 26 added an **Enhanced Security** capability enabling additional runtime and compile-time protections - pointer authentication, hardened heap, memory tagging - via the `com.apple.security.hardened-process.*` entitlement family.
      
      The entitlement schema changed again in Xcode 26.4: `-version` became `-version-string` and `-platform-restrictions` became `-platform-restrictions-string`. If you hand-maintain an entitlements plist rather than using the capability UI, re-check the key spellings against the [Security entitlements reference](https://developer.apple.com/documentation/bundleresources/security-entitlements) when moving between Xcode versions.
      
      ## Installer packages
      
      For apps that need an installer rather than a drag-to-Applications DMG:
      
      ```bash
      pkgbuild --root build/export/MyApp.app \
        --identifier com.example.myapp \
        --version 1.0.0 \
        --install-location /Applications/MyApp.app \
        build/MyApp-component.pkg
      
      productbuild --distribution Distribution.xml \
        --package-path build \
        build/MyApp.pkg
      
      productsign --sign "Developer ID Installer: Your Name (TEAMID)" \
        build/MyApp.pkg build/MyApp-signed.pkg
      ```
      
      Note the identity is **Developer ID Installer**, a different certificate from the **Developer ID Application** identity used for the app itself.
      
      Useful `notarytool` subcommands beyond `submit`:
      
      ```bash
      xcrun notarytool history --key ... --key-id ... --issuer ...
      xcrun notarytool log <submission-id> --key ... --key-id ... --issuer ...
      xcrun notarytool info <submission-id> -f json --key ... --key-id ... --issuer ...
      ```
      
      `--s3-acceleration` speeds up large uploads. `stapler validate <path>` verifies a stapled ticket.
      
      ## Homebrew cask as a second install channel
      
      A signed, notarized Mac app can ship through a Homebrew tap alongside the DMG. Practical constraints, in the order they usually bite:
      
      - **Pick a globally unique cask token.** Tap CI (`brew test-bot --only-tap-syntax`) fails if your token collides with one in homebrew-core, and renaming later orphans every existing install.
      - **If you must rename, ship `cask_renames.json`** in the tap so `brew update` migrates existing installs. `tap_migrations.json` is for cross-tap moves and will not do this.
      - **Set `auto_updates true`** when the app self-updates via Sparkle, so Homebrew does not fight the in-app updater. Add a `livecheck` block so version bumps are detectable.
      - **`depends_on macos:` cannot express a point release** (e.g. 26.1). Enforce a precise minimum in the app at launch and treat the cask constraint as approximate.
      - **Reinstalling over an app already in `/Applications` fails** without `--force`.
      - Run `brew style` and `brew audit --cask` before pushing; generated casks commonly trip on a redundant `version` line.
      
    • fall-2026-releases.md 8.5 KB
      # Fall 2026 Releases (WWDC 2026)
      
      Announced at WWDC 2026 (June 8, 2026), shipping fall 2026. As of 2026-08-20, Xcode 27 is at **beta 5** and macOS 27 at **beta 6**. Build against the shipping stack (Swift 6.3.3 / Xcode 26.6 / macOS 26.6.2 Tahoe) unless you specifically target these betas.
      
      Sources: [Apple Newsroom, 2026-06-08](https://www.apple.com/newsroom/2026/06/apple-aids-app-development-with-new-intelligence-frameworks-and-advanced-tools/); [WWDC26 "What's new in Swift"](https://developer.apple.com/videos/play/wwdc2026/262); [Xcode 27 release notes](https://developer.apple.com/documentation/xcode-release-notes/xcode-27-release-notes); [macOS 27 release notes](https://developer.apple.com/documentation/macos-release-notes/macos-27-release-notes).
      
      ## OS and toolchain
      
      - **macOS 27 Golden Gate** (beta 6, build 26A5416b, 2026-08-17) - next macOS; Apple-silicon only. macOS 26.6.2 Tahoe is the current shipping release until fall.
      - **Xcode 27** (beta 5, build 27A5237l, 2026-08-10) - agentic coding with Anthropic/Google/OpenAI models plus MCP plug-ins and the Agent Client Protocol; Apple-silicon only, ~30% smaller. Requires macOS Tahoe 26.4 or later. Gemini also landed in stable Xcode 26.6. Beta 5 previews an MCP server that runs without an open Xcode workspace and can grant code-signed agents long-lived permission over a directory tree; enable it with `sudo xcrun mcp-server enable`.
      - **Swift 6.4** (beta; shipping toolchain is still 6.3.3) - ships inside the Xcode 27 beta ("Xcode 27 beta 5 includes Swift 6.4 and SDKs for iOS 27, iPadOS 27, tvOS 27, watchOS 27, macOS 27, and visionOS 27"), and the `release/6.4.x` branch is cut with daily snapshots on swift.org. Features: targeted warning suppression (SE-0522), `~Sendable` to explicitly mark a type non-Sendable (SE-0518), Task Cancellation Shields (SE-0504), `Continuation` (SE-0528), `async` calls in `defer` bodies (SE-0493), borrow/mutate accessors (SE-0507), a memberwise initializer that excludes private initialized properties (SE-0502), and Advanced Observation Tracking (SE-0506).
      
      Two version-gate corrections worth carrying:
      
      - `weak let` (SE-0481) is **not** a 6.4 feature - it shipped in Swift 6.3 and is already available in the pinned toolchain.
      - `anyAppleOS` is **not** gated on 6.4 either - it compiles on 6.3.3 behind `-enable-experimental-feature AnyAppleOSAvailability`. Without the flag the compiler names it for you: `error: any Apple OS requires '-enable-experimental-feature AnyAppleOSAvailability'`.
      
      ## Xcode 27 build-breakers
      
      These bite when you first open a project in Xcode 27, before any new API is adopted. Check them ahead of the beta.
      
      - **The ld64 linker is gone.** "The ld64 linker has been removed and the `-ld_classic` option is no longer supported. (165165518)" Any project still carrying `-ld_classic` in `OTHER_LDFLAGS` - a flag Apple itself prescribed as a workaround in Xcode 15 - fails to link. Remove it.
      - **Clang module names must be globally unique per dependency scan.** The optimized Swift dependency scanner now requires that "every Clang module reachable from a single Swift dependency-scan action must have a unique module name. If two module maps visible to the same scan declare a Clang module with the same name, the scan may report an error." The common cause is vendored third-party sources shipping a `module.modulemap` that redeclares an SDK module.
      - **Universal binaries are no longer the default at deployment target 27.0+.** "The `ARCHS_STANDARD` build setting will no longer include x86_64 when `MACOSX_DEPLOYMENT_TARGET` or `DRIVERKIT_DEPLOYMENT_TARGET` >= 27.0." Add x86_64 to `ARCHS` explicitly if you still ship Intel. Xcode 27 itself installs only on Apple silicon.
      - **SE-0508 source break.** "A computed property with both an `init` accessor and an array/dictionary literal initial value will no longer compile if the getter is declared before the `init` accessor." Workaround: declare the `init` accessor first.
      - **libc++ floor raised.** The minimum supported C++ deployment target on macOS moves to 11.0, and `multimap`/`multiset::find` no longer necessarily returns an iterator to the first equal element.
      
      ## Document apps: concurrency and new protocols
      
      macOS 27 adds `ReadableDocument` / `WritableDocument`, plus a combined `Document` protocol for the common read-and-write case. As of beta 6 the older protocols are formally deprecated - not merely superseded:
      
      > You can now use the `Document` protocol for representing documents in `DocumentGroup`. This protocol combines `ReadableDocument` and `WritableDocument` for common read-and-write cases. Use `Document` instead of `ReferenceFileDocument` and `FileDocument`, which are now deprecated.
      
      New `DocumentGroup` initializers adopting them expose an `Observable` `URLDocumentConfiguration`, let you disable document creation for editing-only apps, and present custom UI before any document is opened.
      
      The isolation contract changed in a way that silently defeats off-main I/O if you carry old annotations forward:
      
      > The `read(from:progress:)` and `write(content:to:previous:progress:)` requirements of `DocumentReader` and `DocumentWriter` are declared with `@concurrent` instead of `nonisolated`. With approachable-concurrency defaults that infer `MainActor` isolation, an unannotated `nonisolated` async method runs on the main actor, defeating the intent of off-main reading and writing. Conforming types that previously used `nonisolated` should switch to `@concurrent` to match.
      
      `makeDocument:` and `makeReadableDocument:` closures passed to `DocumentGroup` initializers are now `@MainActor`-isolated.
      
      ## Frameworks
      
      - **Foundation Models next-gen** - the single native Swift API now accepts image input and adds server models running on Private Cloud Compute via `PrivateCloudComputeLanguageModel`, which Apple frames as a one-line swap for `SystemLanguageModel` and which requires the `com.apple.developer.private-cloud-compute` entitlement. `DynamicProfile` (with `LanguageModelSession.Profile` and `DynamicInstructions`) swaps model/tools/instructions mid-session. A new `LanguageModel` protocol makes third-party models (Claude, Gemini) pluggable behind the same API. `ContextOptions` arrives here too - despite the name it configures what appears in the prompt, not a trimming policy. `LanguageModelSession.GenerationError` is **deprecated** as of 27.0 in favour of `LanguageModelError` / `SystemLanguageModel.Error` / `LanguageModelSession.Error`, with `exceededContextWindowSize` renamed `contextSizeExceeded`; Apple states you must update to Xcode 27 to catch the new types before submitting. It remains correct under the macOS 26.5 SDK you build against today.
      - **Core AI** - a brand-new framework, distinct from Foundation Models, for loading and running full-scale LLMs on device, optimized for the Neural Engine and unified memory. Public surface includes `AIModel`, `AIModelAsset`, `AIModelCache`, `InferenceFunction`, `NDArray`, `ComputeUnitKind`, and `SpecializationOptions`, plus a background-inference entitlement `com.apple.developer.background-tasks.continued-processing.inference`. No `updates/coreai` change log exists yet (404), so the framework index is the only source.
      - **SwiftData** - a new data-store observation surface: `ResultsObserver` delivers real-time updates for models matching fetch criteria, and `HistoryObserver` observes remote model changes. `@Query(...sectionBy:)` returns `SectionedResults`/`ResultsSection` for grouped queries, and `Schema.Attribute.Option.codable` lets `Codable` types - including ones you do not control - be stored directly in a model.
      - **SwiftUI** - reorderable list/grid containers, faster layout, and lazier `@State` initialization (back-deployed). In apps built with the macOS 27 SDK, `List` accepts drops in two cases that previously did not work: drags with compatible transfer representations into reorderable content even without `.reorderableItem`, and `.dropDestination(...)` declared on a list item. SwiftUI also now hides menu item symbol images in most contexts by default - use `.labelStyle(.titleAndIcon)` to opt a menu item back in. New **Spatial Preview** framework streams 3D content from a Mac to Apple Vision Pro.
      - **Previews** - code inside `#Preview` now explicitly runs on the main actor, so it can call main-actor-isolated APIs without concurrency warnings or runtime check failures.
      
      ## Liquid Glass becomes mandatory
      
      Apps rebuilt with the Xcode 27 SDK can no longer opt out of Liquid Glass - the `UIDesignRequiresCompatibility` key is ignored. Under Xcode 26 the key still works as a temporary migration aid. A new system transparency slider lets users tune the effect.
      
    • foundation-models.md 22.2 KB
      # Foundation Models Framework
      
      On-device AI using Apple's ~3B parameter LLM. Available on macOS 26+ with Apple Silicon. Free inference, offline, private.
      
      All symbol references cite the shipped SDK swiftinterface at `FoundationModels.framework/.../arm64e-apple-macos.swiftinterface` (lines indicated inline) and/or Apple's developer documentation.
      
      ## Table of Contents
      - Setup & Basic Usage
      - Instructions
      - Guided Generation (Structured Output)
      - Streaming
      - Tool Calling
      - Sessions & Context
      - Generation Options
      - Error Handling
      - Built-in Use Cases
      - Custom Adapters
      - Language Support & Limits
      - Availability & Limitations
      
      ## Setup
      
      ```swift
      import FoundationModels
      
      let model = SystemLanguageModel.default
      
      switch model.availability {
      case .available:
          break
      case .unavailable(.deviceNotEligible):
          // Intel Mac, non-A17/M-series, etc.
          return
      case .unavailable(.appleIntelligenceNotEnabled):
          // User must enable in Settings > Apple Intelligence & Siri.
          return
      case .unavailable(.modelNotReady):
          // Model is still downloading / preparing.
          return
      }
      ```
      
      The four-case `Availability` enum (`.available` plus three `UnavailableReason`s) is defined at swiftinterface lines 555-572. `SystemLanguageModel.default` is at line 576. There is also a `final public var isAvailable: Bool` on the model (line 518) for a quick boolean check.
      
      Requires: macOS 26+ / iOS 26+ / iPadOS 26+ / visionOS 26+, Apple Silicon, Apple Intelligence enabled.
      
      ## Basic Usage
      
      ```swift
      let session = LanguageModelSession()
      let response = try await session.respond(to: "Summarize: \(text)")
      print(response.content)          // Swift.String
      print(response.transcriptEntries) // ArraySlice<Transcript.Entry>
      ```
      
      The return type is `LanguageModelSession.Response<String>`, not `String` directly (swiftinterface lines 347-354). `Response<Content>` exposes `content`, `rawContent: GeneratedContent`, and `transcriptEntries`.
      
      ## Instructions
      
      Set the system prompt at session creation. Instructions are fixed for the lifetime of the session.
      
      ```swift
      let session = LanguageModelSession(
          instructions: """
          You are a concise writing assistant. Focus on grammar and clarity.
          Never add commentary; return only the rewritten text.
          """
      )
      let rewritten = try await session.respond(to: "Review: \(userText)").content
      ```
      
      Correct initializer: `convenience init(model: SystemLanguageModel = .default, tools: [any Tool] = [], instructions: String? = nil)` (swiftinterface line 339). Overloads also accept `Instructions` directly or an `@InstructionsBuilder` closure (lines 340-341).
      
      ## Guided Generation
      
      Generate Swift types directly - the framework constrains decoding to your schema, no JSON parsing.
      
      ```swift
      @Generable
      struct RecipeSuggestion {
          @Guide(description: "Name of the recipe")
          var name: String
      
          @Guide(description: "Ingredients with quantities")
          var ingredients: [String]
      
          @Guide(description: "Step-by-step instructions")
          var steps: [String]
      
          @Guide(description: "Estimated cooking time in minutes", .range(5...180))
          var cookingTime: Int
      }
      
      let session = LanguageModelSession()
      let result: LanguageModelSession.Response<RecipeSuggestion> = try await session.respond(
          to: "Suggest a quick pasta recipe",
          generating: RecipeSuggestion.self
      )
      let recipe = result.content
      print(recipe.name, recipe.cookingTime)
      ```
      
      `respond(to:generating:...)` returns `Response<Content>` where `Content: Generable` (swiftinterface lines 378-382). The `@Generable` macro is at line 16; `@Guide` overloads at lines 24/28/32. `GenerationGuide` supports `.range(_:)`, `.minimum(_:)`, `.maximum(_:)` for numerics; `.anyOf(_:)`, `.constant(_:)`, `.pattern(_:)` for `String`; `.count(_:)`, `.minimumCount(_:)`, `.maximumCount(_:)`, `.element(_:)` for arrays (lines 254-299).
      
      Built-in `Generable` conformances: `Bool`, `String`, `Int`, `Float`, `Double`, `Decimal`, `Array<Element: Generable>`, `Optional`, plus your own `@Generable` structs and enums (lines 92-195).
      
      ### Enum-constrained output
      ```swift
      @Generable
      enum Sentiment: String, CaseIterable {
          case positive, negative, neutral
      }
      
      @Generable
      struct SentimentResult {
          var sentiment: Sentiment
          @Guide(description: "0.0 - 1.0 confidence", .range(0.0...1.0))
          var confidence: Double
      }
      
      let analysis = try await session.respond(
          to: "Analyze sentiment: '\(review)'",
          generating: SentimentResult.self
      ).content
      ```
      
      ## Streaming
      
      Stream partial results for responsive UI. Snapshots contain `Content.PartiallyGenerated` which fills in progressively (swiftinterface lines 463-468).
      
      ```swift
      // Text streaming - snapshots are String snapshots
      for try await snapshot in session.streamResponse(to: "Write a short story.") {
          view.text = snapshot.content // current complete text so far
      }
      
      // Structured streaming — given a @Generable target type:
      @Generable
      struct TripPlan {
          @Guide(description: "Destination city")
          var destination: String
          @Guide(description: "Day-by-day itinerary")
          var days: [String]
          @Guide(description: "Estimated budget in USD", .range(0...10_000))
          var budgetUSD: Int
      }
      
      // Pick one: iterate for live updates, OR collect once for the final value.
      // The stream is single-pass; iterating consumes it.
      let stream = session.streamResponse(
          to: "Plan a weekend trip",
          generating: TripPlan.self
      )
      for try await snapshot in stream {
          // snapshot.content: TripPlan.PartiallyGenerated (optionals fill in over time)
          render(snapshot.content)
      }
      
      // Or, without iterating, collect the final value:
      let finalResponse = try await session.streamResponse(
          to: "Plan a weekend trip",
          generating: TripPlan.self
      ).collect() // Response<TripPlan>
      ```
      
      `ResponseStream.collect()` is at swiftinterface line 491. The stream conforms to `AsyncSequence` (line 473).
      
      ## Tool Calling
      
      `Tool` is a protocol, not a macro. There is no `@Toolbox` or `@Tool` annotation in the framework. Each tool is a `Sendable` type with an associated `Arguments: Generable` and `Output: PromptRepresentable` (swiftinterface lines 1186-1196).
      
      ```swift
      struct GetWeather: Tool {
          let name = "getWeather"
          let description = "Get current weather for a city."
      
          @Generable
          struct Arguments {
              @Guide(description: "City name, e.g. 'San Francisco'")
              var city: String
          }
      
          func call(arguments: Arguments) async throws -> String {
              let w = try await WeatherService.current(for: arguments.city)
              return "\(w.tempF)F, \(w.conditions)"
          }
      }
      
      struct GetForecast: Tool {
          let name = "getForecast"
          let description = "Get an N-day forecast for a city."
      
          @Generable
          struct Arguments {
              var city: String
              @Guide(description: "Number of days", .range(1...10))
              var days: Int
          }
      
          func call(arguments: Arguments) async throws -> String {
              let f = try await WeatherService.forecast(for: arguments.city, days: arguments.days)
              return f.map { "\($0.date): \($0.conditions)" }.joined(separator: "\n")
          }
      }
      
      let session = LanguageModelSession(
          tools: [GetWeather(), GetForecast()],
          instructions: "Use the weather tools when asked about weather."
      )
      let answer = try await session.respond(
          to: "Should I bring an umbrella to SF tomorrow?"
      ).content
      ```
      
      Notes from the framework:
      - `Arguments` must be `Generable`. `String`, `Int`, `Double`, `Float`, `Decimal`, and `Bool` are *explicitly unavailable* as `Arguments` (swiftinterface lines 1219-1268) - use an `@Generable` struct that wraps them.
      - `Output` must be `PromptRepresentable`. `String` and `Array<PromptRepresentable>` already conform (lines 1131, 1139).
      - `call(arguments:)` runs on the `@concurrent` executor; make it `async throws`.
      - TN3193 recommends a maximum of 3-5 tools per session to keep schemas small in the context window.
      
      ## Sessions & Context
      
      Sessions are stateful and thread-safe (`final public class` marked `@unchecked Sendable`, swiftinterface lines 321-397). Re-use one across turns; the transcript grows automatically.
      
      ```swift
      let session = LanguageModelSession(instructions: "You are a code reviewer.")
      session.prewarm() // Optional: start loading the model in the background.
      
      let r1 = try await session.respond(to: "Review: \(code)")
      let r2 = try await session.respond(to: "How would you refactor it?")
      // session.transcript contains all instructions/prompts/responses so far.
      
      // Check whether a response is in flight
      if !session.isResponding {
          // safe to start another respond(...)
      }
      ```
      
      There is no `session.reset()` method. To start fresh, create a new session. To preserve partial state across the reset, rebuild a `Transcript` from the old session's entries:
      
      ```swift
      // Start a new session that carries the first and last turns only.
      func condensed(from old: LanguageModelSession) -> LanguageModelSession {
          let kept = [old.transcript.first, old.transcript.last].compactMap { $0 }
          let transcript = Transcript(entries: kept)
          let session = LanguageModelSession(transcript: transcript)
          session.prewarm()
          return session
      }
      ```
      
      `prewarm(promptPrefix:)` is at swiftinterface line 343. The `LanguageModelSession(transcript:)` initializer is at line 342. This pattern is the one Apple recommends for recovering from context-window errors (TN3193).
      
      ## Generation Options
      
      ```swift
      let options = GenerationOptions(
          sampling: .greedy,              // or .random(top: 40), .random(probabilityThreshold: 0.9)
          temperature: 0.2,               // lower = more deterministic
          maximumResponseTokens: 512      // cap response size
      )
      
      let r = try await session.respond(to: prompt, options: options).content
      ```
      
      `GenerationOptions` is a `struct` with `sampling: SamplingMode?`, `temperature: Double?`, `maximumResponseTokens: Int?` (swiftinterface lines 1311-1328). `SamplingMode` offers `.greedy`, `.random(top:seed:)`, `.random(probabilityThreshold:seed:)` (lines 1315-1322).
      
      TN3193 warns: use `maximumResponseTokens` only as a safety cap against runaway generations; hard truncation can produce malformed partial output. To shorten responses, ask in the prompt ("In 3 sentences...") or use `.maximumCount(_:)` on `Generable` arrays.
      
      ## Error Handling
      
      `LanguageModelSession.GenerationError` is an enum with these cases (swiftinterface lines 405-443):
      
      ```swift
      do {
          let response = try await session.respond(to: prompt, options: options)
          return response.content
      } catch LanguageModelSession.GenerationError.guardrailViolation(let ctx) {
          // Apple safety guardrail tripped. Don't blindly retry - reword or decline.
          log("guardrail: \(ctx.debugDescription)")
          return fallback
      } catch LanguageModelSession.GenerationError.exceededContextWindowSize(let ctx) {
          // Context window full (4096 tokens). Start a new session with a condensed transcript.
          log("context full: \(ctx.debugDescription)")
          return try await condensed(from: session).respond(to: prompt).content
      } catch LanguageModelSession.GenerationError.unsupportedLanguageOrLocale(let ctx) {
          return "Language not supported: \(ctx.debugDescription)"
      } catch LanguageModelSession.GenerationError.assetsUnavailable(let ctx) {
          // Model / adapter assets not downloaded yet.
          return "Model unavailable: \(ctx.debugDescription)"
      } catch LanguageModelSession.GenerationError.rateLimited {
          return "Too many requests - back off."
      } catch LanguageModelSession.GenerationError.concurrentRequests {
          return "Only one request per session at a time."
      } catch LanguageModelSession.GenerationError.refusal(let refusal, _) {
          // Model refused. `refusal.explanation` is an async call to fetch the reason.
          return try await refusal.explanation.content
      } catch LanguageModelSession.GenerationError.decodingFailure(let ctx) {
          // Guided generation produced content that failed to decode into your type.
          throw NSError(domain: "decoding", code: 0, userInfo: [NSLocalizedDescriptionKey: ctx.debugDescription])
      } catch LanguageModelSession.GenerationError.unsupportedGuide(let ctx) {
          // A @Guide constraint isn't supported (e.g. unsupported regex feature).
          throw NSError(domain: "guide", code: 0, userInfo: [NSLocalizedDescriptionKey: ctx.debugDescription])
      }
      ```
      
      Every case carries a `GenerationError.Context` with a `debugDescription`. The `.refusal` case additionally carries a `Refusal` whose `explanation` / `explanationStream` let you ask the model why it refused (swiftinterface lines 416-433).
      
      Tool errors surface as a separate `LanguageModelSession.ToolCallError` that wraps your tool and the underlying error (lines 447-454).
      
      ### `GenerationError` is deprecated in macOS 27 - with a submission deadline
      
      Apple deprecates `LanguageModelSession.GenerationError` in the macOS 27 SDK in favour of `LanguageModelError`, `SystemLanguageModel.Error`, and `LanguageModelSession.Error`, and renames `exceededContextWindowSize` to `contextSizeExceeded`. The deprecation note is unusually strict:
      
      > Use `LanguageModelError`, `SystemLanguageModel.Error`, or `LanguageModelSession.Error` instead. Apps built with Xcode 26 will continue to catch this error until you rebuild with Xcode 27. **You must update to Xcode 27 to catch the new error types before submitting your app.**
      
      The `catch` ladder above is correct for the Xcode 26 / macOS 26.5 SDK this skill targets, but plan the rename before moving to Xcode 27 - a stale `catch LanguageModelSession.GenerationError.exceededContextWindowSize` silently stops matching once rebuilt. Source: <https://developer.apple.com/documentation/foundationmodels/languagemodelsession/generationerror>
      
      ## Built-in Use Cases
      
      `SystemLanguageModel.UseCase` is a struct (not an enum). Only two values ship on macOS 26 (swiftinterface lines 524-528):
      
      - `.general` - the default model.
      - `.contentTagging` - optimized for generating tags/topics/classifications from text.
      
      ```swift
      // Content tagging model - better than .general for tag/topic extraction.
      let taggingModel = SystemLanguageModel(useCase: .contentTagging)
      let session = LanguageModelSession(model: taggingModel)
      
      @Generable
      struct Tags {
          @Guide(description: "3-7 topical tags", .maximumCount(7))
          var tags: [String]
      }
      let tags = try await session.respond(
          to: "Tag this article: \(text)",
          generating: Tags.self
      ).content.tags
      ```
      
      `SystemLanguageModel(useCase:guardrails:)` is at swiftinterface line 582. Guardrails are either `.default` or `.permissiveContentTransformations` (lines 543-547); use the permissive option only for transformation tasks on user-provided content (summarize, translate, rewrite).
      
      ## Custom Adapters
      
      A custom LoRA adapter lets you specialize the base model's behavior. Training happens out-of-band with Apple's Python toolkit; the deployed artifact is an `.fmadapter` package.
      
      Key rules (from Apple's "Loading and using a custom adapter" doc):
      - Adapter files are 160 MB or larger - **do not bundle them in your app**. Download on demand via Background Assets or a Managed Asset Pack.
      - Each adapter is locked to a specific base model version. You must retrain for every new base model release.
      - Shipping adapters requires the `com.apple.developer.foundation-model-adapter` entitlement. Local testing does not.
      - Adapters run only on physical devices (not Simulator).
      
      ### Local testing (file URL)
      
      ```swift
      let url = URL(filePath: "/absolute/path/to/my.fmadapter")
      let adapter = try SystemLanguageModel.Adapter(fileURL: url)
      let model = SystemLanguageModel(adapter: adapter)
      let session = LanguageModelSession(model: model)
      let response = try await session.respond(to: "...")
      ```
      
      `Adapter(fileURL:)` is at swiftinterface line 663; `SystemLanguageModel(adapter:)` is at line 586.
      
      ### Shipping (Background Assets)
      
      ```swift
      // Clear out any adapters from older base-model versions.
      try SystemLanguageModel.Adapter.removeObsoleteAdapters()
      
      // Load by base name. Triggers a Background Assets download if needed.
      let adapter = try SystemLanguageModel.Adapter(name: "myAdapter")
      
      // Optional: compile the draft model once (if your adapter has one) for faster inference.
      try await adapter.compile()
      
      let model = SystemLanguageModel(adapter: adapter)
      let session = LanguageModelSession(model: model)
      ```
      
      `Adapter(name:)`, `removeObsoleteAdapters()`, `compile()`, `compatibleAdapterIdentifiers(name:)`, and `isCompatible(_:)` are all on `SystemLanguageModel.Adapter` (swiftinterface lines 664-673). The asset-downloader extension's `shouldDownload(_:)` should delegate adapter-compatibility checks to `SystemLanguageModel.Adapter.isCompatible(assetPack)`.
      
      See <https://developer.apple.com/apple-intelligence/foundation-models-adapter> for the Python toolkit. (Apple removed the "Loading and using a custom adapter" article - that URL now redirects to the framework root. The `SystemLanguageModel.Adapter` symbols above are still present in the SDK and still compile.)
      
      ## Language Support & Limits
      
      ```swift
      let model = SystemLanguageModel.default
      model.supportedLanguages     // Set<Locale.Language>
      model.supportsLocale(.current) // Bool
      model.contextSize            // Int (4096 on macOS 26)
      ```
      
      `supportedLanguages`, `supportsLocale(_:)`, and `contextSize` are at swiftinterface lines 587-590 and 635-644.
      
      **Context window**: 4096 tokens per `LanguageModelSession`, covering instructions, prompts, tool definitions, tool outputs, `Generable` schemas, and all responses (TN3193). When you exceed it, the framework throws `.exceededContextWindowSize` - create a fresh session with a condensed transcript (see "Sessions & Context" above).
      
      **Token counting** (macOS 26.4+): `SystemLanguageModel` exposes `tokenCount(for:)` overloads for `Prompt`, `Instructions`, `[Tool]`, `GenerationSchema`, and transcript-entry collections (swiftinterface lines 600-624). Use these plus the Foundation Models Instruments template (Xcode > Instruments > Foundation Models) to profile consumption.
      
      ## Availability & Limitations
      
      - **Hardware**: Apple Silicon only (A17 Pro / M1 and later where Apple Intelligence is supported).
      - **OS**: macOS 26+, iOS 26+, iPadOS 26+, visionOS 26+. Unavailable on tvOS and watchOS (swiftinterface `@available ... tvOS, unavailable; watchOS, unavailable`).
      - **Model size**: ~3B parameters on-device.
      - **Strengths**: summarization, content generation, structured extraction, classification, tagging, light tool use.
      - **Limitations**: not a general-knowledge chatbot, 4096-token context window, no image generation, one in-flight request per session (`.concurrentRequests`).
      - **Privacy**: all inference runs on device. No data sent to Apple.
      - **Cost**: free; no API keys, no quotas beyond hardware throughput.
      
      Primary references:
      - <https://developer.apple.com/documentation/foundationmodels>
      - <https://developer.apple.com/documentation/foundationmodels/generating-content-and-performing-tasks-with-foundation-models>
      - <https://developer.apple.com/documentation/technotes/tn3193-managing-the-on-device-foundation-model-s-context-window>
      
      ## `@PromptBuilder` - composing prompts structurally
      
      Every `respond`/`streamResponse` method has an overload taking a `@PromptBuilder` closure instead of a string. Use it when the prompt is assembled conditionally - it keeps the structure readable and avoids string-concatenation bugs:
      
      ```swift
      let response = try await session.respond(generating: Summary.self) {
          "Summarize the following meeting transcript."
      
          if let focus = userFocus {
              "Focus specifically on: \(focus)"
          }
      
          for segment in transcript.segments {
              "[\(segment.speaker)]: \(segment.text)"
          }
      }
      ```
      
      `@InstructionsBuilder` does the same for `Instructions` at session construction:
      
      ```swift
      let session = LanguageModelSession {
          "You are a concise meeting summarizer."
          if compactMode { "Keep every bullet under 12 words." }
      }
      ```
      
      ## Runtime schemas with `DynamicGenerationSchema`
      
      `@Generable` requires the shape at compile time. When the structure is only known at runtime - user-defined extraction fields, a schema fetched from a server - build it dynamically:
      
      ```swift
      let schema = DynamicGenerationSchema(
          name: "ExtractedRecord",
          properties: userFields.map { field in
              DynamicGenerationSchema.Property(
                  name: field.name,
                  schema: DynamicGenerationSchema(type: String.self),
                  isOptional: !field.isRequired
              )
          }
      )
      
      let generationSchema = try GenerationSchema(root: schema, dependencies: [])
      let response = try await session.respond(to: prompt, schema: generationSchema)
      let content: GeneratedContent = response.content
      let value = try content.value(String.self, forProperty: "title")
      ```
      
      The result is `GeneratedContent` rather than a typed struct - read properties by name with `value(_:forProperty:)`. Prefer `@Generable` whenever the shape is static; this path trades compile-time safety for flexibility.
      
      ## Managing the context window
      
      The context window is 4096 tokens and there is **no** API in the macOS 26 SDK that trims or evicts transcript entries for you. The recovery path is manual: catch the context error and start a fresh session seeded with a condensed summary of the old transcript (see "Sessions & Context" above).
      
      Budget proactively instead of reacting. `SystemLanguageModel.tokenCount(for:)` measures a `Prompt`, `Instructions`, `[Tool]`, `GenerationSchema`, or a collection of transcript entries before you send it, and `contextSize` reports the maximum the model supports:
      
      ```swift
      let model = SystemLanguageModel.default
      let used = try await model.tokenCount(for: prompt)
      if used > model.contextSize / 2 { /* condense before sending */ }
      ```
      
      In Xcode, the `#Playground` macro shows an estimate of how much of the 4096-token window a given call consumes, which is the fastest way to find the instruction block or tool schema that is eating the budget.
      
      > **`ContextOptions` is macOS 27 beta, not macOS 26.** It does not exist in the macOS 26.5 SDK (`error: cannot find 'ContextOptions' in scope`), and despite the name it is not a trimming or retention policy - Apple defines it as "Options that configure details that should appear in the prompt." Do not reach for it on the shipping stack. Source: <https://developer.apple.com/documentation/foundationmodels/contextoptions>
      
      ## Server-backed models (macOS 27 beta)
      
      `PrivateCloudComputeLanguageModel` is a variant of the system model that runs on Private Cloud Compute for capabilities the ~3B on-device model cannot handle, while keeping Apple's privacy guarantees. Apple describes it as a one-line swap for `SystemLanguageModel`; it requires the `com.apple.developer.private-cloud-compute` entitlement. macOS 27 beta only. Source: <https://developer.apple.com/documentation/foundationmodels/privatecloudcomputelanguagemodel>
      
    • migration-guide.md 7.5 KB
      # Concurrency Migration Guide
      
      ## Table of Contents
      - Migration Paths
      - From No Concurrency to Swift 6.2
      - From GCD to async/await
      - From Combine to AsyncSequence
      - From ObservableObject to @Observable
      - Common Errors & Fixes
      - Checklist
      
      ## Migration Paths
      
      ### Path A: New project (recommended)
      1. Swift 6 language mode + default MainActor isolation from day one
      2. Use `@concurrent` for explicit background work
      3. Use `@Observable` for state, `@Query` for SwiftData
      
      ### Path B: Existing project, incremental
      1. Start with Swift 5 mode + `StrictConcurrency` upcoming feature (warnings only)
      2. Fix warnings one module at a time
      3. Enable Swift 6 mode per-target as they become clean
      4. Add default isolation last
      
      ## From GCD to async/await
      
      ### Dispatch queues
      ```swift
      // Before
      DispatchQueue.global().async {
          let data = loadData()
          DispatchQueue.main.async {
              self.items = parse(data)
          }
      }
      
      // After
      func loadAndParse() async {
          let data = await loadData()
          items = parse(data) // Already on MainActor with default isolation
      }
      ```
      
      ### DispatchGroup
      ```swift
      // Before
      let group = DispatchGroup()
      var results: [Data] = []
      for url in urls {
          group.enter()
          fetch(url) { data in
              results.append(data)
              group.leave()
          }
      }
      group.notify(queue: .main) {
          process(results)
      }
      
      // After
      func fetchAll(_ urls: [URL]) async throws -> [Data] {
          try await withThrowingTaskGroup(of: Data.self) { group in
              for url in urls {
                  group.addTask { try await fetch(url) }
              }
              return try await group.reduce(into: []) { $0.append($1) }
          }
      }
      ```
      
      ### Serial queue (mutual exclusion)
      ```swift
      // Before
      let queue = DispatchQueue(label: "com.app.serial")
      queue.sync { sharedState.update() }
      
      // After
      actor SharedState {
          func update() { /* ... */ }
      }
      await sharedState.update()
      ```
      
      ### Timer
      ```swift
      // Before
      Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
          refresh()
      }
      
      // After
      func startPolling() async {
          while !Task.isCancelled {
              try? await Task.sleep(for: .seconds(5))
              await refresh()
          }
      }
      ```
      
      ## From Combine to AsyncSequence
      
      ### Publisher to AsyncSequence
      ```swift
      // Before (Combine)
      cancellable = publisher
          .map { $0.name }
          .filter { !$0.isEmpty }
          .sink { name in
              self.displayName = name
          }
      
      // After
      for await value in stream.map(\.name).filter({ !$0.isEmpty }) {
          displayName = value
      }
      ```
      
      ### @Published to @Observable
      ```swift
      // Before
      class ViewModel: ObservableObject {
          @Published var items: [Item] = []
          @Published var isLoading = false
          @Published var error: Error?
      
          private var cancellables = Set<AnyCancellable>()
      
          func load() {
              isLoading = true
              service.fetchItems()
                  .receive(on: DispatchQueue.main)
                  .sink(
                      receiveCompletion: { [weak self] completion in
                          self?.isLoading = false
                          if case .failure(let error) = completion {
                              self?.error = error
                          }
                      },
                      receiveValue: { [weak self] items in
                          self?.items = items
                      }
                  )
                  .store(in: &cancellables)
          }
      }
      
      // After
      @Observable
      final class ViewModel {
          var items: [Item] = []
          var isLoading = false
          var error: Error?
      
          func load() async {
              isLoading = true
              defer { isLoading = false }
              do {
                  items = try await service.fetchItems()
              } catch {
                  self.error = error
              }
          }
      }
      ```
      
      ### View changes
      ```swift
      // Before
      struct ContentView: View {
          @StateObject private var viewModel = ViewModel()
          var body: some View { /* ... */ }
      }
      
      // After
      struct ContentView: View {
          @State private var viewModel = ViewModel()
          var body: some View { /* ... */ }
      }
      ```
      
      Note: `@StateObject` -> `@State`, `@ObservedObject` -> remove or use `@Bindable`
      
      ## Common Errors & Fixes
      
      ### "Sending value of non-Sendable type"
      ```swift
      // Error
      class MyClass { var data: [String] = [] }
      Task { let c = MyClass() } // MyClass not Sendable
      
      // Fix 1: Make it Sendable
      final class MyClass: Sendable { let data: [String] }
      
      // Fix 2: Use actor
      actor MyActor { var data: [String] = [] }
      
      // Fix 3: Use struct
      struct MyData: Sendable { var data: [String] = [] }
      ```
      
      ### "Cannot access property from non-isolated context"
      ```swift
      // Error
      @MainActor class VM { var x = 0 }
      func compute(vm: VM) { print(vm.x) } // non-isolated can't access
      
      // Fix 1: Make function MainActor
      @MainActor func compute(vm: VM) { print(vm.x) }
      
      // Fix 2: Make it async
      func compute(vm: VM) async { print(await vm.x) }
      
      // Fix 3: Use default isolation (Swift 6.2)
      // With -default-isolation MainActor, both are on MainActor
      ```
      
      ### "Global variable must be isolated or Sendable"
      ```swift
      // Error
      var sharedConfig = Config()
      
      // Fix 1: Make it a let with Sendable type
      let sharedConfig = Config(/* immutable */) // Config must be Sendable
      
      // Fix 2: MainActor isolate
      @MainActor var sharedConfig = Config()
      
      // Fix 3: nonisolated(unsafe) for thread-safe globals
      nonisolated(unsafe) let logger = Logger()
      ```
      
      ## Checklist
      
      1. [ ] Enable strict concurrency checking as warnings first
      2. [ ] Make value types (structs/enums) conform to Sendable where needed
      3. [ ] Convert mutable classes to actors or @Observable
      4. [ ] Replace DispatchQueue.main with @MainActor
      5. [ ] Replace GCD with structured concurrency (TaskGroup, async let)
      6. [ ] Replace Combine publishers with AsyncSequence where possible
      7. [ ] Replace @Published with @Observable properties
      8. [ ] Replace completion handlers with async/await
      9. [ ] Enable Swift 6 language mode
      10. [ ] Enable default MainActor isolation (Swift 6.2)
      11. [ ] Add @concurrent to functions needing background execution
      12. [ ] Remove now-redundant @MainActor annotations
      
      ## Let the compiler do the migration: `swift package migrate`
      
      SwiftPM ships a migration command that mechanically rewrites source for a given upcoming feature, instead of chasing diagnostics by hand:
      
      ```bash
      swift package migrate --to-feature ExistentialAny
      swift package migrate --to-feature InternalImportsByDefault
      swift package migrate --target MyApp --to-feature NonisolatedNonsendingByDefault
      ```
      
      It applies the fix-its across the target and updates `Package.swift` to enable the feature. Run it on a clean tree, one feature at a time, and review the diff - it is a mechanical rewrite, not a design review.
      
      Full flag list: `swift package migrate --help`.
      
      ## Upcoming-feature flags worth knowing individually
      
      Enabling `.swiftLanguageMode(.v6)` in one step is often too big a jump for an existing codebase. These dials let you land Swift 6 semantics incrementally, each as `.enableUpcomingFeature("<name>")`:
      
      | Flag | What it turns on |
      |---|---|
      | `StrictConcurrency` | Full data-race checking under language mode 5 |
      | `InferSendableFromCaptures` | Closures infer `Sendable` from what they capture - removes many spurious annotations |
      | `GlobalActorIsolatedTypesUsability` | Makes global-actor-isolated types usable in more generic contexts |
      | `NonisolatedNonsendingByDefault` | `nonisolated async` inherits the caller's isolation |
      | `DisableOutwardActorInference` | Stops a global-actor-isolated property from silently isolating its containing type |
      | `ExistentialAny` | Requires `any` on existential types |
      | `InternalImportsByDefault` | Imports are `internal` unless marked `public` |
      
      `DisableOutwardActorInference` is the one that most often explains a surprise: a single `@MainActor var` can otherwise pull an entire type onto the main actor without you writing that anywhere.
      
    • migrations.md 6.8 KB
      # Schema Migrations
      
      ## Table of Contents
      - Versioned Schemas
      - Lightweight Migration
      - Custom Migration
      - SchemaMigrationPlan
      - Core Data Migration
      - Best Practices
      
      ## Versioned Schemas
      
      Define each version of your schema:
      
      ```swift
      // Version 1: Original
      enum AppSchemaV1: VersionedSchema {
          static var versionIdentifier = Schema.Version(1, 0, 0)
          static var models: [any PersistentModel.Type] {
              [Project.self]
          }
      
          @Model
          final class Project {
              var name: String
              var isActive: Bool
      
              init(name: String) {
                  self.name = name
                  self.isActive = true
              }
          }
      }
      
      // Version 2: Added fields
      enum AppSchemaV2: VersionedSchema {
          static var versionIdentifier = Schema.Version(2, 0, 0)
          static var models: [any PersistentModel.Type] {
              [Project.self]
          }
      
          @Model
          final class Project {
              var name: String
              var isActive: Bool
              var createdAt: Date    // New field
              var priority: Int      // New field
      
              init(name: String) {
                  self.name = name
                  self.isActive = true
                  self.createdAt = .now
                  self.priority = 0
              }
          }
      }
      
      // Version 3: Renamed + added relationship
      enum AppSchemaV3: VersionedSchema {
          static var versionIdentifier = Schema.Version(3, 0, 0)
          static var models: [any PersistentModel.Type] {
              [Project.self, Task.self]
          }
      
          @Model
          final class Project {
              var title: String      // Renamed from 'name'
              var isActive: Bool
              var createdAt: Date
              var priority: Int
      
              @Relationship(deleteRule: .cascade)
              var tasks: [Task] = [] // New relationship
      
              init(title: String) {
                  self.title = title
                  self.isActive = true
                  self.createdAt = .now
                  self.priority = 0
              }
          }
      
          @Model
          final class Task {
              var title: String
              var isComplete: Bool
              var project: Project?
      
              init(title: String) {
                  self.title = title
                  self.isComplete = false
              }
          }
      }
      ```
      
      ## Lightweight Migration
      
      For additive changes (new properties with defaults, new models):
      
      ```swift
      enum AppMigrationPlan: SchemaMigrationPlan {
          static var schemas: [any VersionedSchema.Type] {
              [AppSchemaV1.self, AppSchemaV2.self]
          }
      
          static var stages: [MigrationStage] {
              [migrateV1toV2]
          }
      
          static let migrateV1toV2 = MigrationStage.lightweight(
              fromVersion: AppSchemaV1.self,
              toVersion: AppSchemaV2.self
          )
      }
      ```
      
      Lightweight migration handles:
      - Adding new properties (with default values)
      - Adding new model types
      - Removing properties
      - Renaming properties (with `@Attribute(originalName:)`)
      
      ### Property rename
      ```swift
      // In V2, rename 'name' to 'title'
      @Model
      final class Project {
          @Attribute(originalName: "name")
          var title: String
          // ...
      }
      ```
      
      ## Custom Migration
      
      For complex transformations:
      
      ```swift
      enum AppMigrationPlan: SchemaMigrationPlan {
          static var schemas: [any VersionedSchema.Type] {
              [AppSchemaV1.self, AppSchemaV2.self, AppSchemaV3.self]
          }
      
          static var stages: [MigrationStage] {
              [migrateV1toV2, migrateV2toV3]
          }
      
          static let migrateV1toV2 = MigrationStage.lightweight(
              fromVersion: AppSchemaV1.self,
              toVersion: AppSchemaV2.self
          )
      
          static let migrateV2toV3 = MigrationStage.custom(
              fromVersion: AppSchemaV2.self,
              toVersion: AppSchemaV3.self
          ) { context in
              // Custom migration logic
              let projects = try context.fetch(FetchDescriptor<AppSchemaV2.Project>())
      
              for project in projects {
                  // Transform data during migration
                  // e.g., capitalize all names
                  project.name = project.name.capitalized
              }
      
              try context.save()
          }
      }
      ```
      
      ## SchemaMigrationPlan
      
      Register with container:
      
      ```swift
      @main
      struct MyApp: App {
          var body: some Scene {
              WindowGroup {
                  ContentView()
              }
              .modelContainer(
                  for: AppSchemaV3.Project.self,
                  migrationPlan: AppMigrationPlan.self
              )
          }
      }
      ```
      
      ### Multiple migration chains
      ```swift
      // Each stage migrates sequentially: V1 -> V2 -> V3
      // User on V1 will run both stages
      // User on V2 will run only V2 -> V3
      static var stages: [MigrationStage] {
          [migrateV1toV2, migrateV2toV3]
      }
      ```
      
      ## Migrating from Core Data
      
      ### Coexistence approach (recommended)
      Run both Core Data and SwiftData side by side, migrating data at runtime:
      
      ```swift
      @main
      struct MyApp: App {
          let swiftDataContainer: ModelContainer
          let coreDataStack: NSPersistentContainer
      
          init() {
              // Set up SwiftData
              swiftDataContainer = try! ModelContainer(for: Project.self)
      
              // Set up legacy Core Data
              coreDataStack = NSPersistentContainer(name: "LegacyModel")
              coreDataStack.loadPersistentStores { _, _ in }
      
              // Migrate on first launch
              if !UserDefaults.standard.bool(forKey: "migrated_to_swiftdata") {
                  migrateFromCoreData()
                  UserDefaults.standard.set(true, forKey: "migrated_to_swiftdata")
              }
          }
      
          func migrateFromCoreData() {
              let coreDataContext = coreDataStack.viewContext
              let swiftDataContext = swiftDataContainer.mainContext
      
              // Fetch from Core Data
              let request = NSFetchRequest<NSManagedObject>(entityName: "CDProject")
              let cdProjects = try! coreDataContext.fetch(request)
      
              // Insert into SwiftData
              for cdProject in cdProjects {
                  let project = Project(
                      name: cdProject.value(forKey: "name") as! String
                  )
                  swiftDataContext.insert(project)
              }
      
              try! swiftDataContext.save()
          }
      }
      ```
      
      ### Direct replacement
      If your Core Data model is simple, SwiftData can often read the existing SQLite store:
      
      ```swift
      // Point SwiftData to the existing Core Data store
      let storeURL = NSPersistentContainer.defaultDirectoryURL()
          .appendingPathComponent("MyApp.sqlite")
      
      let config = ModelConfiguration(url: storeURL)
      let container = try ModelContainer(
          for: Project.self,
          configurations: config
      )
      ```
      
      Requirements:
      - SwiftData model class name must match Core Data entity name
      - Property names must match attribute names
      - Types must be compatible
      
      ## Best Practices
      
      1. **Always version your schema** from the start, even if you only have V1
      2. **Use lightweight migration** whenever possible (simpler, safer)
      3. **Test migrations** with real data before shipping
      4. **Keep migration stages small** - one logical change per stage
      5. **Back up user data** before running migrations in production
      6. **Use `@Attribute(originalName:)`** for renames instead of delete+add
      7. **New properties must have defaults** for lightweight migration to work
      8. **Never delete a VersionedSchema** that users might still be running
      
    • models-schema.md 9.4 KB
      # Models & Schema
      
      ## Table of Contents
      - @Model Basics
      - Property Attributes
      - Codable Properties
      - Transient Properties
      - External Storage
      - Transformable
      - Unique Constraints
      - Custom Hashable/Identifiable
      
      ## @Model Basics
      
      `@Model` is a macro that transforms a class into a SwiftData persistent model:
      
      ```swift
      @Model
      final class Note {
          var title: String
          var content: String
          var createdAt: Date
          var updatedAt: Date
          var isPinned: Bool
          var color: NoteColor
      
          init(title: String, content: String = "") {
              self.title = title
              self.content = content
              self.createdAt = .now
              self.updatedAt = .now
              self.isPinned = false
              self.color = .default
          }
      }
      ```
      
      What `@Model` synthesizes:
      - `PersistentModel` conformance (persistence)
      - `Observable` conformance (SwiftUI reactivity)
      - `Hashable` and `Identifiable` (based on persistent ID)
      - Schema definition for the persistence store
      
      ### Rules
      - Must be a `class` (not struct)
      - Prefer `final` for clarity
      - Must have at least one stored property
      - All stored properties must be persistable types
      - Needs a designated initializer
      
      ### Supported property types
      - Primitives: `String`, `Int`, `Double`, `Float`, `Bool`, `Date`, `Data`, `URL`, `UUID`
      - Optional versions of all above
      - Enums with `Codable` raw values
      - `Codable` structs/enums
      - Collections: `[T]` where T is any supported type
      - Relationships: Other `@Model` types
      
      ## Property Attributes
      
      ### @Attribute
      ```swift
      @Model
      final class Document {
          // Unique constraint - no two documents with same slug
          @Attribute(.unique) var slug: String
      
          // Spotlight indexing
          @Attribute(.spotlight) var title: String
      
          // External storage for large data
          @Attribute(.externalStorage) var thumbnail: Data?
      
          // Custom original name (for migration)
          @Attribute(originalName: "old_name") var newName: String
      
          // Preserve value on deletion
          @Attribute(.preserveValueOnDeletion) var archiveID: UUID
      
          // Per-field encryption at rest in CloudKit (NOT local-store encryption).
          // Apple DTS confirms: `.allowsCloudEncryption` only has effect on CloudKit-
          // synced stores. Use platform Data Protection (`FileProtectionType`) for
          // local at-rest encryption.
          @Attribute(.allowsCloudEncryption) var sensitiveData: Data?
      
          var content: String
      
          init(slug: String, title: String, content: String) {
              self.slug = slug
              self.title = title
              self.newName = title
              self.archiveID = UUID()
              self.content = content
          }
      }
      ```
      
      ### @Transient
      
      Properties not persisted to store:
      
      ```swift
      @Model
      final class Task {
          var title: String
          var isComplete: Bool
      
          // Not saved to database
          @Transient var isSelected = false
          @Transient var cachedPreview: NSImage?
      
          init(title: String) {
              self.title = title
              self.isComplete = false
          }
      }
      ```
      
      **Important**: `@Transient` properties must have default values since they won't be loaded from the store.
      
      ## Codable Properties
      
      Store complex types as Codable:
      
      ```swift
      struct Address: Codable {
          var street: String
          var city: String
          var zip: String
          var country: String
      }
      
      struct Preferences: Codable {
          var theme: String
          var fontSize: Int
          var notifications: Bool
      }
      
      @Model
      final class UserProfile {
          var name: String
          var address: Address       // Stored as encoded data
          var preferences: Preferences
      
          init(name: String, address: Address, preferences: Preferences) {
              self.name = name
              self.address = address
              self.preferences = preferences
          }
      }
      ```
      
      ### Enum properties
      ```swift
      enum Priority: String, Codable, CaseIterable {
          case low, medium, high, critical
      }
      
      enum TaskStatus: Int, Codable {
          case todo = 0
          case inProgress = 1
          case done = 2
          case archived = 3
      }
      
      @Model
      final class Task {
          var title: String
          var priority: Priority
          var status: TaskStatus
      
          init(title: String, priority: Priority = .medium) {
              self.title = title
              self.priority = priority
              self.status = .todo
          }
      }
      ```
      
      ## External Storage
      
      For large binary data (images, files):
      
      ```swift
      @Model
      final class Photo {
          var name: String
          var dateTaken: Date
      
          // Stored outside the main SQLite file
          @Attribute(.externalStorage) var imageData: Data?
          @Attribute(.externalStorage) var thumbnailData: Data?
      
          init(name: String, imageData: Data?) {
              self.name = name
              self.dateTaken = .now
              self.imageData = imageData
          }
      }
      ```
      
      External storage stores the data as a file on disk rather than inline in SQLite, improving query performance when you don't need the large data.
      
      ## Unique Constraints
      
      ```swift
      @Model
      final class Tag {
          @Attribute(.unique) var name: String
          var color: String
      
          init(name: String, color: String = "blue") {
              self.name = name
              self.color = color
          }
      }
      
      // Inserting a Tag with a duplicate name will upsert (update existing)
      let tag = Tag(name: "swift")      // Creates new
      let tag2 = Tag(name: "swift")     // Updates existing
      context.insert(tag2)              // No duplicate
      ```
      
      **Note**: Unique constraints are not supported with CloudKit sync.
      
      ## Identifiable & Hashable
      
      `@Model` types automatically conform to `Identifiable` using the persistent model ID:
      
      ```swift
      // Automatic - no need to define 'id'
      @Model
      final class Item {
          var name: String
          init(name: String) { self.name = name }
      }
      
      // item.id is PersistentIdentifier (auto-generated)
      // item.persistentModelID is the same value
      
      // In SwiftUI Lists
      ForEach(items) { item in  // Identifiable via @Model
          Text(item.name)
      }
      ```
      
      If you need a custom ID:
      ```swift
      @Model
      final class Item {
          @Attribute(.unique) var customID: UUID
          var name: String
      
          init(name: String) {
              self.customID = UUID()
              self.name = name
          }
      }
      ```
      
      ## Compound uniqueness: `#Unique`
      
      `@Attribute(.unique)` constrains a single property. For multi-property (compound) uniqueness, use the freestanding `#Unique` macro inside the model body:
      
      ```swift
      @Model
      final class Enrollment {
          #Unique<Enrollment>([\.studentID, \.courseID])   // the pair must be unique
      
          var studentID: UUID
          var courseID: UUID
          var enrolledAt: Date
      
          init(studentID: UUID, courseID: UUID) {
              self.studentID = studentID
              self.courseID = courseID
              self.enrolledAt = .now
          }
      }
      ```
      
      Multiple constraint sets are allowed: `#Unique<T>([\.a, \.b], [\.c])`. Same CloudKit caveat as `.unique` - unique constraints are unsupported when syncing, so a CloudKit-backed model cannot declare them.
      
      ## Indexes: `#Index`
      
      Declares single or compound indexes so predicates and sorts can use them instead of scanning:
      
      ```swift
      @Model
      final class LogEntry {
          #Index<LogEntry>([\.timestamp], [\.level, \.timestamp])
      
          var timestamp: Date
          var level: String
          var message: String
      
          init(level: String, message: String) {
              self.level = level
              self.message = message
              self.timestamp = .now
          }
      }
      ```
      
      Index the properties you actually filter and sort on. Compound index column order matters - `[\.level, \.timestamp]` serves "filter by level, sort by timestamp" but not the reverse.
      
      ## Model inheritance
      
      `@Model` classes can subclass other `@Model` classes (**macOS 26.0+**). Fetches on the parent type return subclass instances by default:
      
      ```swift
      @available(macOS 26, *)
      @Model class Media {
          var title: String
          init(title: String) { self.title = title }
      }
      
      @available(macOS 26, *)
      @Model final class Movie: Media {
          var runtime: Int = 0
          init(title: String, runtime: Int) {
              self.runtime = runtime
              super.init(title: title)
          }
      }
      
      @available(macOS 26, *)
      @Model final class Podcast: Media {
          var episodeCount: Int = 0
          init(title: String, episodeCount: Int) {
              self.episodeCount = episodeCount
              super.init(title: title)
          }
      }
      
      // Returns Movies and Podcasts too
      let all = try context.fetch(FetchDescriptor<Media>())
      ```
      
      The `@available` annotation is not optional: a `PersistentModel` subclass without one fails to compile with `A PersistentModel Subclass is required to have platform availability specified`, and annotating it below 26.0 fails with `A PersistentModel Subclass requires macOS 26.0 or greater`. Subclasses need explicit initializers like every other `@Model`.
      
      Control this on deletes with `includeSubclasses` (defaults to `true`):
      
      ```swift
      try context.delete(model: Media.self,
                         where: #Predicate { $0.title.isEmpty },
                         includeSubclasses: false)   // only exact Media rows
      ```
      
      Inheritance costs query performance and complicates migration - prefer a shared protocol or an enum discriminator column unless you genuinely need polymorphic fetches.
      
      ## `.ephemeral`
      
      The complete `Schema.Attribute.Option` set in the macOS 26.5 SDK is exactly: `.unique`, `.transformable(by:)`, `.externalStorage`, `.allowsCloudEncryption`, `.preserveValueOnDeletion`, `.ephemeral`, `.spotlight`. There is no `.encrypt` option - CloudKit field encryption is `.allowsCloudEncryption` (covered above).
      
      `.ephemeral` is the one not documented elsewhere in this file: the property participates in the model but is never persisted. Use it for transient state - a computed progress value, a cached thumbnail - that should not survive a relaunch.
      
      ```swift
      @Model final class Download {
          var url: URL
          @Attribute(.ephemeral) var bytesReceived: Int = 0   // reset on every launch
          init(url: URL) { self.url = url }
      }
      ```
      
    • relationships-predicates.md 6.9 KB
      # Relationships & Predicates
      
      ## Table of Contents
      - Relationship Types
      - Delete Rules
      - Inverse Relationships
      - Advanced #Predicate
      - Compound Predicates
      - Relationship Queries
      
      ## Relationship Types
      
      ### One-to-many
      ```swift
      @Model
      final class Folder {
          var name: String
      
          @Relationship(deleteRule: .cascade, inverse: \Document.folder)
          var documents: [Document] = []
      
          init(name: String) { self.name = name }
      }
      
      @Model
      final class Document {
          var title: String
          var folder: Folder?
      
          init(title: String, folder: Folder? = nil) {
              self.title = title
              self.folder = folder
          }
      }
      
      // Usage
      let folder = Folder(name: "Projects")
      let doc = Document(title: "Proposal", folder: folder)
      context.insert(folder)
      // doc is automatically associated via the relationship
      ```
      
      ### Many-to-many
      ```swift
      @Model
      final class Student {
          var name: String
      
          @Relationship(inverse: \Course.students)
          var courses: [Course] = []
      
          init(name: String) { self.name = name }
      }
      
      @Model
      final class Course {
          var title: String
          var students: [Student] = []
      
          init(title: String) { self.title = title }
      }
      
      // Associate
      let student = Student(name: "Alice")
      let course = Course(title: "Swift 101")
      student.courses.append(course)
      // course.students now automatically contains student
      ```
      
      ### One-to-one
      ```swift
      @Model
      final class User {
          var name: String
      
          @Relationship(deleteRule: .cascade, inverse: \Profile.user)
          var profile: Profile?
      
          init(name: String) { self.name = name }
      }
      
      @Model
      final class Profile {
          var bio: String
          var avatarURL: URL?
          var user: User?
      
          init(bio: String) { self.bio = bio }
      }
      ```
      
      ## Delete Rules
      
      | Rule | Behavior |
      |------|----------|
      | `.nullify` (default) | Set related objects' reference to nil |
      | `.cascade` | Delete related objects |
      | `.deny` | Prevent deletion if related objects exist |
      | `.noAction` | Do nothing (can leave orphans) |
      
      ```swift
      @Relationship(deleteRule: .cascade)    // Delete children when parent deleted
      @Relationship(deleteRule: .nullify)    // Set to nil, keep children
      @Relationship(deleteRule: .deny)       // Block deletion if children exist
      ```
      
      ### Cascade example
      ```swift
      @Model
      final class Project {
          var name: String
          @Relationship(deleteRule: .cascade)
          var tasks: [ProjectTask] = []
      
          init(name: String) { self.name = name }
      }
      
      // Deleting project automatically deletes all its tasks
      context.delete(project) // tasks are cascade-deleted
      ```
      
      ## Inverse Relationships
      
      Always specify inverse for bidirectional relationships:
      
      ```swift
      // Explicit inverse
      @Relationship(inverse: \Document.folder)
      var documents: [Document] = []
      
      // SwiftData can infer inverse when unambiguous:
      // If Document has only one Folder? property, inverse is auto-detected.
      // Prefer explicit inverse for clarity.
      ```
      
      ## Advanced #Predicate
      
      ### String operations
      ```swift
      // Contains (case-insensitive)
      #Predicate<Document> { doc in
          doc.title.localizedStandardContains(searchText)
      }
      
      // Starts with
      #Predicate<Document> { doc in
          doc.title.starts(with: "Draft")
      }
      
      // Not empty
      #Predicate<Document> { doc in
          !doc.content.isEmpty
      }
      ```
      
      ### Date comparisons
      ```swift
      // Created today
      let startOfDay = Calendar.current.startOfDay(for: .now)
      #Predicate<Document> { doc in
          doc.createdAt >= startOfDay
      }
      
      // Within last week
      let weekAgo = Calendar.current.date(byAdding: .day, value: -7, to: .now)!
      #Predicate<Document> { doc in
          doc.createdAt >= weekAgo
      }
      
      // Between dates
      #Predicate<Document> { doc in
          doc.createdAt >= startDate && doc.createdAt <= endDate
      }
      ```
      
      ### Optional handling
      ```swift
      // Has value
      #Predicate<Task> { task in
          task.dueDate != nil
      }
      
      // Optional comparison (use force unwrap inside predicate)
      #Predicate<Task> { task in
          task.dueDate != nil && task.dueDate! < Date.now
      }
      ```
      
      ### Compound predicates
      ```swift
      // AND
      #Predicate<Task> { task in
          !task.isComplete && task.priority == .high
      }
      
      // OR
      #Predicate<Task> { task in
          task.priority == .high || task.priority == .critical
      }
      
      // NOT
      #Predicate<Task> { task in
          !(task.status == .archived)
      }
      ```
      
      ### Collection predicates
      ```swift
      // Contains element
      #Predicate<Project> { project in
          project.tags.contains("urgent")
      }
      
      // Any match
      #Predicate<Project> { project in
          project.tasks.contains(where: { !$0.isComplete })
      }
      
      // Count
      #Predicate<Project> { project in
          project.tasks.count > 5
      }
      
      // All match
      #Predicate<Folder> { folder in
          folder.documents.allSatisfy { $0.isReviewed }
      }
      ```
      
      ## Dynamic Predicates
      
      Build predicates based on runtime conditions:
      
      ```swift
      func buildPredicate(
          searchText: String,
          showCompleted: Bool,
          priority: Priority?
      ) -> Predicate<Task> {
          #Predicate<Task> { task in
              (searchText.isEmpty || task.title.localizedStandardContains(searchText))
              && (showCompleted || !task.isComplete)
              && (priority == nil || task.priority == priority)
          }
      }
      
      // Use in @Query (via init)
      struct TaskListView: View {
          @Query private var tasks: [Task]
      
          init(searchText: String, showCompleted: Bool, priority: Priority?) {
              let predicate = buildPredicate(
                  searchText: searchText,
                  showCompleted: showCompleted,
                  priority: priority
              )
              _tasks = Query(filter: predicate, sort: \.createdAt, order: .reverse)
          }
      }
      ```
      
      ## Shaping the fetch
      
      `FetchDescriptor` carries more than `predicate` / `sortBy` / `fetchLimit`. These three change how much work a fetch does:
      
      ```swift
      var descriptor = FetchDescriptor<Book>(
          predicate: #Predicate { $0.year > 2000 },
          sortBy: [SortDescriptor(\.title)]
      )
      
      // Fetch only these properties; others fault in on demand.
      descriptor.propertiesToFetch = [\.title, \.year]
      
      // Warm these relationships in the same round trip - avoids the N+1
      // pattern where iterating results triggers a fetch per book.
      descriptor.relationshipKeyPathsForPrefetching = [\.author]
      
      // Exclude unsaved in-context changes (default true).
      descriptor.includePendingChanges = false
      
      let books = try context.fetch(descriptor)
      ```
      
      `relationshipKeyPathsForPrefetching` is the highest-leverage one: a list view that shows `book.author.name` for 500 rows issues 500 extra fetches without it.
      
      Also available: `fetchOffset` for paging alongside `fetchLimit`, and `context.fetchIdentifiers(_:)` when you only need `PersistentIdentifier`s (much cheaper than materializing models - useful for diffing or cross-context handoff).
      
      ## Relationship cardinality
      
      `@Relationship` takes more than `deleteRule` and `inverse`:
      
      ```swift
      @Relationship(
          deleteRule: .cascade,
          minimumModelCount: 1,        // enforce at least one
          maximumModelCount: 10,       // and at most ten
          inverse: \Chapter.book
      )
      var chapters: [Chapter] = []
      ```
      
      `minimumModelCount` / `maximumModelCount` express cardinality constraints in the schema itself. `hashModifier` (on both `@Relationship` and `@Attribute`) forces a property to be treated as changed for migration purposes without renaming it.
      
    • screen-capture-audio.md 63.1 KB
      # Screen Capture & Audio Recording
      
      For per-process system audio without ScreenCaptureKit (call recording, background taps), see `core-audio-tap.md` - CATap is the lower-overhead alternative and has its own HFP / aggregate-device pitfalls.
      
      ## Table of Contents
      - SCShareableContent - Content Discovery
      - SCContentFilter - Filtering
      - SCStream & SCStreamConfiguration
      - SCStreamOutput - Receiving Samples
      - SCStream Production Gotchas
      - SCStream Teardown Gotchas
      - SCRecordingOutput (macOS 15+)
      - SCContentSharingPicker (macOS 14+)
      - SCScreenshotManager (macOS 14+)
      - Audio-Only Capture Pattern
      - AVAudioEngine for Mic (Dual Pipeline)
      - AVAssetWriter for Audio
      - AVAssetWriter Crash Safety
      - AVAudioFile for Audio
      - CMSampleBuffer to AVAudioPCMBuffer
      - AVAudioPCMBuffer to CMSampleBuffer (for AVAssetWriter)
      - Audio Format Settings
      - Permissions
      - TCC Operational Gotchas
      - Complete Examples
      
      ## SCShareableContent - Content Discovery
      
      ```swift
      import ScreenCaptureKit
      
      // Enumerate available content (macOS 12.3+)
      let content = try await SCShareableContent.excludingDesktopWindows(
          false, onScreenWindowsOnly: true
      )
      
      content.displays       // [SCDisplay]
      content.windows        // [SCWindow]
      content.applications   // [SCRunningApplication]
      
      // Async property (macOS 14+)
      let content = try await SCShareableContent.current
      ```
      
      Key types:
      
      ```swift
      // SCDisplay
      display.displayID      // CGDirectDisplayID
      display.width          // Int
      display.height         // Int
      
      // SCRunningApplication
      app.bundleIdentifier   // String
      app.applicationName    // String
      app.processID          // pid_t
      
      // SCWindow
      window.windowID        // CGWindowID
      window.title           // String?
      window.isOnScreen      // Bool
      window.owningApplication // SCRunningApplication?
      ```
      
      **Note**: `SCShareableContent` calls trigger the screen recording permission prompt if not yet granted. First call after granting permission requires app restart.
      
      ## SCContentFilter - Filtering
      
      ```swift
      // Single window (follows across displays)
      let filter = SCContentFilter(desktopIndependentWindow: window)
      
      // Specific apps on a display
      let filter = SCContentFilter(
          display: display,
          including: [app1, app2],
          exceptingWindows: []
      )
      
      // Full display, exclude own app
      let excludedApps = content.applications.filter {
          Bundle.main.bundleIdentifier == $0.bundleIdentifier
      }
      let filter = SCContentFilter(
          display: display,
          excludingApplications: excludedApps,
          exceptingWindows: []
      )
      
      // Specific windows on a display
      let filter = SCContentFilter(display: display, including: [window1, window2])
      
      // Display minus specific windows
      let filter = SCContentFilter(display: display, excludingWindows: [windowToExclude])
      ```
      
      | Use Case | Initializer |
      |----------|------------|
      | Single window (follows across displays) | `init(desktopIndependentWindow:)` |
      | Entire display minus own app | `init(display:excludingApplications:exceptingWindows:)` |
      | Specific apps only | `init(display:including:exceptingWindows:)` |
      | Specific windows | `init(display:including:)` |
      
      Audio filtering works at the **application level** - the filter determines which apps' audio is captured.
      
      ## SCStream & SCStreamConfiguration
      
      ### Configuration
      
      ```swift
      let config = SCStreamConfiguration()
      
      // Video
      config.width = 1920
      config.height = 1080
      config.minimumFrameInterval = CMTime(value: 1, timescale: 60) // 60 fps
      config.pixelFormat = kCVPixelFormatType_32BGRA
      config.queueDepth = 5              // max frames in queue (default 3, max 8)
      config.showsCursor = true
      config.scalesToFit = true
      
      // Audio (macOS 12.3+)
      config.capturesAudio = true
      config.sampleRate = 48000          // up to 48kHz
      config.channelCount = 2            // stereo
      config.excludesCurrentProcessAudio = true
      
      // Microphone (macOS 15+)
      config.captureMicrophone = true
      config.microphoneCaptureDeviceID = AVCaptureDevice.default(for: .audio)?.uniqueID
      
      // HDR (macOS 15+)
      config.captureDynamicRange = .hdrCanonicalDisplay
      
      // Resolution (macOS 14+)
      config.captureResolution = .best
      ```
      
      Configuration presets (macOS 15+):
      
      ```swift
      let config = SCStreamConfiguration(preset: .captureHDRStreamCanonicalDisplay)
      let config = SCStreamConfiguration(preset: .captureHDRScreenshotLocalDisplay)
      ```
      
      ### Stream lifecycle
      
      ```swift
      // Create
      let stream = SCStream(filter: filter, configuration: config, delegate: self)
      
      // Add outputs
      try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: videoQueue)
      try stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: audioQueue)
      try stream.addStreamOutput(self, type: .microphone, sampleHandlerQueue: micQueue) // macOS 15+
      
      // Start/stop
      try await stream.startCapture()
      try await stream.stopCapture()
      
      // Update without restart
      try await stream.updateConfiguration(newConfig)
      try await stream.updateContentFilter(newFilter)
      ```
      
      ### SCStreamDelegate
      
      ```swift
      extension CaptureManager: SCStreamDelegate {
          func stream(_ stream: SCStream, didStopWithError error: Error) {
              // Stream stopped unexpectedly
          }
      }
      ```
      
      ## SCStreamOutput - Receiving Samples
      
      ```swift
      class CaptureEngine: NSObject, SCStreamOutput {
      
          func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer,
                      of type: SCStreamOutputType) {
              guard sampleBuffer.isValid else { return }
      
              switch type {
              case .screen:
                  handleVideo(sampleBuffer)
              case .audio:
                  handleAudio(sampleBuffer)
              case .microphone:  // macOS 15+
                  handleMicrophone(sampleBuffer)
              @unknown default:
                  break
              }
          }
      }
      ```
      
      ### Processing video buffers
      
      ```swift
      private func handleVideo(_ sampleBuffer: CMSampleBuffer) {
          guard let attachments = CMSampleBufferGetSampleAttachmentsArray(
              sampleBuffer, createIfNecessary: false
          ) as? [[SCStreamFrameInfo: Any]],
          let status = attachments.first?[.status] as? Int,
          SCFrameStatus(rawValue: status) == .complete,
          let pixelBuffer = sampleBuffer.imageBuffer else { return }
      
          let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue()
          let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
      }
      ```
      
      ### Processing audio buffers
      
      ```swift
      private func handleAudio(_ sampleBuffer: CMSampleBuffer) {
          try? sampleBuffer.withAudioBufferList { audioBufferList, blockBuffer in
              guard let desc = sampleBuffer.formatDescription?.audioStreamBasicDescription,
                    let format = AVAudioFormat(
                        standardFormatWithSampleRate: desc.mSampleRate,
                        channels: desc.mChannelsPerFrame
                    ),
                    let pcmBuffer = AVAudioPCMBuffer(
                        pcmFormat: format,
                        bufferListNoCopy: audioBufferList.unsafePointer
                    )
              else { return }
              // Use pcmBuffer for processing, level metering, or writing
          }
      }
      ```
      
      ## SCStream Production Gotchas
      
      ### SCStream is NOT reusable after error
      
      After `didStopWithError`, the XPC connection to `replayd` is invalidated. Calling `startCapture()` again throws `attemptToStartStreamState`. You must destroy the stream and create a new one:
      
      ```swift
      func stream(_ stream: SCStream, didStopWithError error: Error) {
          // DO NOT try stream.startCapture() - it will throw
          self.stream = nil // Release the dead stream
          Task { try await restartWithNewStream() } // Create fresh SCStream
      }
      ```
      
      ### SCRecordingOutput stops on updateConfiguration()
      
      `SCRecordingOutput` stops recording when `SCStreamConfiguration` is updated on a running stream. This is documented in the Apple header. If your app needs mid-stream config changes (device following, resolution changes), use manual `SCStreamOutput` + `AVAssetWriter` instead.
      
      ### VPIO and SCStream are fundamentally incompatible
      
      `AVAudioEngine.setVoiceProcessingEnabled(true)` creates a hidden VPIO aggregate device that hooks into the system audio output path for its AEC reference signal. This silences SCStream's system audio capture. Do not use VPIO and SCStream together. Use post-processing AEC or an independent mic pipeline instead.
      
      ### SCStream .microphone output type is unreliable for dual-track
      
      The `.microphone` SCStreamOutputType (macOS 15+) can produce duration mismatches and data corruption when written as a second AVAssetWriterInput alongside system audio. Use an independent AVAudioEngine pipeline for mic capture instead (see "AVAudioEngine for Mic" section).
      
      ### Multiple SCStreams can run simultaneously
      
      Two `SCStream` instances (e.g., one display-wide, one per-app) can run from the same process. Each is an independent XPC connection to the ScreenCaptureKit daemon. Both start without error and deliver audio buffers concurrently.
      
      ### Virtual audio processors cause duplication in display-wide capture
      
      Virtual audio devices (Krisp, SoundSource) process audio with latency (~50ms). Display-wide capture picks up both the original app output and the virtual device's delayed copy, causing audible echo. Fix: use per-app `SCContentFilter(display:including:[specificApp])` to exclude virtual audio processors.
      
      ### Chrome/Electron helper bundle ID resolution
      
      CoreAudio and ScreenCaptureKit report Chrome's renderer subprocess as the audio client (`com.google.Chrome.helper.renderer`). Strip `.helper*` suffix to resolve to the parent app for `SCContentFilter` lookup:
      
      ```swift
      func resolveParentBundleID(_ bundleID: String) -> String {
          if let range = bundleID.range(of: ".helper", options: .literal) {
              return String(bundleID[..<range.lowerBound])
          }
          return bundleID
      }
      // "com.google.Chrome.helper.renderer" -> "com.google.Chrome"
      ```
      
      ### Never mutate SCStream's CMSampleBuffer in-place
      
      SCStream sample buffers are framework-managed and potentially shared. Writing into them via `CMBlockBufferGetDataPointer` is undefined behavior. Use dual-track recording (separate AVAssetWriterInputs) instead of mixing into existing buffers.
      
      ### SCStream error codes and recovery
      
      Full mapping from `SCError.h` in the macOS 26 SDK. Source (local path on any machine with Xcode 26 installed): `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/ScreenCaptureKit.framework/Versions/A/Headers/SCError.h`. API docs: https://developer.apple.com/documentation/screencapturekit/scstreamerror/code
      
      | Code | Enum case | Since | Meaning |
      |------|-----------|-------|---------|
      | -3801 | `userDeclined` | 12.3 | User did not authorize capture (TCC) |
      | -3802 | `failedToStart` | 12.3 | Stream failed to start (generic) |
      | -3803 | `missingEntitlements` | 12.3 | Missing required entitlements |
      | -3804 | `failedApplicationConnectionInvalid` | 12.3 | Recording connection became invalid |
      | -3805 | `failedApplicationConnectionInterrupted` | 12.3 | Recording connection was interrupted |
      | -3806 | `failedNoMatchingApplicationContext` | 12.3 | Context id does not match application |
      | -3807 | `attemptToStartStreamState` | 12.3 | Start attempted on a stream already running |
      | -3808 | `attemptToStopStreamState` | 12.3 | Stop attempted on a stream already stopped |
      | -3809 | `attemptToUpdateFilterState` | 12.3 | Update-filter attempted on stopped stream |
      | -3810 | `attemptToConfigState` | 12.3 | Update-config attempted on stopped stream |
      | -3811 | `internalError` | 12.3 | Video/audio capture failure |
      | -3812 | `invalidParameter` | 12.3 | Invalid parameter |
      | -3813 | `noWindowList` | 12.3 | No window list available |
      | -3814 | `noDisplayList` | 12.3 | No display list available |
      | -3815 | `noCaptureSource` | 12.3 | No display or window list to capture |
      | -3816 | `removingStream` | 12.3 | Failed to remove stream |
      | -3817 | `userStopped` | 12.3 | User stopped via system UI |
      | -3818 | `failedToStartAudioCapture` | 13.0 | Audio capture failed to start |
      | -3819 | `failedToStopAudioCapture` | 13.0 | Audio capture failed to stop |
      | -3820 | `failedToStartMicrophoneCapture` | 15.0 | Microphone capture failed to start |
      | -3821 | `systemStoppedStream` | 15.0 | System stopped the stream (sleep/wake, policy) |
      
      Recovery rules of thumb: `-3801` / `-3803` → permission or entitlement problem, stop and prompt user. `-3817` → user intent, save and stop. `-3818` / `-3820` → restart without the offending track. `-3821` → wait ~1 s and restart with a new stream. Rate-limit restarts (e.g. max 3 in 30 s) to prevent infinite loops. Remember SCStream is not reusable after error — destroy and recreate.
      
      Keep `mappedStartError` and the stop-time error handler in sync - in practice they drift (stop-time maps -3801/-3802/-3821, start-time only maps -3801), producing misleading user-facing error messages.
      
      ## SCStream Teardown Gotchas
      
      Teardown is the phase where SCStream apps lose files, hang, and leak state. Three patterns worth internalizing.
      
      ### `stopCapture()` can hang; wrap it in a timeout
      
      Under WindowServer stalls or TCC-revoked mid-stream state, `try await stream.stopCapture()` can block for 10+ seconds. `applicationShouldTerminate:` gives the app ~8 seconds total before force-kill, so an unguarded `stopCapture` call blows the budget and the writer never finalizes (partial file, or nothing).
      
      Wrap it in a bounded `withTimeout`:
      
      ```swift
      func stopSafely() async {
          _ = await withTimeout(seconds: 3) {
              try? await self.stream?.stopCapture()
          }
          self.audioInput?.markAsFinished()
          _ = await withTimeout(seconds: 3) {
              await self.writer?.finishWriting()
          }
      }
      
      func withTimeout<T: Sendable>(seconds: TimeInterval,
                                    _ op: @escaping @Sendable () async -> T) async -> T? {
          await withTaskGroup(of: T?.self) { group in
              group.addTask { await op() }
              group.addTask {
                  try? await Task.sleep(for: .seconds(seconds))
                  return nil
              }
              let result = await group.next() ?? nil
              group.cancelAll()
              return result
          }
      }
      ```
      
      ### Assign the stream reference *before* awaiting `startCapture()`
      
      ```swift
      // FRAGILE: if TCC was revoked between preflight and start, catch cannot cleanly stop.
      func start() async throws {
          let s = SCStream(filter: filter, configuration: config, delegate: self)
          try s.addStreamOutput(self, type: .audio, sampleHandlerQueue: audioQueue)
          do {
              try await s.startCapture()
              self.stream = s // assigned after success only
          } catch {
              // 's' is out of scope or half-initialized; hard to drive cleanup.
              throw error
          }
      }
      
      // BETTER: assign first, so the catch can drive teardown uniformly.
      func start() async throws {
          let s = SCStream(filter: filter, configuration: config, delegate: self)
          try s.addStreamOutput(self, type: .audio, sampleHandlerQueue: audioQueue)
          self.stream = s
          do {
              try await s.startCapture()
          } catch {
              await self.stopSafely() // cleans up self.stream uniformly
              throw error
          }
      }
      ```
      
      **Trade-off**: assigning before `startCapture` exposes a short window where `self.stream` is non-nil but not yet capturing. A concurrent `stop()` during that window must be tolerant of `stopCapture()` on an unstarted stream (SCStream handles this, but the undocumented error path is worth a test).
      
      ### Prefer `SCShareableContent.current` over `excludingDesktopWindows(false, false)` in restart paths
      
      `SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false)` is the slow variant - it enumerates off-screen windows, virtual desktops, and minimized windows. Auto-restart paths (up to 3 restarts in 30 s after `.systemStopped`) that re-query this on every attempt produce visible UI stalls.
      
      For audio streams you typically don't need the window list at all; cache the display handle at first start and reuse it. When you do need a fresh enumeration, prefer the narrower `SCShareableContent.current` (macOS 14+) or `excludingDesktopWindows(true, onScreenWindowsOnly: true)`.
      
      ```swift
      // Keep this cached across restarts:
      private var cachedDisplay: SCDisplay?
      
      func refreshDisplayIfNeeded() async throws -> SCDisplay {
          if let d = cachedDisplay { return d }
          let content = try await SCShareableContent.current // fast
          guard let d = content.displays.first else { throw CaptureError.noDisplay }
          cachedDisplay = d
          return d
      }
      ```
      
      ## SCRecordingOutput (macOS 15+)
      
      Simplified file recording without manual AVAssetWriter buffer handling.
      
      ```swift
      // Configure
      let recordingConfig = SCRecordingOutputConfiguration()
      recordingConfig.outputURL = fileURL          // file URL, not folder
      recordingConfig.outputFileType = .mov        // .mov or .mp4
      recordingConfig.videoCodecType = .hevc       // .hevc or .h264
      
      // Create
      let recordingOutput = SCRecordingOutput(
          configuration: recordingConfig, delegate: self
      )
      
      // Add to stream BEFORE startCapture for guaranteed first-frame capture
      try stream.addRecordingOutput(recordingOutput)
      try await stream.startCapture()
      
      // Monitor
      recordingOutput.recordedDuration  // CMTime
      recordingOutput.recordedFileSize  // Int64
      
      // Stop recording only (keep streaming)
      try stream.removeRecordingOutput(recordingOutput)
      
      // Or stop everything
      try await stream.stopCapture()
      ```
      
      ### SCRecordingOutputDelegate
      
      ```swift
      extension CaptureManager: SCRecordingOutputDelegate {
          func recordingOutputDidStartRecording(_ output: SCRecordingOutput) { }
          func recordingOutput(_ output: SCRecordingOutput, didFailWithError error: Error) { }
          func recordingOutputDidFinishRecording(_ output: SCRecordingOutput) {
              // File is ready at outputURL
          }
      }
      ```
      
      **Caveats**: Only ONE recording output per stream. Handles video recording; audio-only recording still uses AVAudioFile/AVAssetWriter. Updating `SCStreamConfiguration` on a running stream stops the recording.
      
      ## SCContentSharingPicker (macOS 14+)
      
      System-provided picker UI for selecting content. Apple's preferred approach in macOS 15+.
      
      ```swift
      let picker = SCContentSharingPicker.shared
      picker.add(self)        // SCContentSharingPickerObserver
      picker.isActive = true
      
      // Present
      picker.present()
      picker.present(using: .window)         // specific style
      picker.present(for: existingStream)    // for existing stream
      
      // Configure
      let pickerConfig = SCContentSharingPickerConfiguration()
      pickerConfig.allowedPickerModes = [.singleWindow, .multipleWindows, .singleApplication]
      pickerConfig.excludedBundleIDs = ["com.example.excluded"]
      picker.defaultConfiguration = pickerConfig
      ```
      
      ### Observer
      
      ```swift
      extension CaptureManager: SCContentSharingPickerObserver {
          func contentSharingPicker(_ picker: SCContentSharingPicker,
                                    didUpdateWith filter: SCContentFilter,
                                    for stream: SCStream?) {
              if let stream {
                  try? await stream.updateContentFilter(filter)
              } else {
                  // Create new stream with filter
              }
          }
      
          func contentSharingPicker(_ picker: SCContentSharingPicker,
                                    didCancel forStream: SCStream?) { }
      
          func contentSharingPickerStartDidFailWithError(_ error: Error) { }
      }
      ```
      
      ## SCScreenshotManager (macOS 14+)
      
      Single-frame capture without a persistent stream.
      
      ```swift
      let image: CGImage = try await SCScreenshotManager.captureImage(
          contentFilter: filter, configuration: config
      )
      
      let buffer: CMSampleBuffer = try await SCScreenshotManager.captureSampleBuffer(
          contentFilter: filter, configuration: config
      )
      
      // HDR (macOS 15+)
      let hdrConfig = SCStreamConfiguration(preset: .captureHDRScreenshotLocalDisplay)
      let hdrImage = try await SCScreenshotManager.captureImage(
          contentFilter: filter, configuration: hdrConfig
      )
      ```
      
      ## Audio-Only Capture Pattern
      
      **ScreenCaptureKit has no documented audio-only mode, but `.audio`-only streams work in practice.** You can create an `SCStream` with `capturesAudio = true` and attach only `addStreamOutput(_:type:.audio,...)` - no `.screen` output. Audio buffers flow. Validated across macOS 14/15/26 in multiple shipping apps (Aperture, Blackbox across v0.3/v0.4/v0.6/v0.8, etc.).
      
      Two caveats, and the second one is not cosmetic:
      1. The framework logs `stream output NOT found. Dropping frame` for every video frame when no `.screen` output is attached, because the **video pipeline still runs** internally. Attach a `.screen` output that ignores its buffers (on its own queue, not the audio queue) and the log line goes away.
      2. That still-running video pipeline costs real CPU: every frame is a WindowServer recomposite of the whole display. At the display's refresh rate on a 5K monitor this is ~15-20% of a core across the app, `replayd`, and WindowServer for the entire recording. Throttle it with `minimumFrameInterval` (below).
      
      **`minimumFrameInterval` trap.** The property is the *minimum time between frames*, so a larger value means fewer frames. `CMTime(value: 1, timescale: CMTimeScale.max)` - which reads like "infinite interval" - is ~0.5 ns, the smallest positive interval expressible, and the SDK documents `kCMTimeZero` as "capture at display's native refresh rate". It requests the *maximum* frame rate. One shipping recorder ran with that line from v0.8.0 to v0.9.1 before a user measured the 60 fps recomposite (blackbox#17). `CMTime(value: 1, timescale: 1)` is 1 fps; same shape as the `1/60` used for 60 fps elsewhere in this file.
      
      **Do not reach for CATap as the "zero-overhead" replacement without reading `core-audio-tap.md` first.** Production experience (one call-recorder shipped CATap in v0.7.0 and reverted to display-wide SCStream in v0.8.0 five days later) shows CATap has structural clock-fragility: its IO proc is driven by the hardware output clock, so when that clock is idle, pinned by Bluetooth HFP, or stalled, buffers stop flowing silently. Display-wide SCStream's clock comes from the OS-composited mix and is decoupled from hardware output, making it more robust for long-duration recording even with the cosmetic video-pipeline overhead. CATap is the right tool when you specifically need sub-20 ms capture latency (real-time AEC, live analysis); for disk-bound recording workloads (calls, meetings, lectures), SCStream wins on reliability.
      
      If you stay with SCStream audio-only, minimize the hidden video work:
      
      ```swift
      let config = SCStreamConfiguration()
      config.capturesAudio = true
      config.sampleRate = 48000
      config.channelCount = 2
      config.excludesCurrentProcessAudio = true
      
      // Minimal video: 2x2 at 1 fps. NOT 1/Int32.max - that is ~0s = native refresh rate.
      config.width = 2
      config.height = 2
      config.minimumFrameInterval = CMTime(value: 1, timescale: 1)
      
      // Subscribe .screen even for audio-only (ignore its buffers), or SCStream logs a
      // dropped frame per frame. Keep it off the audio queue so video can never delay audio.
      try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: videoQueue)
      try stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: audioQueue)
      ```
      
      **App-specific audio**: use `SCContentFilter(display:including:exceptingWindows:)` with specific apps to capture only their audio output.
      
      ## AVAudioEngine for Mic (Dual Pipeline)
      
      For dual-track recording (system audio + mic), use SCStream for system audio and AVAudioEngine for mic independently. This avoids SCStream `.microphone` output reliability issues and VPIO incompatibility.
      
      ```swift
      class DualTrackRecorder {
          private var engine = AVAudioEngine()
          private let audioQueue = DispatchQueue(label: "AudioCapture")
      
          func startMicCapture() throws {
              let inputNode = engine.inputNode
              let format = inputNode.inputFormat(forBus: 0)
      
              // CRITICAL: Extract closure to @Sendable to avoid MainActor isolation inheritance
              let tapHandler: @Sendable (AVAudioPCMBuffer, AVAudioTime) -> Void = { [weak self] buffer, time in
                  self?.audioQueue.async { self?.handleMicBuffer(buffer, time: time) }
              }
              inputNode.installTap(onBus: 0, bufferSize: 1024, format: format, block: tapHandler)
      
              engine.prepare()
              try engine.start()
          }
      }
      ```
      
      ### AVAudioEngine device following
      
      When audio hardware changes (headphone plug/unplug, Bluetooth connect), handle `AVAudioEngineConfigurationChange`:
      
      ```swift
      // Use queue: .main to avoid isolation inheritance crash
      NotificationCenter.default.addObserver(
          forName: .AVAudioEngineConfigurationChange, object: engine, queue: .main
      ) { [weak self] _ in
          self?.handleConfigChange()
      }
      
      func handleConfigChange() {
          // Use the NEW device's native format, not the stored old one
          let newFormat = engine.inputNode.inputFormat(forBus: 0)
          guard newFormat.sampleRate > 0, newFormat.channelCount > 0 else { return }
          engine.inputNode.removeTap(onBus: 0)
          // Reinstall tap with new format... (see ObjC wrapper section below)
      }
      ```
      
      Debounce config changes (200-500ms) - virtual devices like Krisp fire rapid sequences.
      
      ### installTap throws ObjC NSException, not Swift Error
      
      `AVAudioEngine.installTap(onBus:)` throws an ObjC `NSException` when the format is incompatible. Swift `do/catch` does NOT catch NSExceptions. A generic `ObjCTryBlock` wrapper also fails because the Swift compiler eliminates the ObjC trampoline for `NS_NOESCAPE` blocks in release builds.
      
      Fix: purpose-built ObjC wrapper methods where the entire throw-to-catch chain is pure ObjC:
      
      ```objc
      // ObjCExceptionCatcher.h
      #import <AVFAudio/AVFAudio.h>
      BOOL ObjCInstallTap(AVAudioNode *node, uint32_t bus, uint32_t bufferSize,
                          AVAudioFormat * _Nullable format,
                          void (^block)(AVAudioPCMBuffer *, AVAudioTime *),
                          NSError **outError);
      BOOL ObjCStartEngine(AVAudioEngine *engine, NSError **outError);
      ```
      
      ```objc
      // ObjCExceptionCatcher.m
      BOOL ObjCInstallTap(AVAudioNode *node, uint32_t bus, uint32_t bufferSize,
                          AVAudioFormat *format,
                          void (^block)(AVAudioPCMBuffer *, AVAudioTime *),
                          NSError **outError) {
          @try {
              [node installTapOnBus:bus bufferSize:bufferSize format:format block:block];
              return YES;
          } @catch (NSException *e) {
              if (outError)
                  *outError = [NSError errorWithDomain:@"ObjCException" code:-1
                      userInfo:@{NSLocalizedDescriptionKey: e.reason ?: e.name}];
              return NO;
          }
      }
      ```
      
      In SPM, create a separate target for the ObjC code:
      ```swift
      .target(
          name: "ObjCExceptionCatcher",
          path: "Sources/ObjCExceptionCatcher",
          publicHeadersPath: "include",
          linkerSettings: [.linkedFramework("AVFAudio")]
      )
      ```
      
      ### VPIO aggregate device reports bogus channel counts
      
      `setVoiceProcessingEnabled(true)` reports combined input+output channels (e.g., 9 = mic + speaker). Pass `nil` as format to `installTap` to let VPIO negotiate internally:
      
      ```swift
      inputNode.installTap(onBus: 0, bufferSize: 1024, format: nil, block: handler)
      ```
      
      ## AVAssetWriter for Audio
      
      **Preferred for ScreenCaptureKit** - accepts CMSampleBuffer directly (no conversion), supports compressed output.
      
      ```swift
      let writer = try AVAssetWriter(url: outputURL, fileType: .m4a)
      
      let audioSettings: [String: Any] = [
          AVFormatIDKey: kAudioFormatMPEG4AAC,
          AVSampleRateKey: 48000.0,
          AVNumberOfChannelsKey: 2,
          AVEncoderBitRateKey: 128_000
      ]
      
      let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings)
      audioInput.expectsMediaDataInRealTime = true
      writer.add(audioInput)
      writer.startWriting()
      
      // In SCStreamOutput callback:
      func handleAudio(_ sampleBuffer: CMSampleBuffer) {
          if !sessionStarted {
              writer.startSession(atSourceTime: sampleBuffer.presentationTimeStamp)
              sessionStarted = true
          }
          if audioInput.isReadyForMoreMediaData {
              audioInput.append(sampleBuffer)
          }
      }
      
      // Stop
      audioInput.markAsFinished()
      await writer.finishWriting()
      ```
      
      File types: `.m4a` (AAC/ALAC), `.mov` (PCM/AAC/ALAC), `.mp4` (AAC), `.wav` (PCM), `.caf` (any).
      
      Pass `nil` for `outputSettings` to write without re-encoding (pass-through).
      
      ## AVAssetWriter Crash Safety
      
      ### movieFragmentInterval for crash recovery
      
      Writes fragment headers periodically, making partial files recoverable after crashes or force-kills. Works with `.m4a` container (empirically verified - not just `.mov`):
      
      ```swift
      writer.movieFragmentInterval = CMTime(seconds: 10, preferredTimescale: 600)
      ```
      
      A force-killed recording produces a valid file with audio up to the last fragment boundary (~10s max data loss).
      
      ### expectsMediaDataInRealTime is critical
      
      Always set `expectsMediaDataInRealTime = true` on inputs, even for post-processing pipelines. Without it, the writer applies internal backpressure that can deadlock synchronous polling loops:
      
      ```swift
      audioInput.expectsMediaDataInRealTime = true // Prevents backpressure deadlock
      ```
      
      ### Guard writer status in polling loops
      
      After `input.append()` fails, the writer enters `.failed` state and `isReadyForMoreMediaData` returns `false` forever. Without a status guard, polling loops hang infinitely:
      
      ```swift
      // BAD - hangs forever if writer fails:
      while !input.isReadyForMoreMediaData { usleep(10_000) }
      
      // GOOD - break on writer failure:
      while !input.isReadyForMoreMediaData {
          guard writer.status == .writing else { break }
          usleep(10_000)
      }
      ```
      
      ### Session start timing for multi-track recording
      
      With dual-track (system audio + mic), start the session on the first system audio sample. Gate mic buffer appending on a `sessionStarted` flag. Mic samples arriving before session start must be dropped:
      
      ```swift
      func handleSystemAudio(_ sampleBuffer: CMSampleBuffer) {
          if !sessionStarted {
              writer.startSession(atSourceTime: sampleBuffer.presentationTimeStamp)
              sessionStarted = true
          }
          systemInput.append(sampleBuffer)
      }
      
      func handleMic(_ sampleBuffer: CMSampleBuffer) {
          guard sessionStarted else { return } // Drop pre-session mic samples
          micInput.append(sampleBuffer)
      }
      ```
      
      ### Channel count mismatch causes silent audio
      
      Output settings must match actual input format. AVAudioEngine delivers mono (1ch) mic audio. Configuring the writer input for stereo (2ch) causes silent output with 100% append success rate:
      
      ```swift
      // System audio: 2ch stereo
      let systemSettings: [String: Any] = [
          AVFormatIDKey: kAudioFormatMPEG4AAC,
          AVNumberOfChannelsKey: 2, AVEncoderBitRateKey: 128_000, ...
      ]
      // Mic audio: 1ch mono (matches AVAudioEngine input)
      let micSettings: [String: Any] = [
          AVFormatIDKey: kAudioFormatMPEG4AAC,
          AVNumberOfChannelsKey: 1, AVEncoderBitRateKey: 64_000, ...
      ]
      ```
      
      ### CMBlockBuffer memory trap
      
      Never use `CMBlockBufferCreateWithMemoryBlock` with `flags: 0` and `memoryBlock: nil` - this defers memory allocation and `CMBlockBufferReplaceDataBytes` writes to uninitialized memory. Use `kCMBlockBufferAssureMemoryNowFlag` or the `CMSampleBufferSetDataBufferFromAudioBufferList` pattern (see next section).
      
      ### AVAssetWriterInput format is immutable after the first append
      
      The internal AAC encoder configures itself from the **first** appended sample buffer's format description. Appending a buffer with a different format later causes `append()` to fail silently (returns `false` and the writer eventually enters `.failed` state, losing **both** tracks in a dual-track output).
      
      This matters concretely when the mic device changes mid-recording:
      - AVAudioEngine rebuilds its tap on `AVAudioEngineConfigurationChange` with whatever the new device's native format is (different sample rate, channel count).
      - If you let that new format flow into the writer input directly, the writer goes to `.failed`.
      
      The fix: **resample/downmix into a fixed target format** (e.g. mono 48 kHz Float32) before appending, and reinstall the tap with whatever source format the device dictates. One production-hardened path: mic tap → `resampleToMono48k()` (linear interpolation, any source rate → 48 kHz mono) → `asSampleBuffer()` → writer input. System track (SCStream) bypasses resampling since SCStream always delivers at the configured `sampleRate`/`channelCount` regardless of hardware.
      
      ### AVAssetWriter collapses PTS gaps; fill with explicit silence
      
      No AVAssetWriter property makes it preserve gaps between buffers. If buffer A ends at t=10.0 s and buffer B arrives with PTS=12.5 s, the AAC encoder writes them back-to-back and the track ends up 2.5 s shorter than it should be. In a dual-track recording this manifests as one track being seconds shorter than the other, growing over the course of the recording.
      
      Detection / fill pattern (run on the same queue as `append`, per input):
      
      ```swift
      actor TrackState {
          var nextExpectedPTS: CMTime = .invalid
          let input: AVAssetWriterInput
          let silenceFormat: CMFormatDescription   // clean LPCM, built once
      
          func append(_ sample: CMSampleBuffer) {
              let incoming = CMSampleBufferGetPresentationTimeStamp(sample)
              if nextExpectedPTS.isValid,
                 CMTimeCompare(incoming, nextExpectedPTS) > 0,
                 CMTimeGetSeconds(incoming - nextExpectedPTS) > 0.005 {
                  fillGap(from: nextExpectedPTS, to: incoming)
              }
              if input.isReadyForMoreMediaData {
                  input.append(sample)
              }
              let dur = CMSampleBufferGetDuration(sample)
              nextExpectedPTS = incoming + dur
          }
      
          private func fillGap(from start: CMTime, to end: CMTime) {
              // Write SMALL chunks (~1024 samples each). One big silent buffer spanning
              // the whole gap causes kCMSampleBufferError_ArrayTooSmall (-12737) or
              // crashes the AAC encoder. Bail out on any failure - never risk the
              // real buffer to chase gap accuracy.
              var cursor = start
              while CMTimeCompare(cursor, end) < 0, input.isReadyForMoreMediaData {
                  let silent = makeSilentSampleBuffer(at: cursor, frames: 1024,
                                                       format: silenceFormat)
                  if !input.append(silent) { break }
                  cursor = cursor + CMTime(value: 1024, timescale: 48_000)
              }
          }
      }
      ```
      
      Three constraints the pattern *must* respect:
      1. **Write silence in small chunks** matching normal buffer cadence (~1024 samples at 48 kHz). One large silent buffer covering the full gap fails with `kCMSampleBufferError_ArrayTooSmall` or crashes the AAC encoder.
      2. **Build a clean LPCM `CMFormatDescription` from scratch** for the silence (Float32, packed, interleaved, no channel-layout extensions). Do NOT reuse the format description from pipeline buffers - they may carry channel layouts or non-interleaved flags that don't match a flat zero-filled block buffer, and `input.append()` will reject or the writer will fail.
      3. **Never block the real buffer on gap-fill success.** If silence `append` fails or `isReadyForMoreMediaData` goes false mid-fill, break out and still try the real buffer. Accept partial desync over data loss. Log the partial fill for diagnostics.
      
      Gap detection threshold >5 ms (240 samples at 48 kHz) avoids false positives from normal PTS jitter.
      
      ## AVAudioFile for Audio
      
      Simpler but PCM-only, requires CMSampleBuffer-to-AVAudioPCMBuffer conversion.
      
      ```swift
      let settings: [String: Any] = [
          AVFormatIDKey: kAudioFormatLinearPCM,
          AVSampleRateKey: 48000.0,
          AVNumberOfChannelsKey: 2,
          AVLinearPCMBitDepthKey: 32,
          AVLinearPCMIsFloatKey: true,
          AVLinearPCMIsBigEndianKey: false,
          AVLinearPCMIsNonInterleaved: false
      ]
      
      let audioFile = try AVAudioFile(
          forWriting: url,
          settings: settings,
          commonFormat: .pcmFormatFloat32,
          interleaved: false  // AVAudioFile requires non-interleaved
      )
      
      // In callback, after converting CMSampleBuffer to AVAudioPCMBuffer:
      try audioFile.write(from: pcmBuffer)
      
      // Close by setting to nil
      audioFile = nil
      ```
      
      **`settings`** = file format on disk. **`commonFormat`** = processing format of buffers passed to `write(from:)`.
      
      ## CMSampleBuffer to AVAudioPCMBuffer
      
      **If audio is silent after the conversion, check this first.** SCStream on macOS 26 delivers Float32 stereo as **non-interleaved** (two separate buffers in the `AudioBufferList`). Code written for interleaved layout that `memcpy`s the `CMBlockBuffer` bytes into `floatChannelData[0]` produces a valid-looking PCM buffer that decodes as silence / garbage. This has silently killed multi-minute recordings in production.
      
      The safe one-liner: let CoreMedia copy into the `AudioBufferList` and handle both layouts:
      
      ```swift
      extension AVAudioPCMBuffer {
          static func from(_ sampleBuffer: CMSampleBuffer) -> AVAudioPCMBuffer? {
              guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) else {
                  return nil
              }
              let numSamples = CMSampleBufferGetNumSamples(sampleBuffer)
              let avFormat = AVAudioFormat(cmAudioFormatDescription: formatDescription)
      
              guard let pcmBuffer = AVAudioPCMBuffer(
                  pcmFormat: avFormat,
                  frameCapacity: AVAudioFrameCount(numSamples)
              ) else { return nil }
      
              pcmBuffer.frameLength = AVAudioFrameCount(numSamples)
              // Handles both interleaved and non-interleaved source layouts.
              CMSampleBufferCopyPCMDataIntoAudioBufferList(
                  sampleBuffer, at: 0, frameCount: Int32(numSamples),
                  into: pcmBuffer.mutableAudioBufferList
              )
              return pcmBuffer
          }
      }
      ```
      
      Better yet for AVAssetWriter destinations: **skip the conversion entirely** and append the `CMSampleBuffer` directly to a stereo AAC `AVAssetWriterInput` (see "AVAssetWriter for Audio"). Passthrough is both simpler and avoids every class of PCM-layout bug.
      
      Modern Swift alternative using `copyPCMData(fromRange:into:)`:
      
      ```swift
      try sampleBuffer.copyPCMData(
          fromRange: 0..<CMSampleBufferGetNumSamples(sampleBuffer),
          into: pcmBuffer.mutableAudioBufferList
      )
      ```
      
      ## AVAudioPCMBuffer to CMSampleBuffer (for AVAssetWriter)
      
      When writing AVAudioEngine tap output to AVAssetWriter, convert `AVAudioPCMBuffer` to `CMSampleBuffer`. Let CoreMedia manage block buffer memory:
      
      ```swift
      func makeSampleBuffer(from pcmBuffer: AVAudioPCMBuffer, time: AVAudioTime) -> CMSampleBuffer? {
          let format = pcmBuffer.format
          let frameCount = pcmBuffer.frameLength
      
          guard let formatDesc = format.formatDescription else { return nil }
      
          var sampleBuffer: CMSampleBuffer?
          var timing = CMSampleTimingInfo(
              duration: CMTime(value: 1, timescale: Int32(format.sampleRate)),
              presentationTimeStamp: CMTime(
                  seconds: AVAudioTime.seconds(forHostTime: time.hostTime),
                  preferredTimescale: 600
              ),
              decodeTimeStamp: .invalid
          )
      
          // Create empty sample buffer
          guard CMSampleBufferCreate(
              allocator: kCFAllocatorDefault, dataBuffer: nil, dataReady: false,
              makeDataReadyCallback: nil, refcon: nil,
              formatDescription: formatDesc,
              sampleCount: CMItemCount(frameCount),
              sampleTimingEntryCount: 1, sampleTimingArray: &timing,
              sampleSizeEntryCount: 0, sampleSizeArray: nil,
              sampleBufferOut: &sampleBuffer
          ) == noErr, let sb = sampleBuffer else { return nil }
      
          // Attach audio data (CoreMedia manages block buffer memory)
          guard CMSampleBufferSetDataBufferFromAudioBufferList(
              sb, blockBufferAllocator: kCFAllocatorDefault,
              blockBufferMemoryAllocator: kCFAllocatorDefault,
              flags: 0, bufferList: pcmBuffer.audioBufferList
          ) == noErr else { return nil }
      
          return sb
      }
      ```
      
      ## Audio Format Settings
      
      | Constant | Value | Container |
      |----------|-------|-----------|
      | `kAudioFormatLinearPCM` | Uncompressed PCM | WAV, CAF, AIFF |
      | `kAudioFormatMPEG4AAC` | AAC (lossy) | M4A, MP4 |
      | `kAudioFormatAppleLossless` | ALAC (lossless) | M4A, CAF |
      | `kAudioFormatFLAC` | FLAC lossless | FLAC, CAF |
      
      Common settings:
      
      ```swift
      // WAV (16-bit PCM)
      [AVFormatIDKey: kAudioFormatLinearPCM, AVSampleRateKey: 48000.0,
       AVNumberOfChannelsKey: 2, AVLinearPCMBitDepthKey: 16,
       AVLinearPCMIsFloatKey: false, AVLinearPCMIsBigEndianKey: false]
      
      // M4A (AAC)
      [AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 48000.0,
       AVNumberOfChannelsKey: 2, AVEncoderBitRateKey: 128_000]
      
      // M4A (Apple Lossless)
      [AVFormatIDKey: kAudioFormatAppleLossless, AVSampleRateKey: 48000.0,
       AVNumberOfChannelsKey: 2, AVEncoderBitDepthHintKey: 16]
      ```
      
      **AVAssetWriter note**: For `kAudioFormatLinearPCM` output, all `AVLinearPCM*` keys are required. For `kAudioFormatMPEG4AAC`, `AVEncoderBitRateKey` is required (`AVEncoderBitRatePerChannelKey` is NOT supported).
      
      ## Permissions
      
      ### Screen recording (TCC)
      
      Screen capture has NO dedicated entitlement. It is governed entirely by macOS TCC runtime permission.
      
      ```swift
      // Check (macOS 11+, does NOT trigger prompt)
      let hasAccess = CGPreflightScreenCaptureAccess()
      
      // Request (triggers system prompt once)
      CGRequestScreenCaptureAccess()
      
      // Or: calling SCShareableContent also triggers the prompt
      let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
      ```
      
      After granting permission, app restart is typically required.
      
      ### macOS 15+ recurring prompts
      
      macOS 15 Sequoia shows monthly re-authorization prompts. Options:
      - Use `SCContentSharingPicker` (Apple's preferred approach)
      - Request `com.apple.developer.persistent-content-capture` entitlement from Apple (VNC/remote desktop apps)
      - MDM: `forceBypassScreenCaptureAlert` key (enterprise only)
      
      ### Audio capture permissions
      
      | Audio Type | Permission | Entitlement |
      |-----------|-----------|-------------|
      | System/app audio via ScreenCaptureKit | Screen Recording (TCC) | None |
      | Microphone via AVFoundation | Microphone (TCC) | `com.apple.security.device.audio-input` (Hardened Runtime) |
      | Microphone via ScreenCaptureKit (macOS 15+) | Both Screen Recording + Microphone | `com.apple.security.device.audio-input` + `NSMicrophoneUsageDescription` |
      
      Microphone permission check:
      
      ```swift
      switch AVCaptureDevice.authorizationStatus(for: .audio) {
      case .authorized: proceed()
      case .notDetermined:
          let granted = await AVCaptureDevice.requestAccess(for: .audio)
      case .denied, .restricted:
          // Direct to System Settings
          NSWorkspace.shared.open(URL(string:
              "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")!)
      }
      ```
      
      ### Entitlements for capture apps
      
      Non-sandboxed (Developer ID):
      
      ```xml
      <dict>
          <key>com.apple.security.device.audio-input</key>
          <true/>
      </dict>
      ```
      
      Sandboxed (App Store):
      
      ```xml
      <dict>
          <key>com.apple.security.app-sandbox</key>
          <true/>
          <key>com.apple.security.device.microphone</key>
          <true/>
          <key>com.apple.security.device.audio-input</key>
          <true/>
          <key>com.apple.security.files.user-selected.read-write</key>
          <true/>
          <key>com.apple.security.network.client</key>
          <true/>
      </dict>
      ```
      
      Info.plist (required for microphone):
      
      ```xml
      <key>NSMicrophoneUsageDescription</key>
      <string>Record audio alongside screen capture.</string>
      ```
      
      ### macOS 26 TCC pane layout
      
      The pane is labeled **"Screen & System Audio Recording"** on macOS 14 Sonoma and later (it was renamed from "Screen Recording" in Sonoma; macOS 15.x and 26.x inherit the same label). On macOS 26 there is a separate "System Audio Recording Only" subsection below the main list. Granting Screen Recording on 26 implicitly grants system-audio capture; on 14/15 the `kTCCServiceAudioCapture` service is distinct from `kTCCServiceScreenCapture` — preflighting one does not answer for the other.
      
      The canonical deep-link prefix is `com.apple.settings.PrivacySecurity.extension` on macOS 26; the legacy `com.apple.preference.security` prefix still opens *something*, but on 26 it lands on the top Privacy pane rather than the subpane. Also: the `Privacy_AudioCapture` anchor lands on an inactive pane when the capturing app uses SCStream - use `Privacy_ScreenCapture` for anything SCStream-based.
      
      ### Open settings programmatically
      
      To avoid 5-site string duplication (and the Audio-vs-Screen URL drift that duplication invites), centralize the URLs in a small enum:
      
      ```swift
      enum SystemPreferenceURL: String {
          case screenCapture = "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture"
          case microphone   = "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Microphone"
          case notifications = "x-apple.systempreferences:com.apple.preference.notifications"
          case accessibility = "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility"
          case loginItems    = "x-apple.systempreferences:com.apple.LoginItems-Settings.extension"
      
          var url: URL { URL(string: rawValue)! }
          func open() { NSWorkspace.shared.open(url) }
      }
      
      // Usage:
      SystemPreferenceURL.screenCapture.open()
      ```
      
      ### Reset permissions (development)
      
      ```bash
      tccutil reset ScreenCapture com.yourcompany.yourapp
      tccutil reset Microphone com.yourcompany.yourapp
      ```
      
      ## TCC Operational Gotchas
      
      ### `CGPreflightScreenCaptureAccess` vs `CGRequestScreenCaptureAccess`
      
      Two superficially similar APIs with very different UX:
      
      | Call | Triggers dialog | Triggers "app will relaunch" | Use when |
      |------|-----------------|------------------------------|----------|
      | `CGPreflightScreenCaptureAccess()` | No | No | Every permission-sensitive UI refresh. Cheap. |
      | `CGRequestScreenCaptureAccess()` | Yes (once) | Yes | Exactly once during onboarding, at the step the user sees. |
      
      Call the preflight on every recording start and on `didBecomeActive` rather than caching a `UserDefaults` flag - the user can revoke permission between launches, and a stale cached flag leads to silent recordings with no error surface.
      
      Call `CGRequestScreenCaptureAccess()` at the onboarding *step* (the "Grant Screen Recording" screen), not at "Complete Setup". Users who click "Open System Settings" before hitting "Complete Setup" otherwise never trigger the in-app request path, and your onboarding behaves asymmetrically with mic / notifications (which fire per step).
      
      ### Reinstalling via force-recursive replace (rm&#8209;rf + cp&#8209;R) can leave TCC in a degraded state
      
      TCC is keyed by code-signature CDHash. When you replace `/Applications/Blackbox.app` via a force-recursive delete of `/Applications/App.app` followed by `cp -R ./export/App.app /Applications/`, TCC *remembers* the grant (same Developer ID, same CDHash) but content delivery can be broken - permission reads as authorized, `SCStream` starts without error, buffers flow at zero amplitude (RMS stays at `-inf`).
      
      There is no programmatic recovery. Direct-reading `~/Library/Application Support/com.apple.TCC/TCC.db` is blocked by SIP. The remediation the user must take:
      
      1. System Settings → Privacy & Security → Screen & System Audio Recording
      2. Toggle the app **off**, then **on** again
      
      Design your installer / update path accordingly - prefer an in-place overwrite (let the OS handle the inode swap) or run a `tccutil reset ScreenCapture $BUNDLE_ID` from a signed installer, rather than a force-recursive replace (rm&#8209;rf + cp). For `make install` dev scripts, always `killall App` before replacing the bundle, otherwise the still-running process keeps executing from its unlinked inode and `open -a` brings the stale instance to front instead of launching the new one.
      
      ### Ad-hoc signing resets TCC on every rebuild
      
      Ad-hoc signing (`codesign --sign -`) generates a different CDHash each build. TCC identifies ad-hoc apps by CDHash, so permissions reset on every rebuild. Fix: use a self-signed development certificate - TCC then uses the designated requirement (cert + bundle ID), and permissions persist across rebuilds.
      
      ```bash
      # Create self-signed dev cert (one-time)
      openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes \
          -subj "/CN=My Development"
      security import cert.pem -k ~/Library/Keychains/login.keychain-db
      security import key.pem -k ~/Library/Keychains/login.keychain-db
      ```
      
      ### Terminal attribution
      
      Running a compiled binary from the terminal attributes Screen Recording permission to the terminal app (e.g., WezTerm, Terminal.app), not the actual app. Always test via `.app` bundle (`open MyApp.app`), not direct binary execution.
      
      ### Bare binaries cannot get Screen Recording
      
      Standalone binaries without a `.app` bundle and `CFBundleIdentifier` in Info.plist cannot reliably get Screen Recording permission on modern macOS. TCC expects a proper bundle. Wrap test binaries in a minimal `.app` with Info.plist.
      
      ### Two separate TCC entries for microphone
      
      `SCStreamConfiguration.captureMicrophone = true` and `AVCaptureDevice.requestAccess(for: .audio)` create separate TCC entries under different services (`kTCCServiceScreenCapture` vs `kTCCServiceMicrophone`). Both must be granted. System Settings Microphone pane shows both.
      
      ### CGRequestScreenCaptureAccess timing
      
      Do NOT call `CGRequestScreenCaptureAccess()` in `App.init()` - macOS attributes the permission to the parent process (terminal). Call it in `applicationDidFinishLaunching` when the app bundle is fully registered.
      
      ### Permission status vs action
      
      For `.notDetermined`: trigger `AVCaptureDevice.requestAccess()` (shows system dialog). For `.denied`: redirect to System Settings (user must toggle manually). Don't open Settings for `.notDetermined` - the system dialog is a better UX.
      
      ### Refresh permission state when app reactivates
      
      Permission state checked in `onAppear` becomes stale after user switches to Settings and back:
      
      ```swift
      .onAppear { refreshPermissions() }
      .onReceive(NotificationCenter.default.publisher(
          for: NSApplication.didBecomeActiveNotification
      )) { _ in refreshPermissions() }
      ```
      
      ### Screen Recording permission requires restart
      
      After granting Screen Recording in System Settings, macOS shows "Quit & Reopen". This is unavoidable system behavior. Design onboarding so Screen Recording is the last permission step, framing the restart as "setup complete."
      
      ## Complete Examples
      
      ### Audio-only recorder for specific apps
      
      ```swift
      import ScreenCaptureKit
      import AVFoundation
      
      class AppAudioRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
          private var stream: SCStream?
          private var writer: AVAssetWriter?
          private var audioInput: AVAssetWriterInput?
          private var sessionStarted = false
          private let audioQueue = DispatchQueue(label: "AudioCapture")
      
          func startRecording(appBundleID: String, to url: URL) async throws {
              let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
              guard let display = content.displays.first,
                    let app = content.applications.first(where: { $0.bundleIdentifier == appBundleID })
              else { throw CaptureError.appNotFound }
      
              let filter = SCContentFilter(display: display, including: [app], exceptingWindows: [])
      
              let config = SCStreamConfiguration()
              config.capturesAudio = true
              config.sampleRate = 48000
              config.channelCount = 2
              config.excludesCurrentProcessAudio = true
              config.width = 2
              config.height = 2
              config.minimumFrameInterval = CMTime(value: 1, timescale: 1)  // 1 fps, not 1/Int32.max
      
              writer = try AVAssetWriter(url: url, fileType: .m4a)
              audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: [
                  AVFormatIDKey: kAudioFormatMPEG4AAC,
                  AVSampleRateKey: 48000.0,
                  AVNumberOfChannelsKey: 2,
                  AVEncoderBitRateKey: 128_000
              ])
              audioInput!.expectsMediaDataInRealTime = true
              writer!.add(audioInput!)
              writer!.startWriting()
      
              stream = SCStream(filter: filter, configuration: config, delegate: self)
              try stream!.addStreamOutput(self, type: .screen, sampleHandlerQueue: nil)
              try stream!.addStreamOutput(self, type: .audio, sampleHandlerQueue: audioQueue)
              try await stream!.startCapture()
          }
      
          func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer,
                      of type: SCStreamOutputType) {
              guard type == .audio, sampleBuffer.isValid,
                    let input = audioInput, input.isReadyForMoreMediaData else { return }
      
              if !sessionStarted {
                  writer?.startSession(atSourceTime: sampleBuffer.presentationTimeStamp)
                  sessionStarted = true
              }
              input.append(sampleBuffer)
          }
      
          func stopRecording() async {
              try? await stream?.stopCapture()
              stream = nil
              audioInput?.markAsFinished()
              await writer?.finishWriting()
              writer = nil
              sessionStarted = false
          }
      
          func stream(_ stream: SCStream, didStopWithError error: Error) {
              Task { await stopRecording() }
          }
      
          enum CaptureError: Error { case appNotFound }
      }
      ```
      
      ### Full display recording with SCRecordingOutput (macOS 15+)
      
      ```swift
      import ScreenCaptureKit
      import AVFoundation
      
      @available(macOS 15.0, *)
      class DisplayRecorder: NSObject, SCStreamDelegate, SCRecordingOutputDelegate {
          private var stream: SCStream?
          private var recordingOutput: SCRecordingOutput?
      
          func startRecording(to url: URL) async throws {
              let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
              guard let display = content.displays.first else { return }
      
              let excludeSelf = content.applications.filter {
                  $0.bundleIdentifier == Bundle.main.bundleIdentifier
              }
              let filter = SCContentFilter(
                  display: display, excludingApplications: excludeSelf, exceptingWindows: []
              )
      
              let config = SCStreamConfiguration()
              config.width = display.width * 2
              config.height = display.height * 2
              config.minimumFrameInterval = CMTime(value: 1, timescale: 60)
              config.capturesAudio = true
              config.sampleRate = 48000
              config.channelCount = 2
              config.excludesCurrentProcessAudio = true
      
              let recordingConfig = SCRecordingOutputConfiguration()
              recordingConfig.outputURL = url
              recordingConfig.outputFileType = .mov
              recordingConfig.videoCodecType = .hevc
      
              stream = SCStream(filter: filter, configuration: config, delegate: self)
              recordingOutput = SCRecordingOutput(configuration: recordingConfig, delegate: self)
              try stream!.addRecordingOutput(recordingOutput!)
              try await stream!.startCapture()
          }
      
          func stopRecording() async throws {
              try await stream?.stopCapture()
              stream = nil
              recordingOutput = nil
          }
      
          func recordingOutputDidStartRecording(_ output: SCRecordingOutput) { }
          func recordingOutputDidFinishRecording(_ output: SCRecordingOutput) { }
          func recordingOutput(_ output: SCRecordingOutput, didFailWithError error: Error) { }
          func stream(_ stream: SCStream, didStopWithError error: Error) { }
      }
      ```
      
      ## API Availability
      
      | API | Minimum macOS |
      |-----|--------------|
      | SCShareableContent, SCContentFilter, SCStream, SCStreamOutput | 12.3 |
      | SCStreamConfiguration.capturesAudio | 12.3 |
      | SCContentSharingPicker, SCScreenshotManager | 14.0 |
      | SCRecordingOutput, SCStreamOutputType.microphone | 15.0 |
      | HDR capture, configuration presets | 15.0 |
      
      ## `synchronizationClock`: aligning SCStream with other capture sources
      
      `SCStream.synchronizationClock` exposes the `CMClock` that SCStream timestamps its buffers against. When you mix SCStream output with a second pipeline - an `AVAudioEngine` mic tap, a `CATap`, an `AVCaptureSession` - both sides must be expressed in the same time base or the muxed result drifts even though each track is individually correct.
      
      ```swift
      if let clock = stream.synchronizationClock {
          session.synchronizationClock = clock       // AVCaptureSession
          // or convert a host-time stamp into the stream's base
          let streamTime = CMSyncConvertTime(hostTime, from: CMClockGetHostTimeClock(), to: clock)
      }
      ```
      
      Do this instead of subtracting a captured "start host time" from both sides - that approximation accumulates error over long recordings and is a common cause of audio slowly sliding against video.
      
      ## Screenshots: `SCScreenshotConfiguration` (macOS 26+)
      
      `SCScreenshotManager.captureImage(contentFilter:configuration:)` takes an `SCStreamConfiguration`, which means single-frame capture inherits stream semantics it does not need. macOS 26 adds a dedicated path:
      
      ```swift
      let config = SCScreenshotConfiguration()
      config.width = 3840
      config.height = 2160
      config.dynamicRange = .high          // HDR without the stream preset dance
      config.includesCursor = false
      
      let output = try await SCScreenshotManager.captureScreenshot(
          contentFilter: filter, configuration: config
      )
      let image = output.cgImage
      ```
      
      Prefer this over `SCStreamConfiguration(preset: .captureHDRScreenshotLocalDisplay)` on macOS 26+; the preset route remains correct for 14/15 back-deployment.
      
      ## Presenter Overlay delegate callbacks
      
      When the user turns on Presenter Overlay (the camera-overlay feature in video calls), the system reshapes your capture. `SCStreamDelegate` gets told, and most implementations silently ignore it:
      
      ```swift
      func stream(_ stream: SCStream, outputVideoEffectDidStartFor screen: SCStream) {
          // Overlay active - your frames now include the camera composite
      }
      func stream(_ stream: SCStream, outputVideoEffectDidStopFor screen: SCStream) { }
      ```
      
      Set `config.presenterOverlayPrivacyAlertSetting` to control whether the system shows its privacy alert. If your app records for later playback rather than live presentation, handling these callbacks lets you warn the user that an overlay is being baked into the recording.
      
      ## `SCStreamConfiguration` properties worth knowing
      
      Beyond `width`/`height`/`capturesAudio`/`minimumFrameInterval`:
      
      | Property | Use |
      |---|---|
      | `sourceRect` | Capture a sub-region of the display without post-cropping |
      | `destinationRect` | Place captured content within the output surface |
      | `preservesAspectRatio` | Letterbox instead of stretching when the rects disagree |
      | `showMouseClicks` | Visualize clicks - useful for tutorial/demo recording |
      | `includeChildWindows` | Include or exclude child windows of a captured window |
      | `capturesShadowsOnly` | Window shadows without the window |
      | `colorMatrix` | Override the YCbCr matrix for the output pixel buffers |
      | `ignoreGlobalClipDisplay` / `ignoreGlobalClipSingleWindow` | Bypass global clipping behavior |
      
      ## Trap: a mono downmix that assumes interleaved layout
      
      A `resampleToMono` helper that indexes `floatChannelData[0]` as `[L, R, L, R, ...]` is only correct for an **interleaved** buffer. `AVAudioPCMBuffer` is frequently **planar (non-interleaved)**, where `floatChannelData[0]` is the entire left channel and the right channel lives at `floatChannelData[1]`.
      
      Feed a planar buffer to interleaved-indexing code and you average two adjacent *left* samples and never read the right channel at all. The output is audibly wrong but not silent - it passes a smoke test and ships.
      
      ```swift
      func downmixToMono(_ buffer: AVAudioPCMBuffer) -> [Float] {
          guard let data = buffer.floatChannelData else { return [] }
          let frames = Int(buffer.frameLength)
          let channels = Int(buffer.format.channelCount)
          guard channels > 1 else { return Array(UnsafeBufferPointer(start: data[0], count: frames)) }
      
          var mono = [Float](repeating: 0, count: frames)
          if buffer.format.isInterleaved {
              let p = data[0]
              for i in 0..<frames {
                  mono[i] = (p[i * channels] + p[i * channels + 1]) * 0.5
              }
          } else {
              let left = data[0], right = data[1]
              for i in 0..<frames {
                  mono[i] = (left[i] + right[i]) * 0.5
              }
          }
          return mono
      }
      ```
      
      Always branch on `buffer.format.isInterleaved`. Test with hostile stereo - opposite-polarity channels sum to silence under a correct downmix and to a full-amplitude signal under the broken one, which makes the bug unmissable.
      
      ## Trap: a stale TCC row from a superseded signing identity
      
      After switching signing identity - ad-hoc or self-signed during development, then Developer ID for re
    • sendable-safety.md 12.9 KB
      # Sendable & Data Race Safety
      
      ## Table of Contents
      - Sendable Protocol
      - Implicit Sendable
      - Making Types Sendable
      - @unchecked Sendable
      - @unchecked Sendable + Serial Queue Pattern
      - @Sendable Closures
      - `[#SendingRisksDataRace]` Shim for Apple Non-Sendable Types
      - @preconcurrency import
      - Swift 6 Language Mode
      - Common Patterns
      
      ## Sendable Protocol
      
      `Sendable` marks types safe to share across concurrency domains (actors, tasks):
      
      ```swift
      // Value types with Sendable stored properties are implicitly Sendable
      struct Point: Sendable {
          var x: Double
          var y: Double
      }
      
      // Enums with Sendable associated values.
      // Note: `any Error` is NOT Sendable by default, so an enum carrying it cannot
      // claim Sendable unless the error type is concrete and Sendable.
      enum NetworkResult: Sendable {
          case success(Data)
          case failure(NetworkError)   // concrete, Sendable error type
      }
      
      // If you need to carry `any Error`, drop Sendable conformance or route through
      // a Sendable wrapper.
      ```
      
      ## Implicit Sendable
      
      These are automatically Sendable without explicit conformance:
      - Primitive types (`Int`, `String`, `Bool`, `Double`, etc.)
      - Structs where all stored properties are Sendable
      - Enums where all associated values are Sendable
      - Tuples of Sendable types
      - Metatypes (`Int.Type`)
      - Actors (isolated state)
      
      ## Making Types Sendable
      
      ### Final immutable classes
      ```swift
      final class AppConfig: Sendable {
          let apiURL: URL
          let timeout: TimeInterval
          let maxRetries: Int
      
          init(apiURL: URL, timeout: TimeInterval, maxRetries: Int) {
              self.apiURL = apiURL
              self.timeout = timeout
              self.maxRetries = maxRetries
          }
      }
      ```
      
      Requirements for class Sendable:
      - Must be `final`
      - All stored properties must be `let` (immutable)
      - All stored properties must be `Sendable`
      
      ### Sendable through actors
      ```swift
      // Instead of making a mutable class Sendable, use an actor
      actor UserSession {
          private var token: String?
          private var refreshTask: Task<String, Error>?
      
          func getToken() async throws -> String {
              if let token { return token }
              if let task = refreshTask { return try await task.value }
      
              let task = Task { try await refreshToken() }
              refreshTask = task
              let newToken = try await task.value
              token = newToken
              refreshTask = nil
              return newToken
          }
      }
      ```
      
      ## @unchecked Sendable
      
      Escape hatch when you ensure thread safety yourself:
      
      ```swift
      // Thread-safe via internal locking
      final class ThreadSafeCache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
          private let lock = NSLock()
          private var storage: [Key: Value] = [:]
      
          func get(_ key: Key) -> Value? {
              lock.withLock { storage[key] }
          }
      
          func set(_ key: Key, value: Value) {
              lock.withLock { storage[key] = value }
          }
      }
      ```
      
      **Use sparingly.** Prefer actors or restructuring to avoid `@unchecked Sendable`. Common legitimate uses:
      - Wrapping C/Objective-C types with internal synchronization
      - Types using `os_unfair_lock` or `NSLock` internally
      - Bridging legacy code during migration
      
      ## @unchecked Sendable + Serial Queue Pattern
      
      For classes that manage their own thread safety via a serial dispatch queue (common in audio/video recording), use `@unchecked Sendable` with `nonisolated(unsafe)` properties:
      
      ```swift
      class AudioRecorder: NSObject, @unchecked Sendable, SCStreamOutput {
          private let audioQueue = DispatchQueue(label: "com.app.audio")
      
          // State accessed from background callbacks - nonisolated(unsafe) + serial queue
          nonisolated(unsafe) private var writer: AVAssetWriter?
          nonisolated(unsafe) private var systemInput: AVAssetWriterInput?
          nonisolated(unsafe) private var micInput: AVAssetWriterInput?
          nonisolated(unsafe) private var sessionStarted = false
          nonisolated(unsafe) private var stopped = false
      
          // SCStreamOutput callback - runs on audioQueue (background)
          nonisolated func stream(_ stream: SCStream,
                                  didOutputSampleBuffer sampleBuffer: CMSampleBuffer,
                                  of type: SCStreamOutputType) {
              // All nonisolated(unsafe) state accessed exclusively on audioQueue
              guard !stopped, let input = systemInput, input.isReadyForMoreMediaData else { return }
              input.append(sampleBuffer)
          }
      
          // Callbacks passed at init, not set after - avoids data races
          nonisolated let onError: (@Sendable (Error) -> Void)?
      
          init(onError: (@Sendable (Error) -> Void)? = nil) {
              self.onError = onError
          }
      }
      ```
      
      With `defaultIsolation(MainActor.self)`, pair `@ObservationIgnored` with `nonisolated(unsafe)` for internal bookkeeping properties in `@Observable` classes:
      
      ```swift
      @Observable
      class ResourceManager: @unchecked Sendable {
          // UI-visible state (MainActor-isolated, observed by SwiftUI)
          var isRecording = false
      
          // Internal state (not for UI, accessed on background queue)
          @ObservationIgnored nonisolated(unsafe) private var writer: AVAssetWriter?
          @ObservationIgnored nonisolated(unsafe) private var listenerIDs: Set<AudioObjectID> = []
      }
      ```
      
      ## @Sendable Closures
      
      Functions passed across concurrency boundaries must be `@Sendable`:
      
      ```swift
      // @Sendable closures cannot capture mutable state
      func performInBackground(_ work: @Sendable () async -> Void) {
          Task.detached { await work() }
      }
      
      // OK - captures immutable value
      let name = "test"
      performInBackground {
          print(name)
      }
      
      // Error - captures mutable variable
      var count = 0
      performInBackground {
          count += 1 // Compiler error: mutation of captured var in @Sendable closure
      }
      ```
      
      ## `[#SendingRisksDataRace]` Shim for Apple Non-Sendable Types
      
      When an `SCStreamOutput` method, `AVAudioEngine` tap, or `AudioObjectPropertyListenerBlock` hands a `CMSampleBuffer` / `AVAudioPCMBuffer` / `AudioBufferList*` into actor-isolated code, Swift 6 flags the parameter with the `SendingRisksDataRace` diagnostic group (canonical identifier used with `-Werror` / `-Wwarning`; the compiler prints it as `[#SendingRisksDataRace]` after each error line — see https://docs.swift.org/compiler/documentation/diagnostics/sending-risks-data-race). The type isn't `Sendable` and can't cross isolation boundaries by the usual rules.
      
      **If the callback already runs on the target executor** (e.g. SCStream's `sampleHandlerQueue` is your actor's `audioQueue`, or the `AudioObjectPropertyListenerBlock` was registered on that queue), the "data race" doesn't exist - the callback and the actor are literally the same isolation domain. The idiomatic workaround is a one-line rebind:
      
      ```swift
      nonisolated func stream(_ stream: SCStream,
                              didOutputSampleBuffer sampleBuffer: CMSampleBuffer,
                              of type: SCStreamOutputType) {
          // Rebinding breaks the sending check. Safe because this callback runs on
          // audioQueue, which IS this actor's executor.
          nonisolated(unsafe) let buffer = sampleBuffer
          self.assumeIsolated { iso in
              iso.handleSampleBuffer(buffer, type: type)
          }
      }
      ```
      
      Reflexively adding an extra `audioQueue.async { ... }` to "fix" the warning is worse than the shim - it adds a redundant queue hop *and* can introduce the ordering bug described in `actors-isolation.md` ("Don't re-dispatch when the block already runs on the actor's executor").
      
      **When NOT to use this shim**: if the callback fires on a queue that is *not* your actor's executor, the data race is real - you need a proper hop (`actor.method(buffer)` or `queue.async { assumeIsolated { ... } }`). The shim lies only when the callback is already running in the actor's isolation domain.
      
      ## @preconcurrency import
      
      Apple framework types like `AVAudioPCMBuffer`, `AVAssetWriter`, `CMSampleBuffer`, and `AVAudioFormat` lack `Sendable` conformance. Use `@preconcurrency import` to suppress warnings while Apple updates their frameworks:
      
      ```swift
      @preconcurrency import AVFoundation  // Covers AVAudioPCMBuffer, AVAssetWriter, etc.
      @preconcurrency import CoreMedia     // Covers CMSampleBuffer, CMTime, etc.
      @preconcurrency import AudioToolbox  // Covers C block types used by CoreAudio listeners
      ```
      
      This treats the imported types as implicitly `Sendable` (matching pre-concurrency behavior). When Apple adds proper annotations, remove `@preconcurrency` to get full checking.
      
      ### For deinit accessing non-Sendable C block types
      
      `deinit` that needs to remove a CoreAudio listener block (`AudioObjectPropertyListenerBlock`) or invoke AudioToolbox cleanup C APIs can fail to compile against a plain `import AudioToolbox` because the block typealias isn't `Sendable`. `@preconcurrency import AudioToolbox` unblocks this case without resorting to `@unchecked Sendable` on the whole enclosing type.
      
      `KeyPath<Root, Value>` is not unconditionally `Sendable` in Swift 6 (SE-0418): keypath-literal captures are inferred Sendable only when the captures themselves are Sendable, and a stored `KeyPath` property needs an explicit `& Sendable` constraint. This bites `KeyPathComparator`-based `Table` sort on Swift 6.2 / Xcode 26.0-26.2 (tracked as swiftlang/swift #75852 and #84983). Most cases are resolved in Swift 6.3 / Xcode 26.3+, but verify against the toolchain you target. Workaround on affected versions: pre-sort data in the source and avoid `KeyPathComparator` entirely.
      
      ## Swift 6 Language Mode
      
      Enable strict data race safety:
      
      ```swift
      // Package.swift
      .target(
          name: "MyApp",
          swiftSettings: [.swiftLanguageMode(.v6)]
      )
      ```
      
      What Swift 6 mode enforces:
      - All `Sendable` violations are errors (not warnings)
      - Global variables must be isolated or Sendable
      - Closures passed across isolation boundaries must be `@Sendable`
      - Protocol conformances must respect isolation
      
      ### Gradual migration
      ```swift
      // Start with strict concurrency checking as warnings
      .target(
          name: "MyApp",
          swiftSettings: [
              .swiftLanguageMode(.v5),
              .enableUpcomingFeature("StrictConcurrency"),
          ]
      )
      ```
      
      Then fix warnings before enabling `.v6`.
      
      ## Common Patterns
      
      ### Global state
      ```swift
      // BAD: Mutable global
      var globalCache: [String: Data] = [:] // Error in Swift 6
      
      // GOOD: Actor-isolated
      actor GlobalCache {
          static let shared = GlobalCache()
          private var storage: [String: Data] = [:]
      
          func get(_ key: String) -> Data? { storage[key] }
          func set(_ key: String, data: Data) { storage[key] = data }
      }
      
      // GOOD: nonisolated(unsafe) for truly thread-safe globals
      nonisolated(unsafe) let logger = Logger(subsystem: "com.app", category: "main")
      ```
      
      ### Delegate patterns
      ```swift
      // Protocol must be MainActor-isolated or Sendable
      @MainActor
      protocol DocumentDelegate: AnyObject {
          func documentDidSave(_ document: Document)
          func documentDidFail(_ document: Document, error: Error)
      }
      ```
      
      ### Migrating ObservableObject
      ```swift
      // Old (pre-Swift 5.9)
      class ViewModel: ObservableObject {
          @Published var items: [Item] = []
      }
      
      // New
      @Observable
      @MainActor
      final class ViewModel {
          var items: [Item] = []
      }
      
      // With default isolation (Swift 6.2), @MainActor is implicit
      @Observable
      final class ViewModel {
          var items: [Item] = []
      }
      ```
      
      ## The `Synchronization` module: `Mutex` and `Atomic`
      
      Actors are the default answer for shared mutable state, but they force `async` at every call site. In a realtime audio callback or a C-API completion handler you cannot `await` - which is exactly where `@unchecked Sendable` plus a manual lock usually gets reached for. The `Synchronization` module gives you a `Sendable`-correct alternative with no `@unchecked` escape hatch.
      
      ```swift
      import Synchronization
      
      final class LevelMeter: Sendable {
          private let peak = Mutex<Float>(0)
      
          // Callable from a realtime audio thread - no await, no actor hop
          func record(_ sample: Float) {
              peak.withLock { $0 = max($0, abs(sample)) }
          }
      
          func drain() -> Float {
              peak.withLock { value in
                  defer { value = 0 }
                  return value
              }
          }
      }
      ```
      
      `Mutex<Value>` is `Sendable` when `Value` is, so the enclosing type conforms to `Sendable` without `@unchecked`. The compiler enforces that the value is only reachable inside `withLock`.
      
      For single values where a lock is overkill:
      
      ```swift
      import Synchronization
      
      let frameCount = Atomic<Int>(0)
      frameCount.add(1, ordering: .relaxed)
      let total = frameCount.load(ordering: .acquiring)
      
      let isRunning = Atomic<Bool>(false)
      // Compare-and-exchange for idempotent start/stop
      let (exchanged, _) = isRunning.compareExchange(
          expected: false, desired: true, ordering: .sequentiallyConsistent
      )
      guard exchanged else { return }   // someone else already started
      ```
      
      Rules that matter:
      
      - **Never `await` inside `withLock`.** The closure is non-async by design; holding a lock across a suspension point is how you deadlock.
      - Keep the critical section tiny - copy out, compute outside.
      - Prefer `.relaxed` only for statistics counters. For anything establishing a happens-before relationship with other memory, use `.acquiring`/`.releasing` or `.sequentiallyConsistent`.
      - This is the tool for realtime and C-callback contexts. For ordinary app state, an actor is still the better default.
      
    • spm-build.md 15.6 KB
      # Swift Package Manager & Build
      
      ## Table of Contents
      - Package.swift Basics
      - Platform & Version Config
      - Dependencies
      - Targets & Products
      - Build Plugins
      - Swift Build (Open Source)
      - Macros
      - Resources
      - Conditional Compilation
      - Manual .app Bundle Assembly
      - Mixed ObjC Targets
      - Swift Testing with CLT (No Xcode)
      - Multiple Executable Targets
      
      ## Package.swift Basics
      
      ```swift
      // swift-tools-version: 6.2
      import PackageDescription
      
      let package = Package(
          name: "MyMacApp",
          platforms: [
              .macOS(.v14), // minimum deployment target
          ],
          products: [
              .executable(name: "MyMacApp", targets: ["MyMacApp"]),
              .library(name: "MyAppCore", targets: ["MyAppCore"]),
          ],
          dependencies: [
              .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.17.0"),
              .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.7.0"),
          ],
          targets: [
              .executableTarget(
                  name: "MyMacApp",
                  dependencies: [
                      "MyAppCore",
                      .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
                      .product(name: "Sparkle", package: "Sparkle"),
                  ],
                  swiftSettings: [
                      .swiftLanguageMode(.v6),
                      .defaultIsolation(MainActor.self),
                  ]
              ),
              .target(
                  name: "MyAppCore",
                  dependencies: [],
                  swiftSettings: [.swiftLanguageMode(.v6)]
              ),
              .testTarget(
                  name: "MyMacAppTests",
                  dependencies: ["MyAppCore"]
              ),
          ]
      )
      ```
      
      ## Swift Settings
      
      ```swift
      .executableTarget(
          name: "MyApp",
          swiftSettings: [
              // Swift 6 language mode (strict concurrency)
              .swiftLanguageMode(.v6),
      
              // Default MainActor isolation (Swift 6.2)
              .defaultIsolation(MainActor.self),
      
              // Enable upcoming features individually
              .enableUpcomingFeature("ExistentialAny"),
              .enableUpcomingFeature("InternalImportsByDefault"),
      
              // Strict memory safety - first-class setting since Swift 6.2 (SE-0458).
              // The old .enableExperimentalFeature("StrictMemorySafety") spelling still
              // builds, but prefer the real API.
              .strictMemorySafety(),
      
              // Warning control (Swift 6.2)
              .treatAllWarnings(as: .error),
              .treatWarning("DeprecatedDeclaration", as: .warning),
          ]
      )
      ```
      
      ## Dependencies
      
      ### Version requirements
      ```swift
      .package(url: "https://github.com/org/repo", from: "1.0.0"),     // >= 1.0.0, < 2.0.0
      .package(url: "https://github.com/org/repo", exact: "1.2.3"),     // exactly 1.2.3
      .package(url: "https://github.com/org/repo", "1.0.0"..<"2.0.0"), // range
      .package(url: "https://github.com/org/repo", branch: "main"),     // branch (dev only)
      .package(url: "https://github.com/org/repo", revision: "abc123"), // specific commit
      .package(path: "../LocalPackage"),                                 // local path
      ```
      
      ### Conditional dependencies
      ```swift
      .target(
          name: "MyApp",
          dependencies: [
              .target(name: "MyCore"),
              .product(name: "ArgumentParser", package: "swift-argument-parser",
                       condition: .when(platforms: [.macOS, .linux])),
          ]
      )
      ```
      
      ## Resources
      
      Bundle resources with targets:
      
      ```swift
      .target(
          name: "MyApp",
          resources: [
              .process("Resources/"),      // Optimize for platform
              .copy("Data/config.json"),   // Copy as-is
          ]
      )
      ```
      
      Access in code:
      ```swift
      let url = Bundle.module.url(forResource: "config", withExtension: "json")!
      let image = NSImage(resource: .appIcon) // Xcode asset catalogs
      ```
      
      ## Build Plugins
      
      ### Build tool plugin
      ```swift
      // Plugins/CodeGenPlugin/plugin.swift
      import PackagePlugin
      
      @main
      struct CodeGenPlugin: BuildToolPlugin {
          func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
              let inputFile = context.package.directory.appending("schema.json")
              let outputFile = context.pluginWorkDirectory.appending("Generated.swift")
      
              return [
                  .buildCommand(
                      displayName: "Generate code from schema",
                      executable: try context.tool(named: "codegen").url,
                      arguments: [inputFile.string, outputFile.string],
                      inputFiles: [inputFile],
                      outputFiles: [outputFile]
                  )
              ]
          }
      }
      ```
      
      ### Command plugin
      ```swift
      @main
      struct FormatPlugin: CommandPlugin {
          func performCommand(context: PluginContext, arguments: [String]) throws {
              let swiftformat = try context.tool(named: "swift-format")
              let process = Process()
              process.executableURL = swiftformat.url
              process.arguments = ["--recursive", context.package.directory.string]
              try process.run()
              process.waitUntilExit()
          }
      }
      
      // Run: swift package format
      ```
      
      ## Swift Build
      
      Apple open-sourced Xcode's build engine as Swift Build (Feb 2025). Aims to unify the build experience between Xcode and SPM:
      
      - Same build rules for both Xcode projects and Swift packages
      - Supports libraries, executables, and GUI applications
      - Build graph optimizations for Swift/C parallel compilation
      - Future: Replace SPM's simple build engine with Swift Build
      
      Current status: **shipping**, not "integration ongoing". SwiftPM 6.3 exposes it as a preview behind a flag; SwiftPM 6.4 makes it the default build system.
      
      ```bash
      # Opt in on the 6.3 toolchain
      swift build --build-system swiftbuild
      
      # Swift Build supports SwiftPM-native universal binaries
      swift build --build-system swiftbuild --arch arm64 --arch x86_64
      ```
      
      The `--arch` flags are a Swift Build capability - the default 6.3 native build system builds a single architecture, which is why manual `.app` assembly elsewhere in this file hardcodes `.build/arm64-apple-macosx/release/`. Sources: [SwiftPM 6.3 notes](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/6.3), [Swift Build preview](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/swiftbuildpreview).
      
      ## Package Traits (SE-0450, Swift 6.1+)
      
      Conditional compilation flags for packages - let consumers opt into optional feature sets without separate products:
      
      ```swift
      let package = Package(
          name: "MyLib",
          traits: [
              .default(enabledTraits: ["Networking"]),
              "Networking",
              .trait(name: "Experimental", enabledTraits: ["Networking"]),
          ],
          targets: [
              .target(name: "MyLib", swiftSettings: [
                  .define("NETWORKING", .when(traits: ["Networking"])),
              ]),
          ]
      )
      
      // Consumer side
      .package(url: "https://github.com/org/mylib", from: "1.0.0", traits: ["Experimental"]),
      ```
      
      Inspect with `swift package show-traits`. **Traits must be strictly additive - enabling a trait must not remove API.** Breaking that rule makes dependency resolution unsound, since two consumers can enable different trait sets on the same package build.
      
      ## Other target kinds
      
      Beyond `.target` / `.executableTarget` / `.testTarget`:
      
      ```swift
      .binaryTarget(name: "Vendor", path: "Vendor.xcframework"),           // prebuilt .xcframework
      .binaryTarget(name: "Remote", url: "https://.../lib.zip", checksum: "..."),
      .systemLibrary(name: "CZlib", pkgConfig: "zlib"),                    // wrap a system C library
      ```
      
      Plugin sandbox permissions must be declared explicitly - a command plugin that writes outside the package or hits the network is denied by default:
      
      ```swift
      .plugin(name: "Generate", capability: .command(
          intent: .custom(verb: "generate", description: "Generate sources"),
          permissions: [
              .writeToPackageDirectory(reason: "Writes generated sources"),
              .allowNetworkConnections(scope: .all(ports: [443]), reason: "Fetches schema"),
          ]
      ))
      ```
      
      ## Build fails before compiling your code: unwritable `$HOME` caches
      
      In sandboxed, CI, or agent environments, `swift build` and `swift test` can die during **manifest compilation** - before touching your sources - because SwiftPM's cache and the clang module cache live under the home directory and are not writable. The error names SwiftPM internals, so it reads like a broken package when the package is fine.
      
      ```bash
      swift build \
        --cache-path "$SCRATCH/spm-cache" \
        --scratch-path "$SCRATCH/build" \
        -Xswiftc -module-cache-path -Xswiftc "$SCRATCH/module-cache"
      ```
      
      Redirect the caches to a writable scratch directory and re-run before concluding anything about the package itself.
      
      ## Consuming packages from `xcodebuild`: two gates that block non-interactive builds
      
      **Plugin and macro validation.** A project whose dependencies carry package plugins or macros stops on a trust prompt that has no answer in a headless build - it surfaces as a stall or failure on `Validate plug-in '<name>' in package '<pkg>'`. Both gates have opt-outs, and both exist for a reason: they are the trust boundary for arbitrary code running at build time, so only skip them for dependencies you have vetted.
      
      ```bash
      xcodebuild -scheme MyApp \
        -skipPackagePluginValidation \
        -skipMacroValidation
      ```
      
      **The Metal toolchain is no longer bundled.** Xcode 26 unbundled it, so any target compiling `.metal` shaders - directly, or through a dependency that does - fails with `cannot execute tool 'metal'` on a fresh install. It is a one-time ~688 MB download:
      
      ```bash
      xcodebuild -downloadComponent MetalToolchain
      ```
      
      `xcodebuild -help` lists `MetalToolchain` as the only supported component value.
      
      ## Macros
      
      ### Using macros
      ```swift
      // Add macro package
      // Pin to the swift-syntax release matching your toolchain: 6.3 -> 603.x
      .package(url: "https://github.com/swiftlang/swift-syntax", from: "603.0.2"),
      
      .target(
          name: "MyApp",
          dependencies: [
              .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
          ]
      )
      ```
      
      ### Swift 6.2 macro performance
      Pre-built swift-syntax is now supported, eliminating the need to build swift-syntax from source on every clean build. Significantly faster CI builds.
      
      ## Swiftly (Toolchain Manager)
      
      Official Swift toolchain manager for macOS:
      
      ```bash
      # Install swiftly
      curl -L https://swift.org/install | bash
      
      # Install latest stable
      swiftly install latest
      
      # Install specific version
      swiftly install 6.3.3
      
      # Switch versions
      swiftly use 6.3.3
      
      # List installed
      swiftly list
      ```
      
      ## Conditional Compilation
      
      ```swift
      #if os(macOS)
      import AppKit
      #elseif os(iOS)
      import UIKit
      #endif
      
      #if canImport(FoundationModels)
      import FoundationModels
      // Use on-device AI
      #endif
      
      #if swift(>=6.2)
      // Use Swift 6.2 features
      #endif
      
      #if DEBUG
      // Debug-only code
      #endif
      
      #if targetEnvironment(simulator)
      // Simulator-specific code
      #endif
      ```
      
      ## Manual .app Bundle Assembly
      
      SPM (`swift build`) produces a bare executable, not a `.app` bundle. macOS requires a proper `.app` for Info.plist keys (LSUIElement, NSMicrophoneUsageDescription), TCC permissions, and framework loading. Assemble manually via Makefile:
      
      ```makefile
      APP_BUNDLE = build/MyApp.app
      BINARY = .build/arm64-apple-macosx/release/MyApp
      
      bundle: build
      	@mkdir -p "$(APP_BUNDLE)/Contents/MacOS"
      	@mkdir -p "$(APP_BUNDLE)/Contents/Frameworks"
      	@mkdir -p "$(APP_BUNDLE)/Contents/Resources"
      	# Copy binary
      	@cp "$(BINARY)" "$(APP_BUNDLE)/Contents/MacOS/MyApp"
      	# CRITICAL: Add rpath for frameworks (SPM default doesn't include it)
      	@install_name_tool -add_rpath @executable_path/../Frameworks \
      	    "$(APP_BUNDLE)/Contents/MacOS/MyApp"
      	# Copy Info.plist and icon
      	@cp Info.plist "$(APP_BUNDLE)/Contents/"
      	@cp Assets/AppIcon.icns "$(APP_BUNDLE)/Contents/Resources/"
      	# Copy dynamic frameworks (e.g., Sparkle)
      	@cp -R .build/artifacts/sparkle/Sparkle/Sparkle.framework \
      	    "$(APP_BUNDLE)/Contents/Frameworks/"
      	# CRITICAL: Copy SPM resource bundles (CoreML models, assets, etc.)
      	@for bundle in .build/arm64-apple-macosx/release/*.bundle; do \
      	    [ -d "$$bundle" ] && cp -R "$$bundle" "$(APP_BUNDLE)/Contents/Resources/"; \
      	done
      	# Sign from inside out: nested bundles first, then main app
      	@codesign --force --options runtime --sign "$(SIGN_ID)" --timestamp \
      	    "$(APP_BUNDLE)/Contents/Frameworks/Sparkle.framework"
      	@codesign --force --options runtime --sign "$(SIGN_ID)" --timestamp \
      	    --identifier com.example.MyApp --entitlements entitlements.plist "$(APP_BUNDLE)"
      ```
      
      Key gotchas:
      - **`install_name_tool -add_rpath`** is required or dynamic frameworks crash with `dyld: Library not loaded: @rpath/...`
      - **SPM resource bundles** (`.bundle` directories from packages with `resources:`) are NOT automatically included. Missing them causes silent crashes on non-dev machines where `Bundle.module` resolves to nil.
      - **Sign from inside out**: nested `.xpc` and `.app` bundles within frameworks must be signed before the framework, which must be signed before the top-level app.
      - **Stale artifacts** can have read-only permissions from previous builds. Use `chmod -R u+w` or clean before copying.
      
      ### make run race condition
      
      `open` won't relaunch an already-running LSUIElement app. Kill and wait before rebuilding:
      
      ```makefile
      run: build
      	@killall MyApp 2>/dev/null; while killall -0 MyApp 2>/dev/null; do sleep 0.1; done
      	@$(MAKE) bundle
      	@open "$(APP_BUNDLE)"
      ```
      
      Rebuilding while the old process is running causes `SIGKILL (Code Signature Invalid)` - macOS detects memory-mapped code pages don't match the new binary's signature.
      
      ## Mixed ObjC Targets
      
      SPM doesn't allow mixing Swift and ObjC in a single target. Create a separate target:
      
      ```swift
      targets: [
          .executableTarget(
              name: "MyApp",
              dependencies: ["ObjCExceptionCatcher"],
              exclude: ["ObjCExceptionCatcher"], // Exclude from main target's path scan
              swiftSettings: [.swiftLanguageMode(.v6), .defaultIsolation(MainActor.self)]
          ),
          .target(
              name: "ObjCExceptionCatcher",
              path: "Sources/ObjCExceptionCatcher",
              publicHeadersPath: "include",
              linkerSettings: [.linkedFramework("AVFAudio")]
          ),
      ]
      ```
      
      Directory layout:
      ```
      Sources/
        MyApp/
          main.swift
        ObjCExceptionCatcher/
          include/ObjCExceptionCatcher.h
          ObjCExceptionCatcher.m
      ```
      
      ## Swift Testing with CLT (No Xcode)
      
      On machines using CommandLineTools (not Xcode), `import Testing` fails. The framework exists but SPM doesn't search the CLT path. Add three `unsafeFlags`:
      
      ```swift
      .testTarget(
          name: "MyAppTests",
          dependencies: ["MyApp"],
          swiftSettings: [
              .swiftLanguageMode(.v6),
              // Compiler: find Testing module
              .unsafeFlags(["-F",
                  "/Library/Developer/CommandLineTools/Library/Developer/Frameworks"]),
          ],
          linkerSettings: [
              // Linker: resolve Testing framework
              .unsafeFlags(["-F",
                  "/Library/Developer/CommandLineTools/Library/Developer/Frameworks"]),
              // Runtime: load Testing framework
              .unsafeFlags(["-Xlinker", "-rpath", "-Xlinker",
                  "/Library/Developer/CommandLineTools/Library/Developer/Frameworks"]),
          ]
      )
      ```
      
      ## Multiple Executable Targets
      
      When adding helper binaries (e.g., watchdog, CLI tool) to the same package:
      
      ```swift
      targets: [
          .executableTarget(
              name: "MyApp",
              exclude: ["Watchdog", "ObjCExceptionCatcher"], // Exclude sibling directories
              swiftSettings: [.swiftLanguageMode(.v6), .defaultIsolation(MainActor.self)]
          ),
          .executableTarget(
              name: "MyWatchdog",
              path: "Sources/Watchdog",
              swiftSettings: [
                  .swiftLanguageMode(.v6),
                  .treatAllWarnings(as: .error),
                  // Helper doesn't need defaultIsolation - it's a simple process monitor
              ]
          ),
      ]
      ```
      
      The Makefile copies helper binaries into `Contents/MacOS/` and signs them before the main app:
      
      ```makefile
      @cp .build/arm64-apple-macosx/release/MyWatchdog "$(APP_BUNDLE)/Contents/MacOS/"
      @codesign --force --sign "$(SIGN_ID)" "$(APP_BUNDLE)/Contents/MacOS/MyWatchdog"
      # Then sign main app last
      @codesign --force --sign "$(SIGN_ID)" --entitlements entitlements.plist "$(APP_BUNDLE)"
      ```
      
    • structured-concurrency.md 8.5 KB
      # Structured Concurrency
      
      ## Table of Contents
      - Task
      - async let
      - TaskGroup
      - Cancellation
      - Priority
      - Task-Local Values
      - Named Tasks
      - Unstructured Tasks
      
      ## Task
      
      ```swift
      // Create a new top-level task
      Task {
          try await refreshData()
      }
      
      // With priority
      Task(priority: .userInitiated) {
          try await importFile()
      }
      
      // Detached task (no inherited context)
      Task.detached(priority: .background) {
          try await cleanupCache()
      }
      ```
      
      ### Task vs Task.detached
      | | Task | Task.detached |
      |---|------|---------------|
      | Inherits actor | Yes | No |
      | Inherits priority | Yes | No |
      | Inherits task-locals | Yes | No |
      | Use when | Most cases | Independent background work |
      
      ## async let
      
      Concurrent bindings - start multiple async operations in parallel:
      
      ```swift
      func loadDashboard() async throws -> Dashboard {
          async let user = fetchUser()
          async let projects = fetchProjects()
          async let notifications = fetchNotifications()
          async let stats = fetchStats()
      
          // All four requests run concurrently
          // Results collected when accessed
          return try await Dashboard(
              user: user,
              projects: projects,
              notifications: notifications,
              stats: stats
          )
      }
      ```
      
      Child tasks are automatically cancelled if the parent scope exits early. But that cancellation alone does **not** implement a timeout — to enforce one you must *race* the work against a sleep using a task group. `async let _ = Task.sleep(...)` is a common trap: the sleep runs concurrently but nothing awaits or races it, so `try await data` still waits forever if the fetch hangs.
      
      ```swift
      struct TimeoutError: Error {}
      
      /// Race `operation` against a sleep; first to finish wins, the loser is cancelled.
      /// Stdlib equivalent `withDeadline` is SE-0526, accepted with modifications 2026-07-29.
      func withTimeout<T: Sendable>(
          seconds: Double,
          operation: sending @escaping () async throws -> T
      ) async throws -> T {
          try await withThrowingTaskGroup(of: T.self) { group in
              group.addTask { try await operation() }
              group.addTask {
                  try await Task.sleep(for: .seconds(seconds))
                  throw TimeoutError()
              }
              let result = try await group.next()!
              group.cancelAll()                 // cancels the loser
              return result
          }
      }
      
      // Usage
      func loadWithTimeout() async throws -> Data {
          try await withTimeout(seconds: 10) {
              try await fetchLargeDataset()
          }
      }
      ```
      
      `withThrowingTaskGroup` + sleep is the canonical pattern until [SE-0526 `withDeadline`](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0526-deadline.md) ships - it was **accepted with modifications on 2026-07-29** and is not in the 6.3.3 toolchain, so keep the helper for now. SE-0526 expresses the limit as a composable absolute clock instant rather than a duration, cancelling the operation if it has not completed in time. Do not reach for `swift-async-algorithms` - the timeout feature was explicitly pulled from that package in favor of the stdlib path.
      
      **Caveat: the helper is cooperative.** Swift's concurrency runtime cannot forcibly stop a task that isn't observing cancellation. `group.cancelAll()` only flips the cancellation flag; if `operation` is a tight synchronous loop, a blocking C bridge, or any path without `try Task.checkCancellation()` / cancellation-aware suspension points, the TaskGroup scope still waits for it and the call hangs past `seconds`. Use this pattern for cancellation-cooperative work (`URLSession`, `Task.sleep`, most modern async APIs). For CPU-bound or C-bridged work, sprinkle `try Task.checkCancellation()` into the work at suspension-friendly granularity, or dispatch the work to a queue/thread that you can kill independently.
      
      ## TaskGroup
      
      Dynamic number of concurrent tasks:
      
      ```swift
      // Throwing task group
      func fetchAllPages() async throws -> [Page] {
          try await withThrowingTaskGroup(of: Page.self) { group in
              for id in pageIDs {
                  group.addTask {
                      try await fetchPage(id)
                  }
              }
      
              var pages: [Page] = []
              for try await page in group {
                  pages.append(page)
              }
              return pages
          }
      }
      
      // With ordered results
      func processFiles(_ urls: [URL]) async throws -> [Result] {
          try await withThrowingTaskGroup(of: (Int, Result).self) { group in
              for (index, url) in urls.enumerated() {
                  group.addTask {
                      let result = try await process(url)
                      return (index, result)
                  }
              }
      
              var results = [(Int, Result)]()
              for try await pair in group {
                  results.append(pair)
              }
              return results.sorted(by: { $0.0 < $1.0 }).map(\.1)
          }
      }
      
      // Limiting concurrency
      func downloadImages(_ urls: [URL], maxConcurrent: Int = 4) async throws -> [NSImage] {
          try await withThrowingTaskGroup(of: (Int, NSImage).self) { group in
              var results = [(Int, NSImage)]()
              var nextIndex = 0
      
              // Start initial batch
              for i in 0..<min(maxConcurrent, urls.count) {
                  let url = urls[i]
                  group.addTask { (i, try await downloadImage(url)) }
                  nextIndex = i + 1
              }
      
              // As each completes, start next
              for try await result in group {
                  results.append(result)
                  if nextIndex < urls.count {
                      let url = urls[nextIndex]
                      let idx = nextIndex
                      group.addTask { (idx, try await downloadImage(url)) }
                      nextIndex += 1
                  }
              }
      
              return results.sorted(by: { $0.0 < $1.0 }).map(\.1)
          }
      }
      ```
      
      ## Cancellation
      
      ```swift
      // Check cancellation
      func processItems(_ items: [Item]) async throws -> [Result] {
          var results: [Result] = []
          for item in items {
              // Check before expensive operation
              try Task.checkCancellation()
              let result = try await process(item)
              results.append(result)
          }
          return results
      }
      
      // Non-throwing cancellation check
      if Task.isCancelled {
          return partialResults // Return what we have
      }
      
      // Cancel a task
      let task = Task {
          try await longRunningOperation()
      }
      // Later:
      task.cancel()
      
      // withTaskCancellationHandler
      func download(_ url: URL) async throws -> Data {
          let session = URLSession.shared
          return try await withTaskCancellationHandler {
              try await session.data(from: url).0
          } onCancel: {
              // Clean up resources
              session.invalidateAndCancel()
          }
      }
      ```
      
      ## Priority
      
      ```swift
      // Task priorities
      Task(priority: .userInitiated) { }  // User is waiting
      Task(priority: .medium) { }          // Default
      Task(priority: .utility) { }         // Long-running, user aware
      Task(priority: .background) { }      // User not waiting
      Task(priority: .low) { }             // Lowest
      
      // TaskGroup with priority
      await withTaskGroup(of: Void.self) { group in
          group.addTask(priority: .high) { await urgentWork() }
          group.addTask(priority: .low) { await backgroundWork() }
      }
      
      // Current task priority
      let priority = Task.currentPriority
      ```
      
      ## Task-Local Values
      
      Thread-safe context propagation:
      
      ```swift
      enum RequestContext {
          @TaskLocal static var requestID: String = "unknown"
          @TaskLocal static var userID: String?
      }
      
      func handleRequest() async {
          await RequestContext.$requestID.withValue(UUID().uuidString) {
              await RequestContext.$userID.withValue("user-123") {
                  // All child tasks inherit these values
                  await processRequest()
              }
          }
      }
      
      func processRequest() async {
          print(RequestContext.requestID) // The inherited value
          print(RequestContext.userID)     // "user-123"
      }
      ```
      
      ## Named Tasks (Swift 6.2)
      
      Assign human-readable names for debugging:
      
      ```swift
      Task(name: "Refresh dashboard data") {
          try await dashboard.refresh()
      }
      
      Task(name: "Export \(document.name)") {
          try await exporter.export(document)
      }
      
      // Visible in:
      // - LLDB: `swift task list`
      // - Instruments: Swift Concurrency instrument
      // - Xcode: Debug navigator > Task column
      ```
      
      ## Unstructured Tasks
      
      When you need tasks that outlive their creation scope:
      
      ```swift
      class DocumentController {
          private var saveTask: Task<Void, Error>?
      
          func autoSave() {
              // Cancel previous save
              saveTask?.cancel()
      
              // Start new save with debounce
              saveTask = Task {
                  try await Task.sleep(for: .seconds(2))
                  try await save()
              }
          }
      
          deinit {
              saveTask?.cancel()
          }
      }
      ```
      
      **Prefer structured concurrency** (async let, TaskGroup) over unstructured Task when possible - it provides automatic cancellation and clearer lifetime management.
      
    • swiftui-macos.md 13.3 KB
      # macOS-Specific SwiftUI
      
      ## Table of Contents
      - Sidebar & Inspectors
      - Table View
      - Forms & Controls
      - Popovers & Sheets
      - Search
      - Split Views & Layout
      - Liquid Glass (macOS 26)
      - macOS Modifiers
      - Platform Conditionals
      
      ## Sidebar
      
      ```swift
      struct SidebarView: View {
          @Binding var selection: SidebarItem?
      
          var body: some View {
              List(selection: $selection) {
                  Section("Favorites") {
                      ForEach(favorites) { item in
                          Label(item.name, systemImage: item.icon)
                              .tag(item)
                              .badge(item.count)
                      }
                  }
      
                  Section("Collections") {
                      ForEach(collections) { collection in
                          Label(collection.name, systemImage: "folder")
                              .tag(SidebarItem.collection(collection.id))
                      }
                  }
              }
              .listStyle(.sidebar)
              .frame(minWidth: 200)
              .toolbar {
                  ToolbarItem {
                      Button(action: addCollection) {
                          Label("New Collection", systemImage: "folder.badge.plus")
                      }
                  }
              }
          }
      }
      ```
      
      ## Inspector
      
      ```swift
      struct ContentView: View {
          @State private var showInspector = false
          @State private var selectedItem: Item?
      
          var body: some View {
              MainContentView(selection: $selectedItem)
                  .inspector(isPresented: $showInspector) {
                      if let item = selectedItem {
                          InspectorView(item: item)
                              .inspectorColumnWidth(min: 200, ideal: 300, max: 400)
                      }
                  }
                  .toolbar {
                      ToolbarItem {
                          Button {
                              showInspector.toggle()
                          } label: {
                              Label("Inspector", systemImage: "sidebar.trailing")
                          }
                      }
                  }
          }
      }
      ```
      
      ## Table View
      
      Full-featured macOS table with sorting, selection, and context menus:
      
      ```swift
      struct FileListView: View {
          @State private var files: [FileItem] = []
          @State private var selectedIDs: Set<FileItem.ID> = []
          @State private var sortOrder = [KeyPathComparator(\FileItem.name)]
      
          var body: some View {
              Table(files, selection: $selectedIDs, sortOrder: $sortOrder) {
                  TableColumn("Name", value: \.name) { file in
                      Label(file.name, systemImage: file.icon)
                  }
                  .width(min: 150, ideal: 250)
      
                  TableColumn("Size", value: \.size) { file in
                      Text(file.size, format: .byteCount(style: .file))
                  }
                  .width(80)
      
                  TableColumn("Modified", value: \.modifiedDate) { file in
                      Text(file.modifiedDate, format: .dateTime.month().day().hour().minute())
                  }
                  .width(min: 100, ideal: 150)
      
                  TableColumn("Kind", value: \.kind)
                      .width(100)
              }
              .onChange(of: sortOrder) { _, newOrder in
                  files.sort(using: newOrder)
              }
              .contextMenu(forSelectionType: FileItem.ID.self) { ids in
                  Button("Open") { openFiles(ids) }
                  Button("Reveal in Finder") { revealInFinder(ids) }
                  Divider()
                  Button("Delete", role: .destructive) { deleteFiles(ids) }
              } primaryAction: { ids in
                  openFiles(ids)
              }
          }
      }
      ```
      
      ## Forms & Controls
      
      macOS-optimized form layout:
      
      ```swift
      Form {
          Section("General") {
              TextField("Name", text: $name)
              TextField("Description", text: $description, axis: .vertical)
                  .lineLimit(3...6)
      
              Picker("Category", selection: $category) {
                  ForEach(Category.allCases) { cat in
                      Text(cat.rawValue).tag(cat)
                  }
              }
              .pickerStyle(.menu) // .radioGroup, .segmented, .inline
      
              DatePicker("Due Date", selection: $dueDate, displayedComponents: [.date])
          }
      
          Section("Options") {
              Toggle("Enable notifications", isOn: $notificationsEnabled)
                  .toggleStyle(.checkbox) // macOS default
      
              Stepper("Priority: \(priority)", value: $priority, in: 1...5)
      
              Slider(value: $opacity, in: 0...1) {
                  Text("Opacity")
              }
          }
      }
      .formStyle(.grouped) // macOS grouped form style
      ```
      
      ## Popovers
      
      ```swift
      Button("Info") {
          showPopover = true
      }
      .popover(isPresented: $showPopover, arrowEdge: .bottom) {
          VStack(alignment: .leading, spacing: 8) {
              Text("Details").font(.headline)
              Text("Additional information here")
              Link("Learn More", destination: helpURL)
          }
          .padding()
          .frame(width: 250)
      }
      ```
      
      ## Sheets (macOS)
      
      ```swift
      .sheet(isPresented: $showingNewProject) {
          NewProjectSheet()
              .frame(minWidth: 400, minHeight: 300)
      }
      
      struct NewProjectSheet: View {
          @Environment(\.dismiss) private var dismiss
      
          var body: some View {
              VStack {
                  // Content
                  HStack {
                      Button("Cancel", role: .cancel) { dismiss() }
                          .keyboardShortcut(.cancelAction)
                      Spacer()
                      Button("Create") { create(); dismiss() }
                          .keyboardShortcut(.defaultAction)
                          .buttonStyle(.borderedProminent)
                  }
              }
              .padding()
          }
      }
      ```
      
      ## Search
      
      ```swift
      struct SearchableListView: View {
          @State private var searchText = ""
          @State private var searchScope: SearchScope = .all
      
          var body: some View {
              List(filteredItems) { item in
                  ItemRow(item: item)
              }
              .searchable(text: $searchText, placement: .toolbar, prompt: "Search items")
              .searchScopes($searchScope) {
                  Text("All").tag(SearchScope.all)
                  Text("Active").tag(SearchScope.active)
                  Text("Archived").tag(SearchScope.archived)
              }
              .searchSuggestions {
                  ForEach(suggestions) { suggestion in
                      Text(suggestion.text)
                          .searchCompletion(suggestion.text)
                  }
              }
          }
      }
      ```
      
      ## Split Views
      
      ### HSplitView / VSplitView (AppKit-backed)
      ```swift
      HSplitView {
          LeftPanel()
              .frame(minWidth: 200, maxWidth: 400)
          RightPanel()
              .frame(minWidth: 300)
      }
      ```
      
      ### NavigationSplitView Visibility
      ```swift
      @State private var columnVisibility: NavigationSplitViewVisibility = .all
      
      NavigationSplitView(columnVisibility: $columnVisibility) {
          SidebarView()
      } detail: {
          DetailView()
      }
      .navigationSplitViewStyle(.balanced) // or .prominentDetail
      ```
      
      ## Liquid Glass (macOS 26)
      
      Apps rebuilt with the Xcode 26 SDK automatically adopt Liquid Glass styling. For custom surfaces and animated shape changes, use the dedicated APIs rather than rolling material backgrounds by hand.
      
      ### Basic glass surfaces
      
      ```swift
      // On any view:
      Rectangle()
          .glassEffect()                       // .regular by default
      Rectangle().glassEffect(.clear)          // less opaque
      Rectangle().glassEffect(.regular.tint(.blue).interactive())
      
      // On buttons:
      Button("Primary") { /* ... */ }
          .buttonStyle(.glassProminent)
      Button("Secondary") { /* ... */ }
          .buttonStyle(.glass)
      ```
      
      ### Morphing transitions between shapes
      
      `GlassEffectContainer` + `.glassEffectID(_:in:)` with a `@Namespace` produces a continuous glass morph when shapes swap rather than a cross-fade:
      
      ```swift
      struct MorphingCard: View {
          @Namespace private var glassNamespace
          @State private var expanded = false
      
          var body: some View {
              GlassEffectContainer(spacing: 8) {
                  if expanded {
                      RoundedRectangle(cornerRadius: 24)
                          .glassEffect()
                          .glassEffectID("card", in: glassNamespace)
                          .frame(width: 400, height: 280)
                  } else {
                      Circle()
                          .glassEffect()
                          .glassEffectID("card", in: glassNamespace)
                          .frame(width: 80, height: 80)
                  }
              }
              .onTapGesture { withAnimation(.smooth) { expanded.toggle() } }
          }
      }
      ```
      
      ### Glass toolbars
      
      Use `ToolbarSpacer` to group glass items so they render as distinct glass surfaces rather than one merged shape. Full signature is `init(_ sizing: SpacerSizing = .flexible, placement: ToolbarItemPlacement = .automatic)` — both parameters default, so the short form works:
      
      ```swift
      .toolbar {
          ToolbarItem { Button("New") { /* ... */ }.buttonStyle(.glass) }
          ToolbarSpacer(.fixed)            // equivalent to: ToolbarSpacer(.fixed, placement: .automatic)
          ToolbarItem { Button("Share") { /* ... */ }.buttonStyle(.glass) }
      }
      ```
      
      Available macOS 26 / iOS 26+. Docs: https://developer.apple.com/documentation/swiftui/toolbarspacer
      
      ### Opting out
      
      Set `UIDesignRequiresCompatibility = YES` (Boolean) in Info.plist to keep the legacy visual style app-wide. It's a temporary migration aid — Apple expects to remove it in a future release; there is no per-view or per-window opt-out. Docs: https://developer.apple.com/documentation/bundleresources/information-property-list/uidesignrequirescompatibility
      
      ## macOS Modifiers
      
      ```swift
      // Window background
      .containerBackground(.ultraThinMaterial, for: .window)
      
      // Hover effect
      .onHover { isHovered in
          self.isHovered = isHovered
      }
      
      // Pointer style (macOS 15+)
      .pointerStyle(.link)   // also: .grabIdle, .zoomIn, .horizontalText, etc.
      // Pre-macOS-15: there is no built-in SwiftUI cursor modifier; wrap an
      // NSViewRepresentable that overrides resetCursorRects. Docs:
      // https://developer.apple.com/documentation/swiftui/view/pointerstyle(_:)
      
      // Visual effect (vibrancy)
      .background(.ultraThinMaterial)
      
      // Focus state (keyboard navigation)
      @FocusState private var isFocused: Bool
      TextField("Name", text: $name)
          .focused($isFocused)
      
      // Help tag (tooltip)
      .help("Click to save your changes")
      
      // File importer/exporter
      .fileImporter(isPresented: $importing, allowedContentTypes: [.json]) { result in
          // handle result
      }
      .fileExporter(isPresented: $exporting, document: doc, contentType: .json) { result in
          // handle result
      }
      ```
      
      ## Platform Conditionals
      
      ```swift
      // Prefer #if over runtime checks
      #if os(macOS)
          .frame(minWidth: 800, minHeight: 600)
          .toolbar { MacToolbar() }
      #elseif os(iOS)
          .toolbar { IOSToolbar() }
      #endif
      
      // Or use view modifiers
      .macOS { view in
          view.frame(minWidth: 800)
      }
      
      // Multiplatform extension helper
      extension View {
          @ViewBuilder
          func macOS<Content: View>(@ViewBuilder modify: (Self) -> Content) -> some View {
              #if os(macOS)
              modify(self)
              #else
              self
              #endif
          }
      }
      ```
      
      ## `@Entry` - custom environment keys in one line
      
      The `EnvironmentKey` + `EnvironmentValues` extension boilerplate is obsolete. `@Entry` generates both:
      
      ```swift
      extension EnvironmentValues {
          @Entry var captureSession: CaptureSession? = nil
          @Entry var isCompactLayout: Bool = false
      }
      
      // Unchanged at the use site
      .environment(\.captureSession, session)
      @Environment(\.captureSession) private var session
      ```
      
      Works for `FocusedValues`, `ContainerValues`, and `Transaction` too.
      
      ## SwiftUI `WebView` (macOS 26+)
      
      WebKit ships a native SwiftUI view - `NSViewRepresentable` around `WKWebView` is no longer the default answer on macOS 26:
      
      ```swift
      import WebKit
      import SwiftUI
      
      struct DocsView: View {
          @State private var page = WebPage()
      
          var body: some View {
              WebView(page)
                  .onAppear { page.load(URLRequest(url: docsURL)) }
                  .navigationTitle(page.title)
          }
      }
      ```
      
      `WebPage` is `@Observable` - `page.title`, `page.url`, `page.isLoading`, and `page.estimatedProgress` drive SwiftUI directly with no KVO bridging. Call `page.callJavaScript(_:)` for evaluation. Keep the `NSViewRepresentable` wrapper only for back-deployment below macOS 26 or when you need `WKWebView` API the SwiftUI type does not expose.
      
      ## Rich text editing (macOS 26+)
      
      `TextEditor` binds to `AttributedString`, making formatted text a first-class SwiftUI feature:
      
      ```swift
      struct NotesEditor: View {
          @State private var text = AttributedString("")
          @State private var selection = AttributedTextSelection()
      
          var body: some View {
              TextEditor(text: $text, selection: $selection)
                  .attributedTextFormattingDefinition(NoteFormatting())
          }
      }
      ```
      
      `AttributedTextFormattingDefinition` constrains which attributes users may apply - use it rather than post-validating, so unsupported formatting never enters the document. The standard Format menu commands work against the selection automatically.
      
      ## Window and scene APIs beyond `Window`/`WindowGroup`
      
      | API | Use |
      |---|---|
      | `UtilityWindow` | Panel-style scene (inspector, tool palette) without dropping to `NSPanel` |
      | `SettingsLink` | A button that opens Settings - correct across macOS versions, unlike hand-rolled selectors |
      | `defaultLaunchBehavior(.presented / .suppressed)` | Whether a scene opens at launch - the supported way to start windowless |
      | `WindowManagerRole` | Declares a scene's role to the window manager |
      | `windowResizeAnchor` | **View** modifier (macOS 26+), not a Scene one: `windowResizeAnchor(_ anchor: UnitPoint?)` - the anchor point that stays fixed when the window resizes |
      | `WindowDragGesture` | Drag the window from arbitrary content, not just the title bar |
      
      `defaultLaunchBehavior(.suppressed)` is the clean replacement for the `LSUIElement`-plus-close-the-window dance when an app should start with no visible window but is not permanently an accessory.
      
    • system-integration.md 28.7 KB
      # System Integration
      
      ## Table of Contents
      - Keyboard Shortcuts
      - Drag & Drop
      - File System Access
      - UserDefaults & AppStorage
      - App Intents
      - Widgets & Control Center
      - Notifications
      - Process Observation
      - Accessibility API (AXUIElement)
      - CoreAudio Per-Process APIs
      - Login Items (SMAppService)
      - XPC (XPCSession / XPCListener)
      - LSUIElement & Background Apps
      - Idle Sleep Prevention
      - Logging & Diagnostics
      - Privacy usage descriptions
      
      ## Keyboard Shortcuts
      
      ### In commands
      ```swift
      .commands {
          CommandMenu("Edit") {
              Button("Find...") { showFind = true }
                  .keyboardShortcut("f", modifiers: .command)
              Button("Replace...") { showReplace = true }
                  .keyboardShortcut("h", modifiers: [.command, .option])
          }
      }
      ```
      
      ### On buttons
      ```swift
      Button("Save") { save() }
          .keyboardShortcut("s", modifiers: .command)
      
      Button("Delete") { delete() }
          .keyboardShortcut(.delete, modifiers: .command)
      
      Button("Cancel") { cancel() }
          .keyboardShortcut(.cancelAction) // Esc
      
      Button("OK") { confirm() }
          .keyboardShortcut(.defaultAction) // Return
      ```
      
      ### Global key handlers
      ```swift
      .onKeyPress(.return) {
          submitForm()
          return .handled
      }
      
      .onKeyPress(characters: .alphanumerics) { press in
          handleTyping(press.characters)
          return .handled
      }
      
      .onKeyPress(phases: .down) { press in
          if press.key == .space {
              startPreview()
              return .handled
          }
          return .ignored
      }
      ```
      
      ## Drag & Drop
      
      ### Draggable
      ```swift
      struct ItemCard: View {
          let item: Item
      
          var body: some View {
              VStack { /* content */ }
                  .draggable(item) // Item must conform to Transferable
          }
      }
      
      // Transferable conformance
      extension Item: Transferable {
          static var transferRepresentation: some TransferRepresentation {
              CodableRepresentation(for: Item.self, contentType: .json)
              ProxyRepresentation(exporting: \.name) // fallback to string
          }
      }
      ```
      
      ### Drop target
      ```swift
      .dropDestination(for: Item.self) { items, location in
          // Handle dropped items
          collection.append(contentsOf: items)
          return true
      }
      
      // File drops
      .dropDestination(for: URL.self) { urls, location in
          importFiles(urls)
          return true
      }
      ```
      
      ### Drag preview
      ```swift
      .draggable(item) {
          // Custom preview
          Label(item.name, systemImage: "doc")
              .padding(8)
              .background(.regularMaterial)
              .clipShape(RoundedRectangle(cornerRadius: 8))
      }
      ```
      
      ### Spring-loaded destination
      ```swift
      .springLoadedDestination(for: Item.self) { items in
          // Auto-open folder/collection when hovering with drag
          navigateToCollection(items)
      }
      ```
      
      ## File System Access
      
      ### Security-scoped bookmarks (for sandboxed apps)
      ```swift
      func saveBookmark(for url: URL) throws {
          let bookmarkData = try url.bookmarkData(
              options: .withSecurityScope,
              includingResourceValuesForKeys: nil,
              relativeTo: nil
          )
          UserDefaults.standard.set(bookmarkData, forKey: "savedBookmark")
      }
      
      func resolveBookmark() throws -> URL {
          let data = UserDefaults.standard.data(forKey: "savedBookmark")!
          var isStale = false
          let url = try URL(
              resolvingBookmarkData: data,
              options: .withSecurityScope,
              relativeTo: nil,
              bookmarkDataIsStale: &isStale
          )
      
          guard url.startAccessingSecurityScopedResource() else {
              throw FileError.accessDenied
          }
          // Remember to call url.stopAccessingSecurityScopedResource() when done
      
          return url
      }
      ```
      
      ### FileManager
      ```swift
      // App support directory
      let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
      let appDir = appSupport.appendingPathComponent(Bundle.main.bundleIdentifier!)
      
      // Create directory
      try FileManager.default.createDirectory(at: appDir, withIntermediateDirectories: true)
      
      // Temporary directory
      let tempDir = FileManager.default.temporaryDirectory
      ```
      
      ## App Intents
      
      Expose actions to Shortcuts and Siri:
      
      ```swift
      import AppIntents
      
      struct CreateProjectIntent: AppIntent {
          static var title: LocalizedStringResource = "Create Project"
          static var description: IntentDescription = "Creates a new project"
      
          @Parameter(title: "Name")
          var name: String
      
          @Parameter(title: "Template", default: .blank)
          var template: ProjectTemplate
      
          func perform() async throws -> some IntentResult & ReturnsValue<String> {
              let project = try await ProjectService.create(name: name, template: template)
              return .result(value: project.id.uuidString)
          }
      }
      
      // Register with app
      struct MyAppShortcuts: AppShortcutsProvider {
          static var appShortcuts: [AppShortcut] {
              AppShortcut(
                  intent: CreateProjectIntent(),
                  phrases: ["Create a project in \(.applicationName)"],
                  shortTitle: "Create Project",
                  systemImageName: "folder.badge.plus"
              )
          }
      }
      ```
      
      ### Beyond primitives: entities
      
      An `AppIntent` whose parameters are only `String`/`Int` can never reference your app's actual data. `AppEntity` is the type system that fixes that - "an interface for making a custom type or app-specific concept discoverable by Apple Intelligence and experiences like Siri or the Shortcuts app" (macOS 13+). The three pieces:
      
      | Type | Role |
      |---|---|
      | `AppEntity` | A model object addressable from Shortcuts/Siri - has an `id`, a `displayRepresentation`, and a query |
      | `EntityQuery` | How the system finds entities: by id, by string match, or by suggestion |
      | `AppEnum` | A fixed set of choices surfaced as a picker (what `ProjectTemplate` above should be) |
      
      ### Spotlight indexing
      
      Conform an entity to `IndexedEntity` to put your app's records in Spotlight:
      
      > Make app entities available in Spotlight that conform to `IndexedEntity` and use the `@ComputedProperty(indexingKey:)` or `@Property(indexingKey:)` Swift macros for attributes you want to add to the Spotlight index.
      
      ### Interactive snippets (macOS 26)
      
      `SnippetIntent` renders an interactive SwiftUI snippet as the intent's result rather than a plain value. One trap Apple calls out explicitly:
      
      > If an app intent conforms to `SnippetIntent` and only returns a snippet ... it's nondiscoverable by the Shortcuts app and in Spotlight. To make such an intent discoverable, explicitly set `isDiscoverable` to `true`.
      
      ### Foreground continuation: use `supportedModes`
      
      `ForegroundContinuableIntent` is **deprecated at macOS 26.0**. Apple's replacement note: "Please include `.foreground(.dynamic)` in the `supportedModes` of your app intent instead." Declare `supportedModes` (`IntentModes`) on the intent rather than conforming to the old protocol.
      
      ### macOS-only onscreen-content bridges
      
      The `UI*` data sources in Apple's App Intents documentation are iOS-only. The Mac equivalents are `NSTableViewAppIntentsDataSource` and `NSCollectionViewAppIntentsDataSource` (macOS 15.4+) - "the methods that an object adopts to make items in a table view or outline view discoverable by Apple Intelligence and Siri." Adopt them so Siri can act on the row a user is looking at.
      
      ## Widgets & Control Center
      
      WidgetKit is macOS 11+ and covers desktop and Notification Center widgets. Two macOS-specific points the MenuBarExtra crowd usually wants:
      
      **Control Center controls reached macOS in 26.0.** Previously watchOS/iOS only:
      
      > Create controls that use `ControlWidgetButton` to execute an action and `ControlWidgetToggle` to toggle some state in your app in watchOS **and macOS**.
      
      The building blocks are `ControlWidget` (a SwiftUI widget kind), `ControlWidgetButton`, `ControlWidgetToggle`, and `ControlConfigurationIntent` for a configurable control. A control is often a better fit than a `MenuBarExtra` for a single toggle - it costs no menu-bar real estate and the system handles placement.
      
      **Liquid Glass rendering.** Use `WidgetAccentedRenderingMode` to control how widget images are treated under the macOS 26 rendering modes; `WidgetPushHandler` drives push-based timeline reloads.
      
      ## Notifications (System)
      
      ### User Notifications
      ```swift
      import UserNotifications
      
      func requestPermission() async throws -> Bool {
          try await UNUserNotificationCenter.current()
              .requestAuthorization(options: [.alert, .sound, .badge])
      }
      
      func scheduleNotification(title: String, body: String) {
          let content = UNMutableNotificationContent()
          content.title = title
          content.body = body
          content.sound = .default
      
          let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
          let request = UNNotificationRequest(identifier: UUID().uuidString,
                                               content: content, trigger: trigger)
      
          UNUserNotificationCenter.current().add(request)
      }
      ```
      
      ## UserDefaults & AppStorage
      
      ```swift
      // @AppStorage in views
      @AppStorage("selectedTheme") private var theme = "system"
      
      // UserDefaults directly
      extension UserDefaults {
          var lastSyncDate: Date? {
              get { object(forKey: "lastSyncDate") as? Date }
              set { set(newValue, forKey: "lastSyncDate") }
          }
      }
      
      // App group (for sharing between app and extensions)
      let sharedDefaults = UserDefaults(suiteName: "group.com.myapp")
      @AppStorage("shared_key", store: UserDefaults(suiteName: "group.com.myapp"))
      private var sharedValue = ""
      ```
      
      ## Process Observation
      
      ### Currently running apps
      
      ```swift
      import AppKit
      
      let runningApps = NSWorkspace.shared.runningApplications
      let isTargetRunning = runningApps.contains { $0.bundleIdentifier == "com.example.target" }
      
      // NSRunningApplication properties
      app.localizedName       // String?
      app.bundleIdentifier    // String?
      app.processIdentifier   // pid_t (Int32)
      app.isActive            // Bool (frontmost)
      app.isTerminated        // Bool
      app.launchDate          // Date?
      app.icon                // NSImage?
      app.activationPolicy    // .regular, .accessory, .prohibited
      ```
      
      ### Notification-based observation
      
      Use `NSWorkspace.shared.notificationCenter` (NOT `NotificationCenter.default`):
      
      ```swift
      // App launched
      NSWorkspace.shared.notificationCenter.addObserver(
          forName: NSWorkspace.didLaunchApplicationNotification,
          object: nil, queue: .main
      ) { notification in
          if let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey]
              as? NSRunningApplication {
              print("Launched: \(app.localizedName ?? "?") (\(app.bundleIdentifier ?? "?")")
          }
      }
      
      // App terminated
      NSWorkspace.shared.notificationCenter.addObserver(
          forName: NSWorkspace.didTerminateApplicationNotification,
          object: nil, queue: .main
      ) { notification in
          if let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey]
              as? NSRunningApplication {
              print("Terminated: \(app.localizedName ?? "?")")
          }
      }
      ```
      
      All workspace notifications:
      
      | Notification | Fires when |
      |-------------|------------|
      | `didLaunchApplicationNotification` | App starts (not background/LSUIElement apps) |
      | `didTerminateApplicationNotification` | App terminates (not background/LSUIElement apps) |
      | `didActivateApplicationNotification` | App becomes frontmost |
      | `didDeactivateApplicationNotification` | App loses frontmost |
      | `didHideApplicationNotification` | App hidden |
      | `didUnhideApplicationNotification` | App unhidden |
      
      ### KVO for ALL apps (including background/LSUIElement)
      
      `didLaunchApplicationNotification` does NOT fire for background or LSUIElement apps. Use KVO instead:
      
      ```swift
      class AppMonitor: NSObject {
          private var observation: NSKeyValueObservation?
      
          func startObserving() {
              observation = NSWorkspace.shared.observe(
                  \.runningApplications, options: [.new, .old]
              ) { workspace, change in
                  let oldPIDs = Set(change.oldValue?.map(\.processIdentifier) ?? [])
                  let newPIDs = Set(change.newValue?.map(\.processIdentifier) ?? [])
      
                  let launched = newPIDs.subtracting(oldPIDs)
                  for app in workspace.runningApplications where launched.contains(app.processIdentifier) {
                      print("Launched: \(app.localizedName ?? "?")")
                  }
              }
          }
      
          func stopObserving() {
              observation?.invalidate()
              observation = nil
          }
      }
      ```
      
      ### Background monitoring pattern
      
      Combine notifications + KVO + optional safety-net poll:
      
      ```swift
      @Observable
      class ProcessMonitor {
          var watchedBundleIDs: Set<String> = []
          private(set) var activeWatchedApps: [NSRunningApplication] = []
      
          private var tokens: [NSObjectProtocol] = []
          private var kvoObservation: NSKeyValueObservation?
      
          func startMonitoring() {
              refreshActiveApps()
      
              // Notifications for regular apps
              tokens.append(NSWorkspace.shared.notificationCenter.addObserver(
                  forName: NSWorkspace.didLaunchApplicationNotification,
                  object: nil, queue: .main
              ) { [weak self] _ in self?.refreshActiveApps() })
      
              tokens.append(NSWorkspace.shared.notificationCenter.addObserver(
                  forName: NSWorkspace.didTerminateApplicationNotification,
                  object: nil, queue: .main
              ) { [weak self] _ in self?.refreshActiveApps() })
      
              // KVO for background/LSUIElement apps
              kvoObservation = NSWorkspace.shared.observe(\.runningApplications, options: [.new]) {
                  [weak self] _, _ in
                  DispatchQueue.main.async { self?.refreshActiveApps() }
              }
          }
      
          func stopMonitoring() {
              tokens.forEach { NSWorkspace.shared.notificationCenter.removeObserver($0) }
              tokens.removeAll()
              kvoObservation?.invalidate()
          }
      
          private func refreshActiveApps() {
              activeWatchedApps = NSWorkspace.shared.runningApplications.filter {
                  guard let id = $0.bundleIdentifier else { return false }
                  return watchedBundleIDs.contains(id)
              }
          }
      }
      ```
      
      ## Accessibility API (AXUIElement)
      
      `NSWorkspace` and KVO tell you *which* apps are running. `AXUIElement` - "a structure used to refer to an accessibility object" - is how a Mac utility reads and drives *another app's* UI: window titles, focused element, text selection, button presses. It is the foundation of window managers, launchers, text-expansion tools, and automation utilities.
      
      Practical constraints that decide whether a feature is even viable:
      
      - Requires the **Accessibility** TCC permission (System Settings > Privacy & Security > Accessibility), granted per app by the user. Check with `AXIsProcessTrusted()`; prompt with `AXIsProcessTrustedWithOptions` passing `kAXTrustedCheckOptionPrompt`.
      - Like screen recording, the grant keys off the app's code signature, so re-signing forces a re-grant (see the TCC notes in `distribution.md`).
      - It is a C API (`ApplicationServices`) with `AXUIElementCopyAttributeValue` returning `CFTypeRef` - the same Create-Rule bridging care as the CoreAudio CFString trap in `core-audio-tap.md` applies.
      - **Not available under the App Sandbox.** An accessibility-driven feature is Developer ID-only.
      
      Docs: <https://developer.apple.com/documentation/applicationservices/axuielement>
      
      ## CoreAudio Per-Process APIs
      
      macOS 14.2+ provides per-process audio state APIs for detecting which apps are using audio I/O. Useful for call detection, audio monitoring, or building audio routing tools.
      
      ```swift
      import CoreAudio
      
      /// Find all processes with active audio input AND output (e.g., call apps)
      func findActiveCallingProcesses() -> [(pid: pid_t, bundleID: String)] {
          var addr = AudioObjectPropertyAddress(
              mSelector: kAudioHardwarePropertyProcessObjectList,
              mScope: kAudioObjectPropertyScopeGlobal,
              mElement: kAudioObjectPropertyElementMain
          )
      
          var size: UInt32 = 0
          guard AudioObjectGetPropertyDataSize(
              AudioObjectID(kAudioObjectSystemObject), &addr, 0, nil, &size
          ) == noErr else { return [] }
      
          let count = Int(size) / MemoryLayout<AudioObjectID>.size
          var objectIDs = [AudioObjectID](repeating: 0, count: count)
          guard AudioObjectGetPropertyData(
              AudioObjectID(kAudioObjectSystemObject), &addr, 0, nil, &size, &objectIDs
          ) == noErr else { return [] }
      
          let myPID = ProcessInfo.processInfo.processIdentifier
          var results: [(pid_t, String)] = []
      
          for objID in objectIDs {
              // Get PID
              var pidAddr = AudioObjectPropertyAddress(
                  mSelector: kAudioProcessPropertyPID,
                  mScope: kAudioObjectPropertyScopeGlobal,
                  mElement: kAudioObjectPropertyElementMain
              )
              var pid: pid_t = 0
              var pidSize = UInt32(MemoryLayout<pid_t>.size)
              guard AudioObjectGetPropertyData(objID, &pidAddr, 0, nil, &pidSize, &pid) == noErr,
                    pid != myPID else { continue }
      
              // Check IsRunningInput AND IsRunningOutput (dual check filters dictation/Siri)
              var inputAddr = AudioObjectPropertyAddress(
                  mSelector: kAudioProcessPropertyIsRunningInput,
                  mScope: kAudioObjectPropertyScopeGlobal,
                  mElement: kAudioObjectPropertyElementMain
              )
              var isInput: UInt32 = 0
              var boolSize = UInt32(MemoryLayout<UInt32>.size)
              guard AudioObjectGetPropertyData(objID, &inputAddr, 0, nil, &boolSize, &isInput) == noErr,
                    isInput != 0 else { continue }
      
              var outputAddr = inputAddr
              outputAddr.mSelector = kAudioProcessPropertyIsRunningOutput
              var isOutput: UInt32 = 0
              guard AudioObjectGetPropertyData(objID, &outputAddr, 0, nil, &boolSize, &isOutput) == noErr,
                    isOutput != 0 else { continue }
      
              // Get bundle ID
              if let bundleID = getBundleID(for: objID) {
                  results.append((pid, bundleID))
              }
          }
          return results
      }
      ```
      
      Key gotchas:
      - **Filter out own PID** and ScreenCaptureKit helper PIDs (`com.apple.screencapturekit*`, `com.apple.replayd`).
      - **Input+Output dual check** filters out dictation, Siri, voice memos (input only). Real call apps (Zoom, Meet, Teams) have both active.
      - **`AudioBufferList` is a variable-length C struct**. `UnsafeMutablePointer<AudioBufferList>.allocate(capacity: 1)` only reserves space for one buffer. Multi-channel devices cause heap overflow. Allocate exact `bufSize` bytes from `GetPropertyDataSize`.
      - **Chrome reports helper subprocess bundle IDs** (e.g., `com.google.Chrome.helper.renderer`). Strip `.helper*` suffix to resolve to the parent app.
      - **Polling (3s interval) is simpler than listeners**. CoreAudio property listeners require `Unmanaged.passUnretained(self)` pointer dance, complex deinit cleanup, and are unreliable with some browser audio pipelines. For call detection, 0-3s delay is negligible.
      - **`CoreAudio.RemovePropertyListenerBlock`** has a known Swift bug where block-copied closures get different addresses, causing removal to fail. Use the C function pointer variant instead.
      
      ## Login Items (SMAppService)
      
      macOS 13+ API for registering login items, agents, and daemons.
      
      ### Register main app as login item
      
      ```swift
      import ServiceManagement
      
      // Register (launches on subsequent logins)
      try SMAppService.mainApp.register()
      
      // Unregister
      try SMAppService.mainApp.unregister()
      
      // Check status
      switch SMAppService.mainApp.status {
      case .notRegistered: print("Not registered")
      case .enabled:       print("Enabled")
      case .requiresApproval:
          SMAppService.openSystemSettingsLoginItems()
      case .notFound:      print("Service not found")
      @unknown default:    break
      }
      ```
      
      ### SwiftUI Settings toggle
      
      Never persist login-item state locally - always read from `SMAppService.mainApp.status`:
      
      ```swift
      import ServiceManagement
      import SwiftUI
      
      struct LaunchAtLoginToggle: View {
          @State private var launchAtLogin = false
          @Environment(\.appearsActive) var appearsActive
      
          var body: some View {
              Toggle("Launch at login", isOn: $launchAtLogin)
                  .onChange(of: launchAtLogin) { _, newValue in
                      if newValue {
                          try? SMAppService.mainApp.register()
                      } else {
                          try? SMAppService.mainApp.unregister()
                      }
                  }
                  .onAppear {
                      launchAtLogin = (SMAppService.mainApp.status == .enabled)
                  }
                  .onChange(of: appearsActive) { _, active in
                      guard active else { return }
                      // Re-sync: user may have toggled in System Settings
                      launchAtLogin = (SMAppService.mainApp.status == .enabled)
                  }
          }
      }
      ```
      
      ### Service types
      
      ```swift
      // Main app login item
      SMAppService.mainApp
      
      // Helper app (in Contents/Library/LoginItems/)
      SMAppService.loginItem(identifier: "com.example.helper")
      
      // LaunchAgent (in Contents/Library/LaunchAgents/)
      SMAppService.agent(plistName: "com.example.agent.plist")
      
      // LaunchDaemon (in Contents/Library/LaunchDaemons/ - requires admin approval)
      SMAppService.daemon(plistName: "com.example.daemon.plist")
      ```
      
      | Type | Runs as | Starts | Can show UI | Approval |
      |------|---------|--------|-------------|----------|
      | mainApp | Current user | Next login | Yes | Background item notification |
      | loginItem | Current user | Immediately + login | Yes | Background item notification |
      | agent | Current user | Immediately + login | If not LSBackgroundOnly | Background item notification |
      | daemon | root | After admin approval | No | Admin authentication |
      
      ### Bundle structure for agents/daemons
      
      ```
      MyApp.app/Contents/
          Library/
              LoginItems/MyHelper.app/       # loginItem bundles
              LaunchAgents/com.example.agent.plist
              LaunchDaemons/com.example.daemon.plist
          Resources/MyHelper                 # helper executable
      ```
      
      Agent plist uses `BundleProgram` (path relative to app bundle):
      
      ```xml
      <dict>
          <key>Label</key>
          <string>com.example.agent</string>
          <key>BundleProgram</key>
          <string>Contents/Resources/MyHelper</string>
          <key>RunAtLoad</key>
          <true/>
      </dict>
      ```
      
      ## XPC (XPCSession / XPCListener)
      
      `SMAppService` registers a LaunchAgent or LaunchDaemon; it does not give you a way to **talk** to it. That is XPC. Since macOS 14 there is a Swift-native API - `XPCSession` ("a type that sends messages to a server process") on the app side, `XPCListener` ("a type that performs tasks for clients across process boundaries") in the helper - so a privileged helper no longer requires the Objective-C `NSXPCConnection` dance.
      
      ```swift
      import XPC
      
      // Client: talk to the registered helper
      let session = try XPCSession(machService: "com.example.MyHelper")
      let reply = try session.sendSync(MyRequest(command: .status))
      
      // Helper: serve
      let listener = try XPCListener(service: "com.example.MyHelper") { request in
          request.accept { (message: MyRequest) -> MyResponse in
              handle(message)
          }
      }
      ```
      
      Messages are `Codable`. The Mach service name must match the `MachServices` key in the helper's launchd plist, and the two binaries must share a team identifier for the connection to be accepted - verify the peer's code signing requirement rather than trusting the connection.
      
      Docs: <https://developer.apple.com/documentation/xpc/xpcsession>
      
      ## LSUIElement & Background Apps
      
      ### LSUIElement (menu-bar-only apps)
      
      Set in Info.plist (`Application is agent (UIElement)`):
      
      ```xml
      <key>LSUIElement</key>
      <true/>
      ```
      
      App does NOT appear in Dock or Cmd+Tab. CAN still show UI (windows, menus, popovers). **This is what menu-bar-only apps use.**
      
      ### LSBackgroundOnly (faceless helpers)
      
      ```xml
      <key>LSBackgroundOnly</key>
      <true/>
      ```
      
      App runs ONLY in background. Cannot show any UI.
      
      | Key | Dock Icon | Can Show UI | Use Case |
      |-----|-----------|-------------|----------|
      | Neither | Yes | Yes | Normal app |
      | `LSUIElement` | No | Yes | Menu bar apps |
      | `LSBackgroundOnly` | No | No | Faceless helpers |
      
      ### Menu-bar-only app pattern
      
      ```swift
      @main
      struct MyMenuBarApp: App {
          var body: some Scene {
              MenuBarExtra("Status", systemImage: "star") {
                  VStack {
                      ContentView()
                      Divider()
                      Button("Quit") { NSApp.terminate(nil) }
                  }
                  .frame(width: 300, height: 200)
              }
              .menuBarExtraStyle(.window)
          }
      }
      ```
      
      With `LSUIElement = true`: no Dock icon, no Cmd+Tab entry, no Force Quit listing. **Always include a Quit button** since users can't right-click the Dock icon.
      
      ### Caveat
      
      `NSWorkspace.didLaunchApplicationNotification` does NOT fire for LSUIElement or background apps. Use KVO on `runningApplications` to detect them (see Process Observation section).
      
      ## Idle Sleep Prevention
      
      Prevent macOS from sleeping during long operations (recording, encoding, uploads):
      
      ```swift
      // Start activity (prevents idle sleep)
      let activity = ProcessInfo.processInfo.beginActivity(
          .userInitiated, reason: "Recording audio"
      )
      
      // ... long-running operation ...
      
      // End activity (allow sleep again)
      ProcessInfo.processInfo.endActivity(activity)
      ```
      
      Use `.userInitiated` for operations the user started. The system will not idle-sleep while the activity is active, but the user can still manually put the machine to sleep.
      
      ## Trap: "is the mic in use?" latches on with a system-wide property
      
      `kAudioDevicePropertyDeviceIsRunningSomewhere` on the default input device looks like the way to auto-trigger recording when another app starts using the microphone. It is not: the property is system-wide and includes **your** process. Once your app opens the mic in response to the trigger, the property stays `true` after the other app stops, so the trigger never releases and auto-stop never fires.
      
      Use the per-process CoreAudio APIs (macOS 14.2+) and exclude yourself:
      
      ```swift
      import CoreAudio
      
      func otherProcessIsUsingInput() -> Bool {
          var address = AudioObjectPropertyAddress(
              mSelector: kAudioHardwarePropertyProcessObjectList,
              mScope: kAudioObjectPropertyScopeGlobal,
              mElement: kAudioObjectPropertyElementMain
          )
          var size: UInt32 = 0
          guard AudioObjectGetPropertyDataSize(
              AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size) == noErr else { return false }
      
          var processes = [AudioObjectID](repeating: 0, count: Int(size) / MemoryLayout<AudioObjectID>.size)
          guard AudioObjectGetPropertyData(
              AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &processes) == noErr else { return false }
      
          let myPID = ProcessInfo.processInfo.processIdentifier
          for process in processes {
              guard pid(for: process) != myPID else { continue }
              if isRunningInput(process) { return true }   // kAudioProcessPropertyIsRunningInput
          }
          return false
      }
      ```
      
      Two caveats before designing around this:
      
      - **App Sandbox**: these low-level per-process APIs are not reliably available under sandboxing, which makes a mic-activity auto-trigger a Mac App Store blocker. Plan the feature as Developer ID-only, or provide a manual path.
      - Poll on a timer or install a property listener on the process list; there is no single "someone started using the mic" notification.
      
      ## Logging & Diagnostics
      
      `print` does not survive shipping. The unified logging system is the standard instrument for a Mac app: `os.Logger` is "an object for writing interpolated string messages to the unified logging system" (macOS 11+), and it is what `log stream` / `log show` and Console.app read.
      
      ```swift
      import os
      
      private let log = Logger(subsystem: "com.example.MyApp", category: "capture")
      
      log.debug("frame \(index, privacy: .public) queued")
      log.error("stream stopped: \(error.localizedDescription, privacy: .public)")
      ```
      
      Points that matter in practice:
      
      - **Interpolated values default to `.private`** and render as `<private>` when read back from another process. Mark non-sensitive diagnostic values `.public` explicitly or your shipped logs are useless. Do the opposite for anything user-derived.
      - **Levels have different persistence.** `.debug` is memory-only and discarded aggressively; `.info` persists only when collected; `.notice` (the default), `.error`, and `.fault` go to the on-disk store. Ship at `.notice` and above for anything you want to see in a user's sysdiagnose.
      - **Read logs for a menu-bar-only app** - which has no console output anywhere - with `log stream --predicate 'subsystem == "com.example.MyApp"' --level debug`.
      
      For performance work, `OSSignposter` brackets intervals that show up as regions in Instruments:
      
      ```swift
      let signposter = OSSignposter(subsystem: "com.example.MyApp", category: "render")
      let state = signposter.beginInterval("compose")
      defer { signposter.endInterval("compose", state) }
      ```
      
      Docs: <https://developer.apple.com/documentation/os/logger>
      
      ## Privacy usage descriptions
      
      The capture-heavy parts of this skill cover screen recording and microphone TCC in depth. One easily missed sibling:
      
      - **`NSLocalNetworkUsageDescription`** - "a message that tells people why the app is requesting access to the local network" (macOS 11+). Required for **any** Bonjour/mDNS discovery or local-subnet traffic. Without it the app is denied local network access, and the failure looks like a networking bug: discovery returns nothing, connections to `.local` names time out. Apps that stream to a local device, discover a companion app, or run a local server for a helper all need it, alongside `NSBonjourServices` listing the service types you browse.
      
      Docs: <https://developer.apple.com/documentation/bundleresources/information-property-list/nslocalnetworkusagedescription>
      
    • testing.md 20.9 KB
      # Testing macOS Apps
      
      ## Table of Contents
      - Swift Testing Framework
      - Test Suites & Organization
      - Expectations & Requirements
      - Parameterized Tests
      - Exit Tests
      - Attachments
      - Async Testing
      - Parallelism Pitfalls for Hardware / Bundle Tests
      - Traits
      - Swift Testing recent features (6.2 and 6.3)
      - UI Testing
      - XCTest Migration
      
      ## Swift Testing Framework
      
      Swift Testing (bundled with Swift 6.0+, Xcode 16+) replaces XCTest for new tests:
      
      ```swift
      import Testing
      
      @Test("user can create account")
      func createAccount() throws {
          let account = try Account(name: "Test", email: "test@example.com")
          #expect(account.name == "Test")
          #expect(account.isActive)
      }
      ```
      
      ### Key differences from XCTest
      
      | Feature | XCTest | Swift Testing |
      |---------|--------|---------------|
      | Test declaration | `func testX()` | `@Test func x()` |
      | Assertions | `XCTAssertEqual` | `#expect(a == b)` |
      | Required values | `XCTUnwrap` | `try #require(value)` |
      | Test suites | `class: XCTestCase` | `@Suite struct` |
      | Parallelism | Sequential | Parallel by default |
      | Parameterized | Manual loops | `@Test(arguments:)` |
      | Traits | None | Tags, conditions, time limits |
      
      ## Test Suites
      
      ```swift
      @Suite("Document Manager")
      struct DocumentManagerTests {
          // Shared setup
          let manager: DocumentManager
          let tempDir: URL
      
          init() throws {
              tempDir = FileManager.default.temporaryDirectory
                  .appendingPathComponent(UUID().uuidString)
              try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
              manager = DocumentManager(directory: tempDir)
          }
      
          // Cleanup (deinit not available for structs, use helper)
      
          @Test func createDocument() throws {
              let doc = try manager.create(name: "test.txt")
              #expect(doc.exists)
          }
      
          @Test func listDocuments() async throws {
              try manager.create(name: "a.txt")
              try manager.create(name: "b.txt")
              let docs = try await manager.listAll()
              #expect(docs.count == 2)
          }
      }
      ```
      
      ### Nested suites
      ```swift
      @Suite("API Client")
      struct APIClientTests {
          @Suite("Authentication")
          struct AuthTests {
              @Test func validToken() { /* ... */ }
              @Test func expiredToken() { /* ... */ }
          }
      
          @Suite("Requests")
          struct RequestTests {
              @Test func getRequest() { /* ... */ }
              @Test func postRequest() { /* ... */ }
          }
      }
      ```
      
      ## Expectations & Requirements
      
      ```swift
      // Basic expectation
      #expect(result == 42)
      #expect(name.isEmpty == false)
      #expect(items.count > 0)
      
      // String contains
      #expect(message.contains("success"))
      
      // Optional handling - #require unwraps or fails test
      let user = try #require(response.user)
      #expect(user.name == "Alice")
      
      // Throws
      #expect(throws: ValidationError.self) {
          try validate(invalidInput)
      }
      
      // Specific error
      #expect {
          try parse("")
      } throws: { error in
          guard let parseError = error as? ParseError else { return false }
          return parseError.code == .emptyInput
      }
      
      // No throw
      #expect(throws: Never.self) {
          try safeOperation()
      }
      ```
      
      ## Parameterized Tests
      
      Test multiple inputs without duplication:
      
      ```swift
      @Test("validates email", arguments: [
          ("user@example.com", true),
          ("invalid", false),
          ("@missing.com", false),
          ("user@.com", false),
          ("a@b.co", true),
      ])
      func validateEmail(email: String, isValid: Bool) {
          #expect(Email.isValid(email) == isValid)
      }
      
      // With zip
      @Test(arguments: zip(
          ["admin", "user", "guest"],
          [Permission.all, Permission.read, Permission.none]
      ))
      func rolePermissions(role: String, expected: Permission) throws {
          let user = try User(role: role)
          #expect(user.permissions == expected)
      }
      
      // From collection
      enum FileFormat: CaseIterable {
          case json, xml, csv
      }
      
      @Test("exports in all formats", arguments: FileFormat.allCases)
      func export(format: FileFormat) throws {
          let data = try exporter.export(items, as: format)
          #expect(!data.isEmpty)
      }
      ```
      
      ## Exit Tests (Swift 6.2)
      
      Verify code terminates under specific conditions:
      
      ```swift
      @Test func preconditionFailsForNegativeIndex() async {
          await #expect(processExitsWith: .failure) {
              let array = [1, 2, 3]
              _ = array[-1] // Should trigger precondition failure
          }
      }
      
      @Test func fatalErrorOnInvalidState() async {
          await #expect(processExitsWith: .failure) {
              StateMachine.transition(from: .completed, to: .idle)
          }
      }
      ```
      
      Exit tests run in a separate process - safe for testing fatal paths.
      
      ## Attachments (Swift 6.2)
      
      Include diagnostic data in test results:
      
      ```swift
      @Test func renderChart() throws {
          let chart = try ChartRenderer.render(data: sampleData)
      
          // Attach PNG bytes for debugging
          let imageData = try chart.pngData()
          Attachment.record(imageData, named: "chart.png")
      
          #expect(chart.width == 800)
          #expect(chart.height == 600)
      }
      
      @Test func apiResponse() async throws {
          let response = try await api.fetchUsers()
      
          // Attach raw JSON for diagnosis
          Attachment.record(response.rawData, named: "response.json")
      
          #expect(response.users.count > 0)
      }
      ```
      
      Attachments appear in Xcode test reports and can be written to disk.
      
      ## Async Testing
      
      ```swift
      @Test func fetchData() async throws {
          let service = DataService()
          let items = try await service.fetchAll()
          #expect(!items.isEmpty)
      }
      
      // With timeout trait
      @Test(.timeLimit(.minutes(1)))
      func longRunningOperation() async throws {
          let result = try await processor.processLargeFile(url)
          #expect(result.isComplete)
      }
      
      // Testing async sequences
      @Test func streamEvents() async throws {
          let stream = EventSource.events()
          var count = 0
          for await event in stream.prefix(5) {
              #expect(event.isValid)
              count += 1
          }
          #expect(count == 5)
      }
      ```
      
      ## Parallelism Pitfalls for Hardware / Bundle Tests
      
      Swift Testing runs tests **in parallel by default**. For tests that launch the same `.app` bundle, drive CoreAudio, or touch shared hardware state (microphone, Screen Recording TCC, a running process), this causes non-obvious races:
      - Two tests launch the same bundle, fight for the CATap, one ends up with silent buffers.
      - Two hardware tests both request microphone access, the second one's `AVCaptureDevice.requestAccess(...)` returns spuriously `false`.
      - Two UI-driving tests try to `open -a MyApp.app` concurrently and `NSWorkspace.runningApplications` reports inconsistent state.
      
      ### Serialize with `.serialized`
      
      Apply the `.serialized` trait on the suite (or individual tests) that share a resource:
      
      ```swift
      @Suite("Hardware Smoke", .serialized)
      struct HardwareSmokeTests {
          @Test func recordsFaceTime() async throws { /* launches bundle, records */ }
          @Test func recordsSystemAudio() async throws { /* launches bundle, records */ }
          @Test func handlesDeviceSwitch() async throws { /* launches bundle, switches device */ }
      }
      ```
      
      Tests outside the suite still run in parallel with suites that don't share the resource.
      
      ### Kill by bundle identifier, not by path
      
      Test harnesses that tear down a stale app instance between tests commonly use:
      
      ```swift
      // WRONG: only kills instances of THIS copy of the bundle.
      // A stale /Applications/MyApp.app running from a previous install stays alive
      // and keeps holding CoreAudio / SCStream resources.
      for app in NSWorkspace.shared.runningApplications where app.bundleURL == testBundleURL {
          app.terminate()
      }
      ```
      
      For LSUIElement / menu-bar apps especially, a stale `/Applications` copy from a previous `make install` is invisible (no Dock icon, no Cmd+Tab, no Force Quit listing) but contends for the CATap / microphone / Screen Recording TCC. Kill by bundle ID instead:
      
      ```swift
      for app in NSWorkspace.shared.runningApplications
          where app.bundleIdentifier == "com.example.MyApp" {
          app.terminate()
      }
      // Or via AppKit's async termination for a cleaner shutdown before yielding.
      ```
      
      ### Polling helpers must be tolerant of transient errors
      
      Bundle-based tests often poll a JSON / IPC state file the app writes. Under full parallel suite load the app can briefly miss its poll interval; if the helper rethrows the inner read error, the outer deadline doesn't get a chance to govern:
      
      ```swift
      // FRAGILE: any transient read failure aborts the wait, even though the outer
      // deadline hasn't been reached.
      func waitUntil<T>(_ timeout: Duration = .seconds(15),
                        _ condition: () async throws -> T?) async throws -> T {
          let deadline = Date().addingTimeInterval(timeout.seconds)
          while Date() < deadline {
              if let v = try await condition() { return v } // rethrows transient errors
              try await Task.sleep(for: .milliseconds(200))
          }
          throw WaitError.timedOut
      }
      
      // ROBUST: outer deadline governs; inner errors are treated as "not yet".
      func waitUntil<T>(_ timeout: Duration = .seconds(15),
                        _ condition: () async throws -> T?) async throws -> T {
          let deadline = Date().addingTimeInterval(timeout.seconds)
          while Date() < deadline {
              if let v = try? await condition() { return v }
              try await Task.sleep(for: .milliseconds(200))
          }
          throw WaitError.timedOut
      }
      ```
      
      ### Gate hardware tests behind an env var
      
      Tests that need live TCC permissions (screen recording, microphone) or specific hardware (AirPods, a running FaceTime call) shouldn't run on developer machines by default:
      
      ```swift
      @Test(.enabled(if: ProcessInfo.processInfo.environment["RUN_HARDWARE_SMOKE"] == "1"))
      func recordsAgainstLiveSystem() async throws { /* ... */ }
      ```
      
      A `Makefile` or `justfile` target that sets the env var is the canonical wrapper (`make smoke-test`). `swift test` without the var shows these as "skipped", which is the desired default.
      
      ### Pipe buffering and `make` swallow test output
      
      Running `swift test` through `make` / `rtk` / any command that pipes through a second process can truncate Swift Testing output at ~120 lines and lose the exit code. When you need the full output (e.g. to see which test crashed and where), invoke `swift test --parallel` directly.
      
      ## Traits
      
      ```swift
      // Tags for filtering
      extension Tag {
          @Tag static var networking: Self
          @Tag static var database: Self
          @Tag static var slow: Self
      }
      
      @Test(.tags(.networking))
      func apiCall() async throws { /* ... */ }
      
      // Conditional execution
      @Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil))
      func ciOnlyTest() { /* ... */ }
      
      // Disabled with reason
      @Test(.disabled("Waiting for server fix"))
      func brokenTest() { /* ... */ }
      
      // Bug reference
      @Test(.bug("https://github.com/org/repo/issues/123"))
      func regressionTest() { /* ... */ }
      
      // Time limit - minutes only. `.seconds(_:)` is @available(*, unavailable):
      // "Time limit must be specified in minutes"
      @Test(.timeLimit(.minutes(1)))
      func quickTest() async throws { /* ... */ }
      
      // Serial execution (see Parallelism Pitfalls above)
      @Suite(.serialized)
      struct HardwareTests { /* ... */ }
      ```
      
      ## Swift Testing recent features (6.2 and 6.3)
      
      A handful of workflow wins scattered across Swift Testing 6.2 (Xcode 26) and 6.3 (Xcode 26.4). Version gate noted per feature — they did not all ship at the same time.
      
      ### Warning-severity issues (non-failing) — Swift 6.3
      
      ```swift
      @Test func parsesPayload() throws {
          let parsed = try parser.parse(payload)
          #expect(parsed.id != nil)
          if parsed.deprecatedField != nil {
              // Does NOT fail the test, but surfaces in reports.
              Issue.record("payload still uses deprecatedField", severity: .warning)
          }
      }
      ```
      
      Use for soft expectations (deprecations, perf regressions short of a hard bar, drift warnings). `Issue.Severity` is annotated `@Available(Swift, introduced: 6.3)` / `@Available(Xcode, introduced: 26.4)` in [Issue.swift](https://github.com/swiftlang/swift-testing/blob/main/Sources/Testing/Issues/Issue.swift) - the ST-0013 design landed earlier, but the shipping gate is 6.3, not 6.2.
      
      ### Exit tests: `processExitsWith:` — Swift 6.2
      
      The `#expect(exitsWith:)` spelling was renamed to `#expect(processExitsWith:)` in swift-testing 6.2:
      
      ```swift
      @Test func preconditionFailsForNegativeIndex() async {
          await #expect(processExitsWith: .failure) {
              let array = [1, 2, 3]
              _ = array[-1]
          }
      }
      ```
      
      ### `Attachment.record(...)` — Swift 6.2 (rename), Swift 6.3 (AppKit/CoreImage/UIKit images)
      
      The static `Attachment.record(_:named:...)` replaces the old instance `.attach()` method (rename shipped in [PR #1032](https://github.com/swiftlang/swift-testing/pull/1032), Swift 6.2). **All** image attachment support - `CGImage` included, not just the `NSImage`/`CIImage`/`UIImage` overlays - is gated at Swift 6.3 / Xcode 26.4: every initializer in [Attachment+AttachableAsImage.swift](https://github.com/swiftlang/swift-testing/blob/main/Sources/Testing/Attachments/Images/Attachment%2BAttachableAsImage.swift) carries `@Available(Swift, introduced: 6.3)` (ST-0014).
      
      ```swift
      // Swift 6.2+ — bytes / CGImage
      Attachment.record(imageData, named: "chart.png")
      Attachment.record(cgImage, named: "chart", as: .png)
      
      // Swift 6.3+ — NSImage / UIImage / CIImage overlays
      @Test func rendersChart() throws {
          let image: NSImage = try ChartRenderer.render(data)
          Attachment.record(image, named: "chart", as: .png)
          #expect(image.size == CGSize(width: 800, height: 600))
      }
      ```
      
      Xcode's test report displays attached images inline; useful for golden-image tests and visual regressions.
      
      ### Cooperative mid-test cancellation — Swift 6.3
      
      `Test.cancel(_:)` has signature `throws -> Never`: it always throws, so the call never returns normally and callers must use `try`. The comment argument is positional (no `reason:` label).
      
      ```swift
      @Test func longRunningCheck() async throws {
          for _ in 0..<1_000_000 {
              if shouldStop() {
                  try Test.cancel("condition met early")
              }
              try await doOneStep()
          }
      }
      ```
      
      Cleaner than `throw XCTSkip("...")` — the test reports cancelled, not failed or skipped. (ST-0016, [swift-testing 6.3](https://github.com/swiftlang/swift-testing/releases/tag/swift-6.3-RELEASE) / Xcode 26.4.) The 6.2 release does not correctly handle task cancellation in all conditions, per the proposal note — require 6.3.
      
      ### `SourceLocation.filePath` — Swift 6.3
      
      Non-underscored file-path access on `SourceLocation` for custom reporters:
      
      ```swift
      let loc = #_sourceLocation
      print(loc.filePath)   // String
      ```
      
      (ST-0020, PR #1538.)
      
      ### ST-0021 XCTest / Swift Testing interop — Implemented in Swift 6.4
      
      Proposal status is now **Implemented (Swift 6.4)**, and `SWIFT_TESTING_XCTEST_INTEROP_MODE` is a documented environment variable accepting `none`, `limited`, `complete`, or `strict` — it controls how XCTest assertion failures recorded during a Swift Testing test are handled.
      
      Practical gate is unchanged for this skill's pinned toolchain: on Swift 6.3 only the fallback-event-handler plumbing is present (PRs #1369, #1503, #1543), so don't rely on interop-mode semantics until you are on a 6.4 toolchain. Sources: [ST-0021](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0021-targeted-interoperability-swift-testing-and-xctest.md), [EnvironmentVariables.md](https://github.com/swiftlang/swift-testing/blob/main/Documentation/EnvironmentVariables.md)
      
      ### In the pipeline (not yet in any toolchain)
      
      Nothing below is usable on 6.3.3; track them if you maintain a test suite that will move to 6.4+.
      
      - **ST-0026 task-local test trait** - **Accepted with revisions** on 2026-08-14. A trait that sets task-local values for the duration of a test, which is today's workaround-by-hand for injecting per-test configuration through `@TaskLocal`. ([proposal](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0026-task-local-test-trait.md))
      - **ST-0027 source-location macro** and **ST-0028 revised attachment `Encodable` interfaces** - both entered active review in August 2026; ST-0028 in particular would reshape the attachment API documented above.
      - The event stream gained an `ABI.Context` API producing human-readable output for CI tooling and dashboards (merged, unreleased).
      
      The newest tagged swift-testing release is still `swift-6.3.2-RELEASE` (2026-05-13).
      
      ## UI Testing (XCTest-based)
      
      UI testing still uses XCTest (Swift Testing doesn't support UI tests yet):
      
      ```swift
      import XCTest
      
      final class ProjectUITests: XCTestCase {
          let app = XCUIApplication()
      
          override func setUpWithError() throws {
              continueAfterFailure = false
              app.launch()
          }
      
          func testCreateProject() throws {
              app.buttons["New Project"].click()
      
              let nameField = app.textFields["projectName"]
              nameField.click()
              nameField.typeText("My Project")
      
              app.buttons["Create"].click()
      
              XCTAssertTrue(app.staticTexts["My Project"].exists)
          }
      }
      ```
      
      ## XCTest Migration
      
      Migrate incrementally - both frameworks coexist in the same target:
      
      ```swift
      // Old (XCTest)
      class OldTests: XCTestCase {
          func testAdd() {
              XCTAssertEqual(add(2, 3), 5)
          }
      }
      
      // New (Swift Testing)
      @Test func add() {
          #expect(add(2, 3) == 5)
      }
      ```
      
      Migration checklist:
      1. `XCTestCase` class -> `@Suite` struct
      2. `func testX()` -> `@Test func x()`
      3. `XCTAssertEqual(a, b)` -> `#expect(a == b)`
      4. `XCTAssertTrue(x)` -> `#expect(x)`
      5. `XCTAssertNil(x)` -> `#expect(x == nil)`
      6. `XCTAssertThrowsError` -> `#expect(throws:)`
      7. `XCTUnwrap(x)` -> `try #require(x)`
      8. `setUp/tearDown` -> `init/deinit` or test-local setup
      9. `measure { }` -> Use Instruments (no direct equivalent yet)
      
      ## Confirmations: testing callbacks and delegates
      
      `#expect` cannot express "this callback fired exactly N times". `confirmation` can, and it is the right tool for the delegate-driven APIs common in macOS apps - `SCStreamOutput`, `AVAudioEngine` taps, `NotificationCenter` observers:
      
      ```swift
      @Test func streamDeliversBuffers() async throws {
          await confirmation("audio buffers delivered", expectedCount: 3) { delivered in
              let output = StubStreamOutput { _ in delivered() }
              try await runCapture(feeding: output, frames: 3)
          }
      }
      ```
      
      The suite fails if the count does not match when the closure returns. Ranges work for "at least once, unbounded" cases:
      
      ```swift
      await confirmation(expectedCount: 1...) { fired in
          monitor.onChange = { _ in fired() }
          try await monitor.pump()
      }
      ```
      
      Use `expectedCount: 0` to assert a callback never fires - stronger than asserting on final state, because it catches a spurious extra invocation.
      
      ## `withKnownIssue` instead of disabling
      
      A test that documents a real bug should keep running. `.disabled()` hides it; `withKnownIssue` records the failure without failing the suite, and **fails loudly if the issue stops reproducing** - so the test tells you when the bug is fixed:
      
      ```swift
      @Test func handlesMalformedHeader() throws {
          withKnownIssue("parser drops the extension bit - FB12345678") {
              let parsed = try Parser.parse(malformedFixture)
              #expect(parsed.extensionBit == true)
          }
      }
      
      // Intermittent failures: don't fail when it happens to pass
      withKnownIssue(isIntermittent: true) { try flakyPath() }
      ```
      
      ## `swift test` command-line surface
      
      Beyond `--parallel`:
      
      ```bash
      swift test --list-tests                       # enumerate without running
      swift test --filter ProjectTests              # regex over suite/test names
      swift test --disable-xctest                   # run only Swift Testing
      swift test --enable-xctest                    # run only XCTest
      swift test --attachments-path ./test-output   # write Attachment.record(...) payloads here
      ```
      
      `--attachments-path` is what makes attachments useful in CI - without it, recorded attachments have nowhere to land.
      
      ### Trap: `swift test` cannot run tests under Command Line Tools alone
      
      With only the Command Line Tools installed (no full Xcode), tests **compile and link but never execute** - there is no `xctest` runner binary. Both XCTest and Swift Testing suites report nothing, which reads as "all tests passed" in a CI log. A placeholder test can sit "green" for weeks without ever having run.
      
      ```bash
      xcode-select -p                              # /Library/Developer/CommandLineTools == the problem
      sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
      ```
      
      Standardize on `swift test --disable-xctest` in your Justfile/Makefile so Swift Testing runs without needing the XCTest runner at all, and assert on a known-nonzero test count in CI rather than trusting a clean exit code.
      
      ## Trap: menu-bar-only apps are invisible to UI automation
      
      An `LSUIElement` app does not appear in the accessibility registry the way a regular app does, so harnesses that enumerate running or installed applications through Launch Services or the accessibility APIs report it as absent regardless of its actual state. XCUITest-style automation of the menu bar UI is a dead end.
      
      Drive the real bundle through a narrow, explicitly-flagged test surface instead, and assert on the artifacts it produces:
      
      ```swift
      // In the app, gated so it cannot ship enabled by accident
      if CommandLine.arguments.contains("--ui-test-mode") {
          TestControlSurface.install()   // exposes start/stop hooks only
      }
      ```
      
      Launch the `.app` with that argument, exercise the hooks, and verify the output files. Keep the surface minimal - the goal is testability, not restructuring the app around the test harness.
      
  • CHANGELOG.md 12 KB
    # Changelog
    
    All notable changes to this skill will be documented in this file.
    
    The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
    and this skill adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
    
    ## [Unreleased]
    
    ## [0.8.3] - 2026-09-09
    
    ### Changed
    - Description condensed to fit the repo's 250-character limit.
    
    ## [0.8.2] - 2026-08-21
    
    ### Changed
    
    - Declared ClawHub browse categories (`development`) and topics in `metadata`, so the release pipeline publishes them instead of leaving the skill in the `other` category.
    
    ### Removed
    
    - `skill-card.md`. The ClawHub CLI strips a root `skill-card.md` from every publish and the registry generates its own card, so the authored file never reached ClawHub.
    
    ## [0.8.1] - 2026-08-20
    
    ### Fixed
    
    - ScreenCaptureKit audio-only capture recommended `CMTime(value: 1, timescale: CMTimeScale.max)` for `minimumFrameInterval`. That reads like "infinite interval" but is ~0.5 ns - the smallest positive interval - so it requests the display's native refresh rate and burns ~15-20% of a core on WindowServer recomposites for the whole recording. Replaced with `CMTime(value: 1, timescale: 1)` (1 fps) in SKILL.md and screen-capture-audio.md, with an explicit note on the trap.
    - screen-capture-audio.md called the `stream output NOT found. Dropping frame` log "cosmetic" and left the video pipeline unaddressed. The fix is to attach a `.screen` output that discards its buffers on its own queue (never the audio queue) - documented in the snippet.
    
    ## [0.8.0] - 2026-08-20
    
    ### Fixed
    
    - SKILL.md Relationships snippet and relationships-predicates.md Cascade example did not compile: `@Model` requires an explicit initializer, which neither had. Verified against the macOS 26.5 SDK.
    - models-schema.md model-inheritance snippet failed twice - `@Model` subclasses require explicit platform availability and an initializer - and the section carried no version gate at all. Subclassing is macOS 26.0+.
    - `.presentedWindowStyle(.automatic) // .fullScreen` in app-lifecycle.md: `WindowStyle` has no `.fullScreen` member. The complete SDK set is `.automatic`, `.titleBar`, `.hiddenTitleBar`, `.plain`.
    - `windowResizeAnchor` was documented as a Scene API taking an edge; it is a `View` modifier taking `UnitPoint?`.
    - `restorationBehavior` was labelled "(macOS 26)"; it is macOS 15.0.
    - `ContextOptions` is not in the macOS 26.5 SDK - it is macOS 27 beta - and was described as a trimming/retention policy; it configures prompt content. Section rewritten around `tokenCount(for:)`/`contextSize`, which do exist.
    - Three stale documentation links: the Foundation Models custom-adapter article was removed by Apple, and the privacy-manifest and `SCStreamErrorCode` URLs now redirect.
    - SE-0526 `withDeadline` status was "in review April 2026"; it was accepted with modifications on 2026-07-29.
    - `anyAppleOS` was listed as a Swift 6.4 feature; it compiles on 6.3.3 behind `-enable-experimental-feature AnyAppleOSAvailability`.
    - The App Store SDK-minimum requirement is now past tense ("Since April 28, 2026").
    
    ### Changed
    
    - Current shipping macOS is 26.6.2 (was 26.6); Xcode 27 pinned to beta 5 (27A5237l) and macOS 27 to beta 6 (26A5416b), with Swift 6.4 noted as shipping inside the Xcode 27 beta.
    - macOS 27 beta 6 deprecates both `FileDocument` and `ReferenceFileDocument` in favor of a combined `Document` protocol; the skill had quoted the superseded beta-4 note saying `ReferenceFileDocument` remained available.
    - Foundation Models `GenerationError` is deprecated in macOS 27 with a hard submission deadline, and `exceededContextWindowSize` is renamed `contextSizeExceeded`.
    
    ### Added
    
    - AppKit Liquid Glass interop: `NSGlassEffectView`, `NSGlassEffectContainerView`, `NSBackgroundExtensionView`, and `NSView.prefersCompactControlSizeMetrics` as the escape hatch when Liquid Glass inflates a dense AppKit layout.
    - SwiftUI/AppKit bridging: `NSHostingSceneRepresentation` for hosting whole scenes, `NSHostingSizingOptions`, `NSHostingSceneBridgingOptions`, and `NSGestureRecognizerRepresentable`.
    - Pasteboard privacy (macOS 15.4): `NSPasteboard.accessBehavior` and the `detect` methods that inspect the pasteboard without triggering the new read alert - the existing clipboard recipe was exactly the triggering pattern.
    - App Intents beyond the single snippet: `AppEntity`/`EntityQuery`/`AppEnum`, `IndexedEntity` with `@ComputedProperty(indexingKey:)`, `SnippetIntent` and its `isDiscoverable` trap, `supportedModes` replacing the deprecated `ForegroundContinuableIntent`, and the macOS-only `NSTableViewAppIntentsDataSource`.
    - WidgetKit on macOS and Control Center controls (`ControlWidget`, `ControlWidgetButton`, `ControlWidgetToggle`, `ControlConfigurationIntent`), which reached macOS in 26.0.
    - Logging and diagnostics: `os.Logger`, the `.private`-by-default interpolation trap, log-level persistence, reading logs from a menu-bar-only app, and `OSSignposter`.
    - `XPCSession`/`XPCListener` - the missing half of the existing `SMAppService` login-item coverage.
    - `AXUIElement` accessibility API with its TCC, signature, and sandbox constraints.
    - `NSLocalNetworkUsageDescription`, whose absence presents as a networking bug rather than a permission failure.
    - `SpeechAnalyzer`/`SpeechTranscriber` as the on-device transcription stage of the existing capture pipeline.
    - ScreenCaptureKit gaps: `streamDidBecomeActive`/`streamDidBecomeInactive` (macOS 15.2), `SCStreamConfiguration.Preset`, `SCScreenshotOutput`, and the macOS 27 beta `SCStream.isCapturing`, `SCClipBufferingOutput`, `SCRecordingEditor`.
    - xcodebuild-on-.xcodeproj field notes: builds that report `BUILD SUCCEEDED` while emitting an ad-hoc-signed bundle, `codesign -dv` as the post-build gate, `CODE_SIGNING_REQUIRED=YES`, bracketed build settings being unpassable on the command line, certificate-class prefix matching, symlinked-path build-database corruption, and TCC behavior across signature changes and bundle moves.
    - SPM consumer-side gates: `-skipPackagePluginValidation`/`-skipMacroValidation` for non-interactive builds, and `-downloadComponent MetalToolchain` after Xcode 26 unbundled the Metal toolchain.
    - SE-0506 Advanced Observation Tracking (`withObservationTracking(options:)`, `withContinuousObservationTracking`), Implemented in Swift 6.4.
    - Swift Testing pipeline: ST-0026 task-local test trait (accepted with revisions), ST-0027 and ST-0028 in active review, and the event-stream `ABI.Context` API.
    - macOS 27 beta surface: SwiftData `ResultsObserver`/`HistoryObserver`/`@Query(sectionBy:)`/`Schema.Attribute.Option.codable`, Foundation Models `PrivateCloudComputeLanguageModel` with its entitlement, the `#Playground` macro, and Core AI's public API names.
    
    Verified against: swift@6.3.3, xcode@26.6, macos@26.6.2
    
    ## [0.7.1] - 2026-08-07
    
    ### Changed
    
    - Trimmed the frontmatter description to what-plus-when; dropped the trailing 15-item trigger-keyword list.
    
    ## [0.7.0] - 2026-07-28
    
    ### Fixed
    
    - Foundation Models quick start assigned `Response<Content>` to the bare content type and did not compile; now reads `.content`. Verified with the compiler against the macOS 26.5 SDK.
    - `.timeLimit(.seconds(30))` in testing.md is `@available(*, unavailable)` ("Time limit must be specified in minutes"); switched to `.minutes(1)`.
    - `FetchDescriptor.fetchBatchSize` does not exist and the snippet mutated a `let`; replaced with `enumerate(_:batchSize:)`.
    - Quick Start `@Model final class Task` shadowed `Swift.Task` in a file that then uses `Task {}` and `TaskGroup`; renamed to `ProjectTask`, matching relationships-predicates.md.
    
    ### Changed
    
    - Current shipping macOS is 26.6 (was 26.5); noted that Xcode 26.6 still bundles the macOS 26.5 SDK.
    - `weak let` corrected from a Swift 6.4 beta feature to SE-0481, shipped in Swift 6.3; 6.4 feature list rewritten against swift-evolution data.
    - Xcode 27 and macOS 27 pinned to beta 4 status.
    - Swift Testing version gates corrected: issue severity and all image attachments (including `CGImage`) are Swift 6.3 / Xcode 26.4, not 6.2. ST-0021 is now Implemented (Swift 6.4).
    - SPM guidance modernized: `.strictMemorySafety()` over the experimental spelling, Swift Build as a 6.3 preview and 6.4 default, swift-syntax 603.0.2, swiftly 6.3.3.
    - Dropped `altool --upload-app` (notary service stopped accepting it in 2023) in favor of notarytool.
    - Gatekeeper Control-click bypass replaced with the Open Anyway flow - the Control-click override was removed in macOS Sequoia and is still absent in Tahoe.
    - Corrected the `Schema.Attribute.Option` list; there is no `.encrypt` option.
    
    ### Added
    
    - Xcode 27 build-breakers: ld64 and `-ld_classic` removal, unique Clang module-name requirement, `ARCHS_STANDARD` dropping x86_64 at deployment target 27.0+, the SE-0508 source break, and the `DocumentReader`/`DocumentWriter` `@concurrent` isolation change alongside the new `ReadableDocument`/`WritableDocument` protocols.
    - SwiftData: `#Unique` and `#Index` macros, model inheritance with `includeSubclasses`, `.ephemeral`, history tracking (`HistoryDescriptor`/`HistoryToken`/tombstones), custom `DataStore`, fetch shaping (`propertiesToFetch`, `relationshipKeyPathsForPrefetching`), relationship cardinality.
    - Swift Testing: `confirmation` for callback- and delegate-driven code, `withKnownIssue`, the `swift test` CLI surface, and the trap where Command Line Tools alone silently run no tests.
    - Concurrency: the `Synchronization` module (`Mutex`, `Atomic`) for realtime and C-callback contexts, the `nonisolated(nonsending)` spelling, isolated conformances (SE-0470), `swift package migrate --to-feature`, and the individual upcoming-feature flags.
    - ScreenCaptureKit: `synchronizationClock`, `SCScreenshotConfiguration`, Presenter Overlay delegate callbacks, and the omitted `SCStreamConfiguration` properties.
    - SwiftUI: `@Entry`, native `WebView`/`WebPage`, rich-text `TextEditor`, and the `UtilityWindow`/`SettingsLink`/`defaultLaunchBehavior` scene surface.
    - Distribution: `get-task-allow` as the top notarization blocker, plug-in entitlement inheritance, the Enhanced Security capability, `-exportNotarizedApp`, installer packaging, and Homebrew cask as a second channel.
    - SPM: package traits, `--build-system swiftbuild`, `.binaryTarget`/`.systemLibrary`, plugin permissions, and the unwritable-`$HOME`-cache build failure.
    - Foundation Models: `@PromptBuilder`/`@InstructionsBuilder`, `DynamicGenerationSchema`, `ContextOptions`.
    - Field-hardening notes: actor reentrancy across awaits, the interleaved-vs-planar downmix trap, stale TCC rows after a signing-identity change, mic taps dying on output-device switches, third-party Voice Processing reshaping your input, mic-activity latching, MenuBarExtra label-update crashes, and crash visibility for menu-bar-only apps.
    
    Verified against: swift@6.3.3, xcode@26.6, macos@26.6
    
    ## [0.6.3] - 2026-07-22
    
    ### Added
    
    - skill-card.md release record following NVIDIA's skill-card format
    
    ### Changed
    
    - metadata.openclaw audited against the official ClawHub spec
    
    ## [0.6.2] - 2026-07-10
    
    ### Changed
    - CHANGELOG preamble pinned to Keep a Changelog 2.0.0 (format unchanged; KaC 2.0.0 keeps existing changelogs valid).
    
    ## [0.6.1] - 2026-07-01
    ### Changed
    - Moved the "Fall 2026 Releases (WWDC 2026)" section out of SKILL.md into `references/fall-2026-releases.md` (progressive disclosure), keeping SKILL.md under the 500-line policy limit. Content unchanged, expanded slightly with sources.
    
    ## [0.6.0] - 2026-07-01
    ### Changed
    - Current toolchain updated to Swift 6.3.3 / Xcode 26.6 (macOS 26.5 Tahoe SDK); dropped the stale "Swift 6.2.4, Feb 2026 latest" framing.
    - Liquid Glass section notes the `UIDesignRequiresCompatibility` opt-out is removed for apps built with Xcode 27.
    
    ### Added
    - Forward-looking "Fall 2026 Releases (WWDC 2026, beta)" section: macOS 27 Golden Gate, Xcode 27, Swift 6.4, Foundation Models next-gen (image input, server models, Dynamic Profiles, pluggable models), Core AI, Spatial Preview.
    - CoreAudio CFString Create-Rule trap (`takeRetainedValue`) in core-audio-tap.md.
    - `SIGKILL (Code Signature Invalid)` dev-loop gotcha in distribution.md.
    - CHANGELOG and upstream tracking established.
    
    Verified against: swift@6.3.3, xcode@26.6
    
  • LICENSE.txt 8.9 KB
    Apache License
    Version 2.0, January 2004
    https://www.apache.org/licenses/
    
    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    
    1. Definitions.
    
    "License" shall mean the terms and conditions for use, reproduction, and
    distribution as defined by Sections 1 through 9 of this document.
    
    "Licensor" shall mean the copyright owner or entity authorized by the
    copyright owner that is granting the License.
    
    "Legal Entity" shall mean the union of the acting entity and all other
    entities that control, are controlled by, or are under common control with
    that entity. For the purposes of this definition, "control" means (i) the
    power, direct or indirect, to cause the direction or management of such
    entity, whether by contract or otherwise, or (ii) ownership of fifty percent
    (50%) or more of the outstanding shares, or (iii) beneficial ownership of
    such entity.
    
    "You" (or "Your") shall mean an individual or Legal Entity exercising
    permissions granted by this License.
    
    "Source" form shall mean the preferred form for making modifications,
    including but not limited to software source code, documentation source, and
    configuration files.
    
    "Object" form shall mean any form resulting from mechanical transformation or
    translation of a Source form, including but not limited to compiled object
    code, generated documentation, and conversions to other media types.
    
    "Work" shall mean the work of authorship, whether in Source or Object form,
    made available under the License, as indicated by a copyright notice that is
    included in or attached to the work (an example is provided in the Appendix
    below).
    
    "Derivative Works" shall mean any work, whether in Source or Object form,
    that is based on (or derived from) the Work and for which the editorial
    revisions, annotations, elaborations, or other modifications represent, as a
    whole, an original work of authorship. For the purposes of this License,
    Derivative Works shall not include works that remain separable from, or
    merely link (or bind by name) to the interfaces of, the Work and Derivative
    Works thereof.
    
    "Contribution" shall mean any work of authorship, including the original
    version of the Work and any modifications or additions to that Work or
    Derivative Works thereof, that is intentionally submitted to Licensor for
    inclusion in the Work by the copyright owner or by an individual or Legal
    Entity authorized to submit on behalf of the copyright owner. For the
    purposes of this definition, "submitted" means any form of electronic, verbal,
    or written communication sent to the Licensor or its representatives,
    including but not limited to communication on electronic mailing lists, source
    code control systems, and issue tracking systems that are managed by, or on
    behalf of, the Licensor for the purpose of discussing and improving the Work,
    but excluding communication that is conspicuously marked or otherwise
    designated in writing by the copyright owner as "Not a Contribution."
    
    "Contributor" shall mean Licensor and any individual or Legal Entity on
    behalf of whom a Contribution has been received by Licensor and subsequently
    incorporated within the Work.
    
    2. Grant of Copyright License. Subject to the terms and conditions of this
    License, each Contributor hereby grants to You a perpetual, worldwide,
    non-exclusive, no-charge, royalty-free, irrevocable copyright license to
    reproduce, prepare Derivative Works of, publicly display, publicly perform,
    sublicense, and distribute the Work and such Derivative Works in Source or
    Object form.
    
    3. Grant of Patent License. Subject to the terms and conditions of this
    License, each Contributor hereby grants to You a perpetual, worldwide,
    non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
    section) patent license to make, have made, use, offer to sell, sell, import,
    and otherwise transfer the Work, where such license applies only to those
    patent claims licensable by such Contributor that are necessarily infringed by
    their Contribution(s) alone or by combination of their Contribution(s) with
    the Work to which such Contribution(s) was submitted. If You institute patent
    litigation against any entity (including a cross-claim or counterclaim in a
    lawsuit) alleging that the Work or a Contribution incorporated within the Work
    constitutes direct or contributory patent infringement, then any patent
    licenses granted to You under this License for that Work shall terminate as of
    the date such litigation is filed.
    
    4. Redistribution. You may reproduce and distribute copies of the Work or
    Derivative Works thereof in any medium, with or without modifications, and in
    Source or Object form, provided that You meet the following conditions:
    
    (a) You must give any other recipients of the Work or Derivative Works a copy
    of this License; and
    
    (b) You must cause any modified files to carry prominent notices stating that
    You changed the files; and
    
    (c) You must retain, in the Source form of any Derivative Works that You
    distribute, all copyright, patent, trademark, and attribution notices from
    the Source form of the Work, excluding those notices that do not pertain to
    any part of the Derivative Works; and
    
    (d) If the Work includes a "NOTICE" text file as part of its distribution,
    then any Derivative Works that You distribute must include a readable copy of
    the attribution notices contained within such NOTICE file, excluding those
    notices that do not pertain to any part of the Derivative Works, in at least
    one of the following places: within a NOTICE text file distributed as part of
    the Derivative Works; within the Source form or documentation, if provided
    along with the Derivative Works; or, within a display generated by the
    Derivative Works, if and wherever such third-party notices normally appear.
    The contents of the NOTICE file are for informational purposes only and do not
    modify the License. You may add Your own attribution notices within Derivative
    Works that You distribute, alongside or as an addendum to the NOTICE text from
    the Work, provided that such additional attribution notices cannot be
    construed as modifying the License.
    
    You may add Your own copyright statement to Your modifications and may provide
    additional or different license terms and conditions for use, reproduction, or
    distribution of Your modifications, or for any such Derivative Works as a
    whole, provided Your use, reproduction, and distribution of the Work otherwise
    complies with the conditions stated in this License.
    
    5. Submission of Contributions. Unless You explicitly state otherwise, any
    Contribution intentionally submitted for inclusion in the Work by You to the
    Licensor shall be under the terms and conditions of this License, without any
    additional terms or conditions. Notwithstanding the above, nothing herein
    shall supersede or modify the terms of any separate license agreement you may
    have executed with Licensor regarding such Contributions.
    
    6. Trademarks. This License does not grant permission to use the trade names,
    trademarks, service marks, or product names of the Licensor, except as
    required for reasonable and customary use in describing the origin of the Work
    and reproducing the content of the NOTICE file.
    
    7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
    writing, Licensor provides the Work (and each Contributor provides its
    Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied, including, without limitation, any warranties
    or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    PARTICULAR PURPOSE. You are solely responsible for determining the
    appropriateness of using or redistributing the Work and assume any risks
    associated with Your exercise of permissions under this License.
    
    8. Limitation of Liability. In no event and under no legal theory, whether in
    tort (including negligence), contract, or otherwise, unless required by
    applicable law (such as deliberate and grossly negligent acts) or agreed to in
    writing, shall any Contributor be liable to You for damages, including any
    direct, indirect, special, incidental, or consequential damages of any
    character arising as a result of this License or out of the use or inability to
    use the Work (including but not limited to damages for loss of goodwill, work
    stoppage, computer failure or malfunction, or any and all other commercial
    damages or losses), even if such Contributor has been advised of the
    possibility of such damages.
    
    9. Accepting Warranty or Additional Liability. While redistributing the Work
    or Derivative Works thereof, You may choose to offer, and charge a fee for,
    acceptance of support, warranty, indemnity, or other liability obligations
    and/or rights consistent with this License. However, in accepting such
    obligations, You may act only on Your own behalf and on Your sole
    responsibility, not on behalf of any other Contributor, and only if You agree
    to indemnify, defend, and hold each Contributor harmless for any liability
    incurred by, or claims asserted against, such Contributor by reason of your
    accepting any such warranty or additional liability.
    
    END OF TERMS AND CONDITIONS
    
  • SKILL.md 17.3 KB
    ---
    name: swift-macos
    description: macOS apps with Swift 6.3, SwiftUI, SwiftData, Swift Concurrency, Foundation Models, Swift Testing, and ScreenCaptureKit. Use when building native Mac apps - windows, menus, SwiftData, on-device AI, capture, AppKit bridges, notarization.
    metadata:
      version: "0.8.3"
      categories: "development"
      topics: "swift, swiftui, macos, swiftdata, screencapturekit"
      upstream: "swift@6.3.3, xcode@26.6, macos@26.6.2"
      openclaw:
        homepage: https://github.com/tenequm/skills/tree/main/skills/swift-macos
        emoji: "🍎"
        os:
          - macos
    ---
    
    # macOS App Development - Swift 6.3
    
    Build native macOS apps with Swift 6.3 (latest: 6.3.3, bundled in Xcode 26.6, Jun 2026), SwiftUI, SwiftData, and macOS 26 Tahoe (26.6.2 current). Target macOS 14+ for SwiftData/@Observable, macOS 15+ for latest SwiftUI, macOS 26 for Liquid Glass and Foundation Models. Note Xcode 26.6 still bundles the macOS **26.5** SDK, so 26.6-only API is not yet buildable. For the WWDC 2026 beta stack (macOS 27, Xcode 27, Swift 6.4, shipping fall 2026), see `references/fall-2026-releases.md`.
    
    ## Quick Start
    
    ```swift
    import SwiftUI
    import SwiftData
    
    @Model
    final class Project {
        var name: String
        var createdAt: Date
        // Named ProjectTask, not Task - `Task` would shadow `Swift.Task`
        @Relationship(deleteRule: .cascade) var tasks: [ProjectTask] = []
    
        init(name: String) {
            self.name = name
            self.createdAt = .now
        }
    }
    
    @Model
    final class ProjectTask {
        var title: String
        var isComplete: Bool
        var project: Project?
    
        init(title: String) {
            self.title = title
            self.isComplete = false
        }
    }
    
    @main
    struct MyApp: App {
        var body: some Scene {
            WindowGroup("Projects") {
                ContentView()
            }
            .modelContainer(for: [Project.self, ProjectTask.self])
            .defaultSize(width: 900, height: 600)
    
            #if os(macOS)
            Settings { SettingsView() }
    
            MenuBarExtra("Status", systemImage: "circle.fill") {
                MenuBarView()
            }
            .menuBarExtraStyle(.window)
            #endif
        }
    }
    
    struct ContentView: View {
        @Query(sort: \Project.createdAt, order: .reverse)
        private var projects: [Project]
    
        @Environment(\.modelContext) private var context
        @State private var selected: Project?
    
        var body: some View {
            NavigationSplitView {
                List(projects, selection: $selected) { project in
                    NavigationLink(value: project) {
                        Text(project.name)
                    }
                }
                .navigationSplitViewColumnWidth(min: 200, ideal: 250)
            } detail: {
                if let selected {
                    DetailView(project: selected)
                } else {
                    ContentUnavailableView("Select a Project",
                        systemImage: "sidebar.left")
                }
            }
        }
    }
    ```
    
    ## Scenes & Windows
    
    | Scene | Purpose |
    |-------|---------|
    | `WindowGroup` | Resizable windows (multiple instances) |
    | `Window` | Single-instance utility window |
    | `Settings` | Preferences (Cmd+,) |
    | `MenuBarExtra` | Menu bar with `.menu` or `.window` style |
    | `DocumentGroup` | Document-based apps |
    
    Open windows: `@Environment(\.openWindow) var openWindow; openWindow(id: "about")`
    
    For complete scene lifecycle, see `references/app-lifecycle.md`.
    
    ## Menus & Commands
    
    ```swift
    .commands {
        CommandGroup(replacing: .newItem) {
            Button("New Project") { /* ... */ }
                .keyboardShortcut("n", modifiers: .command)
        }
        CommandMenu("Tools") {
            Button("Run Analysis") { /* ... */ }
                .keyboardShortcut("r", modifiers: [.command, .shift])
        }
    }
    ```
    
    ## Table (macOS-native)
    
    ```swift
    Table(items, selection: $selectedIDs, sortOrder: $sortOrder) {
        TableColumn("Name", value: \.name)
        TableColumn("Date") { Text($0.date, format: .dateTime) }
            .width(min: 100, ideal: 150)
    }
    .contextMenu(forSelectionType: Item.ID.self) { ids in
        Button("Delete", role: .destructive) { delete(ids) }
    }
    ```
    
    For forms, popovers, sheets, inspector, and macOS modifiers, see `references/swiftui-macos.md`.
    
    ## @Observable
    
    ```swift
    @Observable
    final class AppState {
        var projects: [Project] = []
        var isLoading = false
    
        func load() async throws {
            isLoading = true
            defer { isLoading = false }
            projects = try await ProjectService.fetchAll()
        }
    }
    
    // Use: @State var state = AppState()          (owner)
    // Pass: .environment(state)                    (inject)
    // Read: @Environment(AppState.self) var state  (child)
    ```
    
    ## SwiftData
    
    ### @Query & #Predicate
    
    ```swift
    @Query(filter: #Predicate<Project> { !$0.isArchived }, sort: \Project.name)
    private var active: [Project]
    
    // Dynamic predicate
    func search(_ term: String) -> Predicate<Project> {
        #Predicate { $0.name.localizedStandardContains(term) }
    }
    
    // FetchDescriptor (outside views)
    var desc = FetchDescriptor<Project>(predicate: #Predicate { $0.isArchived })
    desc.fetchLimit = 50
    let results = try context.fetch(desc)
    let count = try context.fetchCount(desc)
    ```
    
    ### Relationships
    
    ```swift
    @Model final class Author {
        var name: String
        @Relationship(deleteRule: .cascade, inverse: \Book.author)
        var books: [Book] = []
    
        init(name: String) { self.name = name }
    }
    
    @Model final class Book {
        var title: String
        var author: Author?
        @Relationship var tags: [Tag] = []  // many-to-many
    
        init(title: String) { self.title = title }
    }
    ```
    
    Delete rules: `.cascade`, `.nullify` (default), `.deny`, `.noAction`.
    
    Every `@Model` class needs an explicit initializer - the macro does not synthesize one, and omitting it fails with `@Model requires an initializer be provided for '<Type>'`.
    
    ### Schema Migration
    
    ```swift
    enum SchemaV1: VersionedSchema { /* ... */ }
    enum SchemaV2: VersionedSchema { /* ... */ }
    
    enum MigrationPlan: SchemaMigrationPlan {
        static var schemas: [any VersionedSchema.Type] { [SchemaV1.self, SchemaV2.self] }
        static var stages: [MigrationStage] {
            [.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)]
        }
    }
    
    // Apply: .modelContainer(for: Model.self, migrationPlan: MigrationPlan.self)
    ```
    
    ### CloudKit Sync
    
    Enable iCloud capability, then `.modelContainer(for: Model.self)` auto-syncs. Constraints: all properties need defaults/optional, no unique constraints, optional relationships.
    
    For model attributes, background contexts, batch ops, undo/redo, and testing, see SwiftData references below.
    
    ## Concurrency (Swift 6.2+)
    
    ### Default MainActor Isolation
    
    Opt entire module into main actor - all code runs on main actor by default:
    
    ```swift
    // Package.swift
    .executableTarget(name: "MyApp", swiftSettings: [
        .defaultIsolation(MainActor.self),
    ])
    ```
    
    Or Xcode: Build Settings > Swift Compiler > Default Isolation > MainActor.
    
    ### @concurrent
    
    Mark functions for background execution:
    
    ```swift
    @concurrent
    func processFile(_ url: URL) async throws -> Data {
        let data = try Data(contentsOf: url)
        return try compress(data) // runs off main actor
    }
    // After await, automatically back on main actor
    let result = try await processFile(fileURL)
    ```
    
    Use for CPU-intensive work, I/O, anything not touching UI.
    
    ### Actors
    
    ```swift
    actor DocumentStore {
        private var docs: [UUID: Document] = [:]
        func add(_ doc: Document) { docs[doc.id] = doc }
        func get(_ id: UUID) -> Document? { docs[id] }
        nonisolated let name: String
    }
    // Requires await: let doc = await store.get(id)
    ```
    
    ### Structured Concurrency
    
    ```swift
    // Parallel with async let
    func loadDashboard() async throws -> Dashboard {
        async let profile = fetchProfile()
        async let stats = fetchStats()
        return try await Dashboard(profile: profile, stats: stats)
    }
    
    // Dynamic with TaskGroup
    func processImages(_ urls: [URL]) async throws -> [NSImage] {
        try await withThrowingTaskGroup(of: (Int, NSImage).self) { group in
            for (i, url) in urls.enumerated() {
                group.addTask { (i, try await loadImage(url)) }
            }
            var results = [(Int, NSImage)]()
            for try await r in group { results.append(r) }
            return results.sorted { $0.0 < $1.0 }.map(\.1)
        }
    }
    ```
    
    ### Sendable
    
    ```swift
    struct Point: Sendable { var x, y: Double }              // value types: implicit
    final class Config: Sendable { let apiURL: URL }          // final + immutable
    actor SharedState { var count = 0 }                       // mutable: use actors
    // Enable strict mode: .swiftLanguageMode(.v6) in Package.swift
    ```
    
    ### AsyncSequence & Observations
    
    ```swift
    // Stream @Observable changes (macOS 26+ / iOS 26+, SE-0475)
    // Observations uses a closure init, not Observations(of:).
    let progresses = Observations { manager.progress }
    for await p in progresses { print(p) }
    
    // Typed NotificationCenter (macOS 26+)
    struct DocSaved: NotificationCenter.MainActorMessage {
        typealias Subject = Document
        static var name: Notification.Name { .init("DocSaved") }
        let id: UUID
    }
    NotificationCenter.default.post(DocSaved(id: document.id), subject: document)
    let token = NotificationCenter.default.addObserver(of: document, for: DocSaved.self) { msg in
        refresh(msg.id)
    }
    ```
    
    For concurrency deep dives, see concurrency references below.
    
    ## Foundation Models (macOS 26+)
    
    On-device ~3B LLM. Free, offline, private:
    
    ```swift
    import FoundationModels
    
    let session = LanguageModelSession()
    let response = try await session.respond(to: "Summarize: \(text)")
    print(response.content)  // respond() returns Response<Content>, not Content
    
    // Structured output
    @Generable struct Summary { var title: String; var points: [String] }
    let result = try await session.respond(to: prompt, generating: Summary.self)
    let summary: Summary = result.content
    ```
    
    For tool calling, streaming, and sessions, see `references/foundation-models.md`.
    
    ## Testing
    
    ```swift
    import Testing
    
    @Suite("Project Tests")
    struct ProjectTests {
        @Test("creates with defaults")
        func create() {
            let p = Project(name: "Test")
            #expect(p.name == "Test")
        }
    
        @Test("formats sizes", arguments: [(1024, "1 KB"), (0, "0 KB")])
        func format(bytes: Int, expected: String) {
            #expect(formatSize(bytes) == expected)
        }
    }
    
    // SwiftData testing
    let container = try ModelContainer(
        for: Project.self,
        configurations: ModelConfiguration(isStoredInMemoryOnly: true)
    )
    let ctx = ModelContext(container)
    ctx.insert(Project(name: "Test"))
    try ctx.save()
    ```
    
    For exit tests, attachments, UI testing, see `references/testing.md`.
    
    ## Distribution
    
    | Method | Sandbox | Notarization | Review |
    |--------|---------|--------------|--------|
    | App Store | Required | Automatic | Yes |
    | Developer ID | Recommended | Required | No |
    | Ad-Hoc | No | No | Local only |
    
    ```bash
    xcodebuild archive -scheme MyApp -archivePath MyApp.xcarchive
    xcodebuild -exportArchive -archivePath MyApp.xcarchive \
      -exportPath ./export -exportOptionsPlist ExportOptions.plist
    xcrun notarytool submit ./export/MyApp.dmg \
      --apple-id you@example.com --team-id TEAM_ID \
      --password @keychain:AC_PASSWORD --wait
    xcrun stapler staple ./export/MyApp.dmg
    ```
    
    For complete distribution guide, see `references/distribution.md`.
    
    ## SPM
    
    ```swift
    // swift-tools-version: 6.3
    let package = Package(
        name: "MyApp",
        platforms: [.macOS(.v14)],
        targets: [
            .executableTarget(name: "MyApp", swiftSettings: [
                .swiftLanguageMode(.v6),
                .defaultIsolation(MainActor.self),
            ]),
            .testTarget(name: "MyAppTests", dependencies: ["MyApp"]),
        ]
    )
    ```
    
    For build plugins, macros, and Swift Build, see `references/spm-build.md`.
    
    ## Liquid Glass (macOS 26)
    
    Apps rebuilt with Xcode 26 SDK get automatic Liquid Glass styling. Use `.glassEffect()` for custom glass surfaces, `GlassEffectContainer` for custom hierarchies. Opt out (Xcode 26 only): `UIDesignRequiresCompatibility = YES` in Info.plist keeps the legacy visual style - a temporary migration aid. Apps rebuilt with Xcode 27 (beta) can no longer opt out; the key is ignored and Liquid Glass is mandatory (see `references/fall-2026-releases.md`).
    
    ## ScreenCaptureKit
    
    Capture screen content, app audio, and microphone (macOS 12.3+):
    
    ```swift
    import ScreenCaptureKit
    
    let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
    guard let display = content.displays.first else { return }
    
    // Filter: specific apps only
    let filter = SCContentFilter(display: display, including: [targetApp], exceptingWindows: [])
    
    // Configure
    let config = SCStreamConfiguration()
    config.capturesAudio = true
    config.sampleRate = 48000
    config.channelCount = 2
    config.excludesCurrentProcessAudio = true
    
    // Audio-only: throttle the video pipeline (it always runs). minimumFrameInterval is a
    // MINIMUM gap between frames - 1/Int32.max is ~0s, i.e. native refresh rate (60+ fps
    // of discarded frames and a full-screen recomposite each). Use 1 fps.
    config.width = 2; config.height = 2
    config.minimumFrameInterval = CMTime(value: 1, timescale: 1)
    
    let stream = SCStream(filter: filter, configuration: config, delegate: self)
    try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: nil)
    try stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: audioQueue)
    try await stream.startCapture()
    ```
    
    macOS 15+: `SCRecordingOutput` for simplified file recording, `config.captureMicrophone` for mic capture. macOS 14+: `SCContentSharingPicker` for system picker UI, `SCScreenshotManager` for single-frame capture.
    
    For complete API reference, audio writing (AVAssetWriter/AVAudioFile), permissions, and examples, see `references/screen-capture-audio.md`.
    
    ## AppKit Interop
    
    ```swift
    struct WebViewWrapper: NSViewRepresentable {
        let url: URL
        func makeNSView(context: Context) -> WKWebView { WKWebView() }
        func updateNSView(_ v: WKWebView, context: Context) {
            v.load(URLRequest(url: url))
        }
    }
    ```
    
    For hosting SwiftUI in AppKit and advanced bridging, see `references/appkit-interop.md`.
    
    ## Architecture
    
    | Pattern | Best For | Complexity |
    |---------|----------|------------|
    | SwiftUI + @Observable | Small-medium, solo | Low |
    | MVVM + @Observable | Medium, teams | Medium |
    | TCA | Large, strict testing | High |
    
    See `references/architecture.md` for all patterns with examples.
    
    ## References
    
    | File | When to read |
    |------|-------------|
    | `references/fall-2026-releases.md` | WWDC 2026 beta stack: macOS 27, Xcode 27, Swift 6.4, Foundation Models next-gen, Core AI, Spatial Preview, mandatory Liquid Glass |
    | **SwiftUI & macOS** | |
    | `references/app-lifecycle.md` | Window management, scenes, DocumentGroup, MenuBarExtra gotchas, async termination, LSUIElement issues |
    | `references/swiftui-macos.md` | Sidebar, Inspector, Table, forms, popovers, sheets, search |
    | `references/appkit-interop.md` | NSViewRepresentable, hosting controllers, NSHostingSceneRepresentation, sizing/scene-bridging options, AppKit Liquid Glass (NSGlassEffectView), pasteboard privacy, NSPanel/floating HUD |
    | `references/screen-capture-audio.md` | ScreenCaptureKit, SCStream gotchas, SCStream teardown hazards, AVAudioEngine dual pipeline, AVAssetWriter crash safety, non-interleaved stereo trap, TCC gotchas, CDHash degraded-state after reinstall, SpeechAnalyzer transcription |
    | `references/core-audio-tap.md` | CATap for per-process audio: tap-only aggregate (HFP-safe), drift compensation, rate-change anti-pattern, interleaved-stereo frame-count trap, IO proc isolation |
    | `references/system-integration.md` | Keyboard shortcuts, drag & drop, file access, App Intents (entities, Spotlight, snippets), widgets & Control Center, process monitoring, AXUIElement, CoreAudio per-process APIs, login items, XPC, LSUIElement, os.Logger & signposts, privacy usage descriptions |
    | `references/foundation-models.md` | On-device AI: guided generation, tool calling, streaming |
    | `references/architecture.md` | MVVM, TCA, dependency injection, project structure |
    | `references/testing.md` | Swift Testing, exit tests, attachments, UI testing, XCTest migration |
    | `references/distribution.md` | App Store, Developer ID, notarization gotchas, nested bundle signing, xcodebuild signing traps (silent ad-hoc builds), sandboxing, universal binaries |
    | `references/spm-build.md` | Package.swift, Swift Build, plugins, macros, plugin/macro validation gates, Metal toolchain download, manual .app bundle assembly, mixed ObjC targets, CLT testing |
    | **Concurrency** | |
    | `references/approachable-concurrency.md` | Default MainActor isolation, @concurrent, nonisolated async, runtime pitfalls |
    | `references/actors-isolation.md` | Actor model, global actors, custom executors, reentrancy |
    | `references/structured-concurrency.md` | Task, TaskGroup, async let, cancellation, priority, named tasks |
    | `references/sendable-safety.md` | Sendable protocol, data race safety, @unchecked Sendable + serial queue, @preconcurrency import |
    | `references/async-patterns.md` | AsyncSequence, AsyncStream, Observations, continuations, Clock |
    | `references/migration-guide.md` | GCD to async/await, Combine to AsyncSequence, Swift 6 migration |
    | **SwiftData** | |
    | `references/models-schema.md` | @Model, @Attribute options, Codable, transformable, external storage |
    | `references/relationships-predicates.md` | Advanced relationships, inverse rules, compound predicates |
    | `references/container-context.md` | ModelContainer, ModelContext, background contexts, undo/redo, batch ops |
    | `references/cloudkit-sync.md` | CloudKit setup, conflict resolution, sharing, debugging sync |
    | `references/migrations.md` | VersionedSchema, lightweight/custom migration, Core Data migration |
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related