macos-swiftui

Installation
SKILL.md

macOS SwiftUI Development

Comprehensive macOS app development skill for macOS 14+ (Sonoma) with SwiftUI and modern Swift practices.

Overview

Target Platform: macOS 14+ (Sonoma) / Swift 5.9+

Core Technologies:

  • UI Framework: SwiftUI (primary), AppKit (when needed)
  • Window Management: WindowGroup, Window, MenuBarExtra
  • Navigation: NavigationSplitView, NavigationStack
  • Data: SwiftData (recommended), Core Data
  • Document: FileDocument, DocumentGroup

Quick Start

New Project Setup

To create a new macOS project:

  1. Open Xcode and select "Create New Project"
  2. Choose "App" template under macOS
  3. Select SwiftUI for Interface
  4. Set minimum deployment target to macOS 14.0
  5. Enable Swift strict concurrency checking in Build Settings

Basic App Structure

import SwiftUI

@main
struct MyMacApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            // Custom menu commands
        }

        Settings {
            SettingsView()
        }
    }
}

Existing Project Analysis

To analyze an existing project structure:

bash scripts/macos-project-analyzer.sh /path/to/project

Window Management

WindowGroup (Multi-Instance Windows)

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .defaultSize(width: 800, height: 600)
        .defaultPosition(.center)
    }
}

Window (Single Instance)

@main
struct MyApp: App {
    var body: some Scene {
        Window("Inspector", id: "inspector") {
            InspectorView()
        }
        .defaultSize(width: 300, height: 400)
        .windowResizability(.contentSize)
    }
}

Opening Windows Programmatically

struct ContentView: View {
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("Open Inspector") {
            openWindow(id: "inspector")
        }
    }
}

For detailed window patterns, see references/window-management.md.

Menu Bar Integration

Custom Commands

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            CommandGroup(after: .newItem) {
                Button("New Document") {
                    // Action
                }
                .keyboardShortcut("n", modifiers: [.command, .shift])
            }

            CommandMenu("Custom") {
                Button("Action 1") { }
                    .keyboardShortcut("1", modifiers: .command)
                Divider()
                Button("Action 2") { }
            }
        }
    }
}

FocusedValue for Window-Specific State

struct FocusedDocumentKey: FocusedValueKey {
    typealias Value = Binding<Document>
}

extension FocusedValues {
    var document: Binding<Document>? {
        get { self[FocusedDocumentKey.self] }
        set { self[FocusedDocumentKey.self] = newValue }
    }
}

// In View
.focusedSceneValue(\.document, $document)

For detailed menu patterns, see references/menu-commands.md.

Navigation Patterns

NavigationSplitView (Two/Three Column)

struct ContentView: View {
    @State private var selectedCategory: Category?
    @State private var selectedItem: Item?

    var body: some View {
        NavigationSplitView {
            // Sidebar
            List(categories, selection: $selectedCategory) { category in
                Label(category.name, systemImage: category.icon)
            }
            .navigationSplitViewColumnWidth(min: 180, ideal: 200, max: 250)
        } content: {
            // Content
            if let category = selectedCategory {
                List(category.items, selection: $selectedItem) { item in
                    Text(item.name)
                }
            }
        } detail: {
            // Detail
            if let item = selectedItem {
                ItemDetailView(item: item)
            } else {
                ContentUnavailableView("Select an Item", systemImage: "doc")
            }
        }
        .navigationSplitViewStyle(.balanced)
    }
}

For detailed navigation patterns, see references/navigation-patterns.md.

Settings/Preferences

Settings Scene

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

        Settings {
            SettingsView()
        }
    }
}

struct SettingsView: View {
    var body: some View {
        TabView {
            GeneralSettingsView()
                .tabItem {
                    Label("General", systemImage: "gear")
                }

            AdvancedSettingsView()
                .tabItem {
                    Label("Advanced", systemImage: "gearshape.2")
                }
        }
        .frame(width: 450, height: 300)
    }
}

@AppStorage for User Defaults

struct GeneralSettingsView: View {
    @AppStorage("showSidebar") private var showSidebar = true
    @AppStorage("fontSize") private var fontSize = 14.0

    var body: some View {
        Form {
            Toggle("Show Sidebar", isOn: $showSidebar)
            Slider(value: $fontSize, in: 10...24, step: 1) {
                Text("Font Size: \(Int(fontSize))")
            }
        }
        .formStyle(.grouped)
        .padding()
    }
}

For detailed settings patterns, see references/settings-preferences.md.

Document-Based Apps

FileDocument Implementation

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 string = String(data: data, encoding: .utf8) else {
            throw CocoaError(.fileReadCorruptFile)
        }
        text = string
    }

    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        let data = text.data(using: .utf8)!
        return FileWrapper(regularFileWithContents: data)
    }
}

@main
struct TextEditorApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: TextDocument()) { file in
            TextEditorView(document: file.$document)
        }
    }
}

