| name | swift |
| description | Use when working with Swift code, especially macOS SwiftUI apps with modern concurrency, plugin architectures, and dependency injection. Covers @MainActor, @Observable, structured concurrency, protocol-based design, and Swift 6 migration. |
Swift for macOS Development
Expert guidance for Swift development on Apple platforms, with focus on modern Swift patterns, macOS SwiftUI, plugin architectures, and strict concurrency.
When to Use
- Building or refactoring macOS/iOS apps with SwiftUI
- Working with plugin-based architectures (like machNotch)
- Implementing dependency injection without singletons
- Swift 6 migration and strict concurrency issues
- Modern concurrency patterns (async/await, actors, Sendable)
- @Observable migration from ObservableObject
- @MainActor isolation and UI threading
Agent Behavior Contract
- Check Swift version first — Look at
Package.swift tools version or Xcode project settings before giving version-specific advice
- Prefer modern patterns —
@Observable over ObservableObject, protocol-based DI over .shared singletons
- Respect @MainActor — UI code must be
@MainActor; don't suggest @MainActor as blanket fix for non-UI code
- Strict concurrency aware — Assume Swift 6 complete checking; justify any
@preconcurrency or @unchecked Sendable
- Type-erasure when needed — Use
AnyView or custom existential wrappers for protocol types with associated types
Quick Decision Tree
Starting a new file?
- Check existing patterns in sibling files first
- Max 300 lines per file (hard limit)
- Use
@Observable + @MainActor for state
- Protocol-based services, injected via init
Refactoring legacy code?
- Eliminate singletons first — Replace
.shared with injected protocols
- Migrate to @Observable — Replace
ObservableObject/@Published with @Observable
- Add @MainActor — UI-related classes only
- Fix concurrency — Make types
Sendable, use actors for shared state
Plugin architecture?
- Read
NotchPlugin protocol definition
- Plugin receives
PluginContext via activate(context:)
- Never access
Defaults[.] directly — use settings wrappers
- Use
PluginEventBus for cross-plugin communication
Core Patterns
Plugin Protocol Design
@MainActor
protocol NotchPlugin: Identifiable, Observable, AnyObject {
var id: String { get }
var metadata: PluginMetadata { get }
var isEnabled: Bool { get set }
var state: PluginState { get }
func activate(context: PluginContext) async throws
func deactivate() async
@ViewBuilder
func closedNotchContent() -> AnyView?
}
Dependency Injection (No Singletons)
class MyView: View {
@StateObject private var vm = NotchViewModel.shared
}
struct MyView: View {
@Environment(\.serviceContainer) private var services
let viewModel: MyViewModel
init(viewModel: MyViewModel) {
self.viewModel = viewModel
}
}
@Observable + @MainActor
@MainActor
class OldViewModel: ObservableObject {
@Published var data: String = ""
}
@MainActor
@Observable
final class NewViewModel {
var data: String = ""
private let service: DataService
init(service: DataService) {
self.service = service
}
}
Service Protocol Pattern
protocol DataService: Sendable {
func fetch() async throws -> Data
}
@MainActor
final class DataServiceImpl: DataService {
func fetch() async throws -> Data {
}
}
Type Erasure for Protocols
@MainActor
struct AnyNotchPlugin: Identifiable {
let id: String
private let _activate: (PluginContext) async throws -> Void
init<P: NotchPlugin>(_ plugin: P) {
self.id = plugin.id
self._activate = { try await plugin.activate(context: $0) }
}
func activate(context: PluginContext) async throws {
try await _activate(context)
}
}
Swift 6 Concurrency Patterns
Sendable Conformance
struct Config: Sendable {
let name: String
let value: Int
}
final class Cache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
func get(_ key: String) -> Data? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
}
Actor Isolation
actor DataStore {
private var items: [Item] = []
func add(_ item: Item) {
items.append(item)
}
func allItems() -> [Item] {
items
}
}
let store = DataStore()
await store.add(newItem)
let items = await store.allItems()
MainActor for UI
@MainActor
final class ViewModel {
var text: String = ""
func updateFromBackground() async {
let result = await backgroundTask()
self.text = result
}
}
macOS SwiftUI Specifics
Window Management
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
}
}
MenuBar Extra
MenuBarExtra("App", systemImage: "icon") {
ContentView()
}
.menuBarExtraStyle(.window)
NSView Integration
struct NativeView: NSViewRepresentable {
func makeNSView(context: Context) -> SomeNSView {
let view = SomeNSView()
return view
}
func updateNSView(_ nsView: SomeNSView, context: Context) {}
}
Common Refactoring Patterns
Eliminating Singletons
- Extract protocol from the singleton class
- Create implementation conforming to protocol
- Inject via init or environment
- Update all call sites — find with grep:
\.shared
Settings Access Pattern
Defaults[.showIcon] = true
@MainActor
final class NotchSettings {
var showIcon: Bool {
get { Defaults[.showIcon] }
set { Defaults[.showIcon] = newValue }
}
}
@Environment(\.settings) private var settings
Event Bus Communication
PluginEventBus.shared.emit(.sneakPeekRequested, from: pluginId, payload: data)
eventBus.on(.sneakPeekRequested) { event in
}
LSP Integration (On-Demand)
Use the bundled LSP script for code intelligence when needed. Requires npx (comes with Node.js) — tsx will be auto-installed on first run.
~/.pi/skills/swift/scripts/lsp.ts status
~/.pi/skills/swift/scripts/lsp.ts goto ./MyFile.swift 42 15
~/.pi/skills/swift/scripts/lsp.ts hover ./MyFile.swift 42 15
The LSP client auto-detects:
- SourceKit-LSP location (Xcode, Homebrew, system)
- Workspace root (Package.swift or .xcodeproj)
- Swift files in the project
Note: First run installs tsx via npx (may take 10-15s). Subsequent runs are instant.
Build & Debug
Build Command
xcodebuild -project Apps/machNotch/machNotch.xcodeproj -scheme machNotch -destination 'platform=macOS' build 2>&1 | tail -50
Test Command
xcodebuild -project Apps/machNotch/machNotch.xcodeproj -scheme machNotch -destination 'platform=macOS' test 2>&1 | tail -50
Common Build Errors
| Error | Likely Cause | Fix |
|---|
Cannot find 'X' in scope | Missing import or deleted file | Check imports, file targets |
Cannot convert value of type | Type mismatch in async context | Add await, check isolation |
Call can throw, but... | Missing try/await | Add try await |
Main actor-isolated... | Accessing @MainActor from non-isolated | Move to @MainActor context |
Sending 'X' risks causing data races | Non-Sendable crossing isolation | Make Sendable or use actor |
References
Verification Checklist
Before claiming a Swift refactor is complete: