AgentStack
SKILL verified MIT Self-run

Swiftui Mac Pro Skill

skill-toankhontech-swiftui-mac-pro-skill-swiftui-mac-pro-skill · by toankhontech

Use for ALL macOS SwiftUI app dev — building, editing, debugging `.swift` files on macOS targets. Current focus: macOS 15 Sequoia + macOS 26 Tahoe; future macOS added as Apple ships. Covers window scenes (WindowGroup, Window, MenuBarExtra, Settings, DocumentGroup), navigation (NavigationSplitView, NavigationStack), toolbars (.toolbar/NSToolbar), main menu (CommandGroup, .commands), AppKit interop…

No reviews yet
0 installs
6 views
0.0% view→install

Install

$ agentstack add skill-toankhontech-swiftui-mac-pro-skill-swiftui-mac-pro-skill

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Swiftui Mac Pro Skill? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

swiftui-mac-pro-skill — Production Mac SwiftUI for Claude Code

> Currently focused on macOS 15 Sequoia (baseline) + macOS 26 Tahoe (Liquid Glass opt-in). These are the most-deployed macOS versions in 2026. Future macOS releases will be added as Apple ships them. The skill is version-flexible: APIs gated by if #available(...) keep older targets working.

When this skill applies

Use these instructions for SwiftUI work whose deployment target is macOS 15 Sequoia. The skill is macOS 15-first: every code path must run on macOS 15. macOS 26 (Tahoe) Liquid Glass features are allowed only as opt-in enhancements gated with if #available(macOS 26.0, *) — see [references/macos26-optin.md](references/macos26-optin.md).

Code must compile cleanly under Swift 5 and Swift 6 language modes (including the Swift 6.2 default-isolation defaults if the project enables them). New code should not rely on patterns that become errors when strict concurrency is on. See [references/swift6-concurrency.md](references/swift6-concurrency.md).

Distribution-aware: Every Mac project ships to one of: Mac App Store (App Sandbox required), Direct distribution via Developer ID (Hardened Runtime required, Sandbox optional), or both. Before introducing any API that touches the file system, network, devices, or other apps, check the project's .entitlements file. If unstated, assume both modes must work — that's the most restrictive path.

Respect the existing project. Before changing code, check the deployment target, Swift language mode, persistence stack, sandbox status, and naming conventions. Existing settings win unless the user explicitly asks to migrate. Never introduce macOS 16+ / macOS 26 APIs without an availability gate.

Working rules

  1. Match the app's existing architecture, deployment target, and naming before introducing a new pattern.
  2. Distribution-aware: never assume sandbox on/off — check .entitlements first. If not stated, assume both Mac App Store + Direct must work.
  3. Prefer Apple-built frameworks. Fall to AppKit when SwiftUI cannot deliver pro-grade UX (NSToolbar customization, NSWindowController state, custom titlebar, NSStatusItem behavior).
  4. Write complete, compilable snippets — include imports, no undefined placeholders inside "complete" examples.
  5. Build or typecheck when a local project is available before declaring code done.
  6. Never introduce macOS 16+ / macOS 26 APIs without if #available(macOS 26.0, *) gate.
  7. Swift 6.2 default-isolation aware: explicit @MainActor on view models even when project sets defaultActorIsolation = MainActor (consistency + readability).

Deprecated patterns to avoid

These are deprecated as of macOS 15 SwiftUI. Each has a recommended replacement and a brief reason — knowing why helps when you encounter edge cases.

Navigation

  • NavigationViewNavigationSplitView (sidebar/detail) or NavigationStack (push). NavigationView produces unpredictable layout on macOS and is being removed.
  • NavigationLink(isActive:), tag:, selection:List(selection:) + .navigationDestination(for:) with typed values. Old initializers don't compose with typed paths and break programmatic back-navigation.

State / Observation

  • ObservableObject/@Published/@ObservedObject/@StateObject@Observable + @State/@Bindable. Observation tracks reads at the property level; @Published invalidates on any change.
  • @StateObject for an @Observable class → @State. CRITICAL: the migration is NOT 1:1 — @StateObject was lazy/once-per-lifetime; @State initializes eagerly. If your view model has a heavy initializer (network call, file I/O, model load), the regression can cause double-fetches or re-init storms. Refactor the heavy work into a .task body or move it behind a lazy property.
  • ReferenceFileDocument still requires ObservableObject conformance (not yet @Observable-native, verified 2026-05-06). Keep @Published properties for that protocol only.

Previews

  • PreviewProvider#Preview.

Alerts and dialogs

  • Alert / ActionSheet structs → .alert(...) / .confirmationDialog(...) modifiers.

Lifecycle

  • .onChange(of:perform:) (single-parameter closure) → zero- or two-parameter overload ({ } or { old, new in }).
  • Task { … } inside .onAppear for initial async load → .task (or .task(id:)). .task cancels automatically on disappear.