For detailed document patterns, see references/document-based-apps.md.

Toolbar

Toolbar Items

struct ContentView: View {
    @State private var searchText = ""

    var body: some View {
        NavigationSplitView {
            Sidebar()
        } detail: {
            DetailView()
        }
        .toolbar {
            ToolbarItem(placement: .primaryAction) {
                Button(action: addItem) {
                    Label("Add", systemImage: "plus")
                }
            }

            ToolbarItem(placement: .navigation) {
                Button(action: toggleSidebar) {
                    Label("Sidebar", systemImage: "sidebar.left")
                }
            }
        }
        .searchable(text: $searchText, placement: .toolbar)
    }
}

ToolbarItemGroup

.toolbar {
    ToolbarItemGroup(placement: .primaryAction) {
        Button { } label: {
            Label("Bold", systemImage: "bold")
        }
        Button { } label: {
            Label("Italic", systemImage: "italic")
        }
    }
}

System Integration

Notifications

import UserNotifications

func requestNotificationPermission() async -> Bool {
    let center = UNUserNotificationCenter.current()
    do {
        return try await center.requestAuthorization(options: [.alert, .sound, .badge])
    } catch {
        return false
    }
}

func sendNotification(title: String, body: String) {
    let content = UNMutableNotificationContent()
    content.title = title
    content.body = body
    content.sound = .default

    let request = UNNotificationRequest(
        identifier: UUID().uuidString,
        content: content,
        trigger: nil
    )

    UNUserNotificationCenter.current().add(request)
}

Menu Bar Extra (Status Bar Apps)

@main
struct MyApp: App {
    var body: some Scene {
        MenuBarExtra("My App", systemImage: "star") {
            MenuBarView()
        }
        .menuBarExtraStyle(.window)
    }
}

For detailed system integration patterns, see references/system-integration.md.

Drag & Drop / Pasteboard

Transferable Protocol

struct MyItem: Transferable, Codable {
    var id: UUID
    var name: String

    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .myItem)
        ProxyRepresentation(exporting: \.name)
    }
}

extension UTType {
    static var myItem = UTType(exportedAs: "com.example.myitem")
}

Draggable and DropDestination

struct ItemView: View {
    let item: MyItem

    var body: some View {
        Text(item.name)
            .draggable(item)
    }
}

struct DropZoneView: View {
    @State private var items: [MyItem] = []

    var body: some View {
        VStack {
            ForEach(items) { item in
                Text(item.name)
            }
        }
        .dropDestination(for: MyItem.self) { items, location in
            self.items.append(contentsOf: items)
            return true
        }
    }
}

For detailed drag & drop patterns, see references/drag-drop-pasteboard.md.

macOS-Specific UI Components

Table View

struct TableExample: View {
    @State private var items: [Item] = []
    @State private var selection: Set<Item.ID> = []
    @State private var sortOrder = [KeyPathComparator(\Item.name)]

    var body: some View {
        Table(items, selection: $selection, sortOrder: $sortOrder) {
            TableColumn("Name", value: \.name)
            TableColumn("Date", value: \.date) { item in
                Text(item.date.formatted())
            }
            TableColumn("Size", value: \.size) { item in
                Text("\(item.size) KB")
            }
        }
        .onChange(of: sortOrder) { _, newOrder in
            items.sort(using: newOrder)
        }
    }
}

Popover

struct PopoverExample: View {
    @State private var showPopover = false

    var body: some View {
        Button("Show Info") {
            showPopover = true
        }
        .popover(isPresented: $showPopover, arrowEdge: .bottom) {
            VStack {
                Text("Information")
                    .font(.headline)
                Text("Details go here")
            }
            .padding()
            .frame(width: 200)
        }
    }
}

AppKit Integration

NSViewRepresentable

import AppKit
import SwiftUI

struct NSTextViewWrapper: 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.string = text
        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(self)
    }

    class Coordinator: NSObject, NSTextViewDelegate {
        var parent: NSTextViewWrapper

        init(_ parent: NSTextViewWrapper) {
            self.parent = parent
        }

        func textDidChange(_ notification: Notification) {
            guard let textView = notification.object as? NSTextView else { return }
            parent.text = textView.string
        }
    }
}

Code Quality Checklist

Before submitting code, verify:

  • All public APIs have documentation comments
  • ViewModels use @MainActor for UI updates
  • Windows have appropriate default sizes and positions
  • Menu commands have keyboard shortcuts where appropriate
  • Settings are properly persisted with @AppStorage
  • Document apps handle errors gracefully
  • Drag & drop uses Transferable protocol correctly
  • No force unwrapping (!) without safety checks
  • Memory management: no retain cycles in closures
  • App works correctly in sandboxed environment

Additional Resources

Reference Files

Example Files

Utility Scripts

Weekly Installs
1
GitHub Stars
1
First Seen
11 days ago
Installed on
mcpjam1
claude-code1
kilo1
junie1
windsurf1
zencoder1