Loading...
Loading...
Comprehensive macOS app development with Swift 6.2, SwiftUI, SwiftData, Swift Concurrency, Foundation Models, Swift Testing, ScreenCaptureKit, and app distribution. Use when building native Mac apps, implementing windows/scenes/navigation/menus/toolbars, persisting data with SwiftData (@Model, @Query,
npx skill4agent add tenequm/skills swift-macosimport SwiftUI
import SwiftData
@Model
final class Project {
var name: String
var createdAt: Date
@Relationship(deleteRule: .cascade) var tasks: [Task] = []
init(name: String) {
self.name = name
self.createdAt = .now
}
}
@Model
final class Task {
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, Task.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")
}
}
}
}| Scene | Purpose |
|---|---|
| Resizable windows (multiple instances) |
| Single-instance utility window |
| Preferences (Cmd+,) |
| Menu bar with |
| Document-based apps |
@Environment(\.openWindow) var openWindow; openWindow(id: "about")references/app-lifecycle.md.commands {
CommandGroup(replacing: .newItem) {
Button("New Project") { /* ... */ }
.keyboardShortcut("n", modifiers: .command)
}
CommandMenu("Tools") {
Button("Run Analysis") { /* ... */ }
.keyboardShortcut("r", modifiers: [.command, .shift])
}
}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) }
}references/swiftui-macos.md@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)@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)@Model final class Author {
var name: String
@Relationship(deleteRule: .cascade, inverse: \Book.author)
var books: [Book] = []
}
@Model final class Book {
var title: String
var author: Author?
@Relationship var tags: [Tag] = [] // many-to-many
}.cascade.nullify.deny.noActionenum 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).modelContainer(for: Model.self)// Package.swift
.executableTarget(name: "MyApp", swiftSettings: [
.defaultIsolation(MainActor.self),
])@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)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)// 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)
}
}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// Stream @Observable changes (Swift 6.2)
for await state in Observations(of: manager) {
print(state.progress)
}
// Typed NotificationCenter (Swift 6.2)
struct DocSaved: MainActorMessage { let id: UUID }
NotificationCenter.default.post(DocSaved(id: doc.id))
for await n in NotificationCenter.default.notifications(of: DocSaved.self) {
refresh(n.id)
}import FoundationModels
let session = LanguageModelSession()
let response = try await session.respond(to: "Summarize: \(text)")
// Structured output
@Generable struct Summary { var title: String; var points: [String] }
let result: Summary = try await session.respond(to: prompt, generating: Summary.self)references/foundation-models.mdimport 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()references/testing.md| 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.dmgreferences/distribution.md// swift-tools-version: 6.2
let package = Package(
name: "MyApp",
platforms: [.macOS(.v14)],
targets: [
.executableTarget(name: "MyApp", swiftSettings: [
.swiftLanguageMode(.v6),
.defaultIsolation(MainActor.self),
]),
.testTarget(name: "MyAppTests", dependencies: ["MyApp"]),
]
)references/spm-build.md.glassEffect()GlassEffectContainerUIDesignRequiresLiquidGlass = NOimport 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: minimize video overhead
config.width = 2; config.height = 2
config.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale.max)
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()SCRecordingOutputconfig.captureMicrophoneSCContentSharingPickerSCScreenshotManagerreferences/screen-capture-audio.mdstruct WebViewWrapper: NSViewRepresentable {
let url: URL
func makeNSView(context: Context) -> WKWebView { WKWebView() }
func updateNSView(_ v: WKWebView, context: Context) {
v.load(URLRequest(url: url))
}
}references/appkit-interop.md| Pattern | Best For | Complexity |
|---|---|---|
| SwiftUI + @Observable | Small-medium, solo | Low |
| MVVM + @Observable | Medium, teams | Medium |
| TCA | Large, strict testing | High |
references/architecture.md| File | When to read |
|---|---|
| SwiftUI & macOS | |
| Window management, scenes, DocumentGroup, MenuBarExtra gotchas, async termination, LSUIElement issues |
| Sidebar, Inspector, Table, forms, popovers, sheets, search |
| NSViewRepresentable, hosting controllers, AppKit bridging, NSPanel/floating HUD |
| ScreenCaptureKit, SCStream gotchas, AVAudioEngine dual pipeline, AVAssetWriter crash safety, TCC gotchas |
| Keyboard shortcuts, drag & drop, file access, App Intents, process monitoring, CoreAudio per-process APIs, login items, LSUIElement, idle sleep prevention |
| On-device AI: guided generation, tool calling, streaming |
| MVVM, TCA, dependency injection, project structure |
| Swift Testing, exit tests, attachments, UI testing, XCTest migration |
| App Store, Developer ID, notarization gotchas, nested bundle signing, sandboxing, universal binaries |
| Package.swift, Swift Build, plugins, macros, manual .app bundle assembly, mixed ObjC targets, CLT testing |
| Concurrency | |
| Default MainActor isolation, @concurrent, nonisolated async, runtime pitfalls |
| Actor model, global actors, custom executors, reentrancy |
| Task, TaskGroup, async let, cancellation, priority, named tasks |
| Sendable protocol, data race safety, @unchecked Sendable + serial queue, @preconcurrency import |
| AsyncSequence, AsyncStream, Observations, continuations, Clock |
| GCD to async/await, Combine to AsyncSequence, Swift 6 migration |
| SwiftData | |
| @Model, @Attribute options, Codable, transformable, external storage |
| Advanced relationships, inverse rules, compound predicates |
| ModelContainer, ModelContext, background contexts, undo/redo, batch ops |
| CloudKit setup, conflict resolution, sharing, debugging sync |
| VersionedSchema, lightweight/custom migration, Core Data migration |