Styling and gestures

  • .animation(_:) (broad view-level) → .animation(_:value:) or withAnimation { … }.
  • .accentColor(_:) → asset catalog accent or .tint(_:).
  • Text(...).onTapGesture for tappable controls → Button.

Tabs

  • .tabItemTab (macOS 15+) for new code. .tabItem is soft-deprecated in macOS 15 but not yet hard-removed.
  • Nuance for Settings scene specifically: both .tabItem and Tab render the same underlying NSTabView; Apple's own samples (e.g., Backyard Birds, WWDC24) still use .tabItem for Settings. Migration is code cleanliness, not API removal urgency for that case.

AppKit lifecycle

  • NSApp.activate(ignoringOtherApps: true)NSApp.activate() (macOS 14+). Old form deprecated; emits runtime warning on macOS 15 because it ignores activation policy. New form respects the policy and is the only correct call going forward.
  • UserDefaults.standard.synchronize() → remove the call entirely. No-op since macOS 10.12 (2016). Apple docs: "This method is unnecessary and shouldn't be used."
  • NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)@Environment(\.openSettings) + openSettings() (macOS 14+). The selector form is private API and brittle; the environment value is the official, type-safe replacement.

Window

  • .background(Color) for window background → .containerBackground(_:for: .window) (macOS 14+).
  • NSSavePanel/NSOpenPanel direct presentation → .fileImporter(_:) / .fileExporter(_:) SwiftUI modifiers (macOS 11+) where possible.

macOS 26 features — opt-in via availability gates

Liquid Glass APIs (glassEffect, Glass, GlassEffectContainer, .buttonStyle(.glass), Observations, tabBarMinimizeBehavior) crash on macOS 15. Always gate:

if #available(macOS 26.0, *) {
    SomeView().glassEffect(.regular)
} else {
    SomeView()
}

Liquid Glass auto-adopts on toolbars/sidebars/sheets when the app is recompiled with Xcode 26 — no code change needed for system chrome. See [references/macos26-optin.md](references/macos26-optin.md).

Window scenes

The biggest cognitive shift from iOS to Mac SwiftUI is the Scene system. Every Mac app has one or more Scenes that define windows. Pick the right one:

| Scene | Use when | macOS available | |---|---|---| | WindowGroup | Multi-instance content; user expects File > New Window | 11+ | | WindowGroup(for: T.self) | One window per data value (open Document X brings existing to front if open) | 13+ | | Window | Single uniquely-identified window (Inspector, Welcome, About) | 13+ | | Settings | App preferences — auto-wires Settings… menu and Cmd+, | 11+ | | MenuBarExtra | Status-bar utility | 13+ | | DocumentGroup | File-backed documents with Open/Save/Recent | 11+ |

Minimal example combining the most common scenes:

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .windowToolbarStyle(.unified)

        Window("Inspector", id: "inspector") {
            InspectorView()
        }
        .windowLevel(.floating)

        Settings {
            SettingsView()
        }

        MenuBarExtra("Status", systemImage: "star") {
            StatusMenu()
        }
        .menuBarExtraStyle(.menu)
    }
}

For multi-window patterns (@FocusedValue, \.openWindow, .handlesExternalEvents, state restoration), see [references/windows-and-scenes.md](references/windows-and-scenes.md).

For menu bar–only apps (no Dock icon, status-bar lifecycle), see [references/menu-bar-apps.md](references/menu-bar-apps.md).

For Settings scene patterns (single-pane, multi-pane, App Group storage), see [references/settings-scene.md](references/settings-scene.md).

For document-based apps (FileDocument vs ReferenceFileDocument), see [references/nsdocument-documentgroup.md](references/nsdocument-documentgroup.md).

Navigation

On Mac, NavigationSplitView (sidebar/detail or 3-column) is the default — not NavigationStack. Mac users expect a sidebar.

import SwiftUI

struct ContentView: View {
    @State private var selection: Item.ID?
    let items: [Item]

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

For sub-flows inside a detail pane (e.g., drilling into a sub-view), nest NavigationStack within the detail.

Programmatic navigation: bind path and selection to @State for deep links / "go back to root" / state restoration.

Toolbar

Use SwiftUI's .toolbar for almost all toolbar work — it produces a real NSToolbar under the hood with system styling.

Common placements on Mac (ToolbarItemPlacement):

