Install
$ agentstack add skill-toankhontech-swiftui-mac-pro-skill-swiftui-mac-pro-skill ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
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
- Match the app's existing architecture, deployment target, and naming before introducing a new pattern.
- Distribution-aware: never assume sandbox on/off — check
.entitlementsfirst. If not stated, assume both Mac App Store + Direct must work. - Prefer Apple-built frameworks. Fall to AppKit when SwiftUI cannot deliver pro-grade UX (NSToolbar customization, NSWindowController state, custom titlebar, NSStatusItem behavior).
- Write complete, compilable snippets — include imports, no undefined placeholders inside "complete" examples.
- Build or typecheck when a local project is available before declaring code done.
- Never introduce macOS 16+ / macOS 26 APIs without
if #available(macOS 26.0, *)gate. - Swift 6.2 default-isolation aware: explicit
@MainActoron view models even when project setsdefaultActorIsolation = 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
NavigationView→NavigationSplitView(sidebar/detail) orNavigationStack(push).NavigationViewproduces 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;@Publishedinvalidates on any change.@StateObjectfor an@Observableclass →@State. CRITICAL: the migration is NOT 1:1 —@StateObjectwas lazy/once-per-lifetime;@Stateinitializes 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.taskbody or move it behind a lazy property.ReferenceFileDocumentstill requiresObservableObjectconformance (not yet@Observable-native, verified 2026-05-06). Keep@Publishedproperties for that protocol only.
Previews
PreviewProvider→#Preview.
Alerts and dialogs
Alert/ActionSheetstructs →.alert(...)/.confirmationDialog(...)modifiers.
Lifecycle
.onChange(of:perform:)(single-parameter closure) → zero- or two-parameter overload ({ }or{ old, new in }).Task { … }inside.onAppearfor initial async load →.task(or.task(id:))..taskcancels automatically on disappear.
Styling and gestures
.animation(_:)(broad view-level) →.animation(_:value:)orwithAnimation { … }..accentColor(_:)→ asset catalog accent or.tint(_:).Text(...).onTapGesturefor tappable controls →Button.
Tabs
.tabItem→Tab(macOS 15+) for new code..tabItemis soft-deprecated in macOS 15 but not yet hard-removed.- Nuance for
Settingsscene specifically: both.tabItemandTabrender the same underlyingNSTabView; Apple's own samples (e.g., Backyard Birds, WWDC24) still use.tabItemfor 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/NSOpenPaneldirect 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
@MainActorexplicitly at the class level, even if the project setsdefaultActorIsolation = MainActor. - Services: prefer
Sendablevalue types, or actors when shared mutable state is involved. @Sendablefor closures crossing concurrency boundaries.Task.detachedis rarely correct — escapes inherited isolation. Most UI work belongs inTask { … }(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.
- Author: toankhontech
- Source: toankhontech/swiftui-mac-pro-skill
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.