  • .primaryAction — leading edge of toolbar (right edge on iOS).
  • .confirmationAction, .cancellationAction, .destructiveAction — sheet/dialog buttons get correct keyboard handling.
  • .principal — center title-bar slot; replaces default title.
  • .navigation — leading nav controls.
  • .automatic — system picks slot.

Minimal example:

.toolbar {
    ToolbarItem(placement: .primaryAction) {
        Button("Refresh", systemImage: "arrow.clockwise") {
            Task { await viewModel.load() }
        }
    }
    ToolbarItem(placement: .principal) {
        Text(viewModel.title).font(.headline)
    }
}
.windowToolbarStyle(.unified)

Choose .windowToolbarStyle(.unified) for most pro Mac apps; .unifiedCompact for utility windows; .expanded for chunky toolbars.

For toolbar customization with stable IDs, .toolbarRole, .toolbarTitleMenu, and when to drop to NSToolbar directly, see [references/nstoolbar.md](references/nstoolbar.md).

Commands and main menu

Mac apps have a real main menu. Customize it via .commands { … } on a Scene.

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            CommandGroup(replacing: .newItem) {
                Button("New Item") { /* ... */ }
                    .keyboardShortcut("n", modifiers: .command)
            }
            CommandMenu("Tools") {
                Button("Run") { /* ... */ }
                    .keyboardShortcut("r", modifiers: .command)
            }
            SidebarCommands()
            ToolbarCommands()
        }
    }
}

SidebarCommands() adds View > Show/Hide Sidebar; ToolbarCommands() adds View > Show/Hide/Customize Toolbar — both with standard keyboard shortcuts. Use them by default.

Common CommandGroupPlacement identifiers: .appInfo, .appSettings, .newItem, .saveItem, .printItem, .undoRedo, .pasteboard, .textEditing, .toolbar, .sidebar, .windowSize, .windowList, .help. For the full list, focused-value integration, and Services menu, see [references/commands-and-menus.md](references/commands-and-menus.md).

Observation

For new reference-type UI state, use @Observable. Mark UI-facing view models @MainActor so view updates stay on the main thread.

import Foundation
import Observation

protocol ItemServiceProtocol: Sendable {
    func fetchItems() async throws -> [String]
}

struct ItemService: ItemServiceProtocol {
    func fetchItems() async throws -> [String] { [] }
}

@Observable
@MainActor
final class ItemListViewModel {
    var items: [String] = []
    var isLoading = false
    var errorMessage: String?

    private let service: any ItemServiceProtocol

    init(service: any ItemServiceProtocol = ItemService()) {
        self.service = service
    }

    func loadItems() async {
        isLoading = true
        errorMessage = nil
        defer { isLoading = false }
        do {
            items = try await service.fetchItems()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

Owning the object in a view: use @State, not @StateObject.

struct ItemListScreen: View {
    @State private var viewModel = ItemListViewModel()

    var body: some View {
        List(viewModel.items, id: \.self) { Text($0) }
            .task { await viewModel.loadItems() }
    }
}

Lifecycle nuance vs @StateObject: both @State and @StateObject initialize once per view identity — not per body re-evaluation. The difference is what triggers re-initialization. SwiftUI re-creates the storage when:

  • The view's structural identity changes (e.g., parent applies a different .id(_)).
  • The view is removed from the hierarchy and re-added.

What this means in practice: if your @Observable view model has a heavy init (network call, big file load, model warmup), and a parent thrashes the view's identity (e.g., conditional rendering inside a high-frequency-updating parent), you can see re-init storms. Mitigations: move heavy work into .task (cancels on disappear); use a lazy property; or share via .environment(_:) so the object is owned higher up where identity is stable.

Passing to children: plain let/var is enough for read access. Use @Bindable only when the child needs a Binding to a property.

For shared app state: inject through the environment — .environment(store) to provide, @Environment(Store.self) private var store to read.

ReferenceFileDocument exception: the ReferenceFileDocument protocol still requires ObservableObject conformance. Keep @Published for that protocol only — you can use @Observable everywhere else.

Don't @Observable-decorate services with no UI state. The macro adds property-level read tracking, observer registration, and change-detection plumbing — all wasted overhead when no view reads the properties. Worse, the assumption "anything @Observable triggers UI updates" silently stops being true and can mask observability bugs in cleanup tasks, telemetry senders, loggers, or background workers. Use plain final class for service classes that don't expose state to views.

// ❌ Wasted overhead — no view reads telemetry's properties
@Observable
final class TelemetrySender {
    private var queue: [Event] = []
    func enqueue(_ event: Event) { queue.append(event) }
}

// ✅ Plain class — no observation infrastructure needed
final class TelemetrySender {
    private var queue: [Event] = []
    func enqueue(_ event: Event) { queue.append(event) }
}

Concurrency (Swift 6.2)

Swift 6.2's Approachable Concurrency changed defaults around actor isolation and nonisolated async. Quick rules for SwiftUI work:

  • Always mark UI-facing view models @MainActor explicitly at the class level, even if the project sets defaultActorIsolation = MainActor.
  • Services: prefer Sendable value types, or actors when shared mutable state is involved.
  • @Sendable for closures crossing concurrency boundaries.
  • Task.detached is rarely correct — escapes inherited isolation. Most UI work belongs in Task { … } (structured) or in an actor.
  • @concurrent (Swift 6.2) — explicit opt-in for "this work runs in parallel, off

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.