
Macos Development
- 1.2k installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
macos-development is an agent skill for SwiftUI macOS app development in Xcode.
About
The macos-development skill guides native macOS application development using SwiftUI, Xcode project conventions, and Apple Human Interface Guidelines. It covers window management, menu bar integration, sandbox entitlements, and distribution considerations for Mac App Store versus direct distribution. Agents apply SwiftUI layout patterns suited to resizable desktop windows, keyboard shortcuts, and accessibility identifiers for macOS targets. The skill triggers on Mac-specific UI tasks, Xcode build issues, and platform APIs that differ from iOS counterparts.
- SwiftUI and Xcode patterns for native macOS apps.
- Window management, menus, and sandbox entitlements.
- Apple Human Interface Guidelines alignment.
- Mac App Store versus direct distribution notes.
- Desktop accessibility and keyboard shortcut conventions.
Macos Development by the numbers
- 1,232 all-time installs (skills.sh)
- +51 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #201 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
What macos-development says it does
macOS development
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill macos-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.2k |
|---|---|
| repo stars | ★ 591 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
How should I implement this feature for macOS with SwiftUI and HIG compliance?
Build macOS apps with SwiftUI patterns, Xcode workflows, and Apple HIG alignment.
Who is it for?
Developers building native macOS applications with Claude Code assistance.
Skip if: Skip for iOS-only or cross-platform web apps without macOS targets.
When should I use this skill?
User builds macOS apps, Xcode projects, or Mac-specific SwiftUI features.
What you get
macOS UI and project configuration following SwiftUI and Apple platform conventions.
- Architecture audit document
- UI/UX compatibility report
- Performance profiling summary
By the numbers
- Produces 10 comprehensive analysis documents for existing macOS apps
Files
App Planner for macOS
You are a macOS app architect specializing in project planning and analysis.
When This Skill Activates
- User wants to plan a new macOS app
- User wants to analyze or audit an existing macOS project
- User asks to architect a macOS app
- User wants planning documents for their macOS app
- User asks to review project structure for a macOS app
Your Role
Create comprehensive planning documents for new macOS apps or analyze existing projects.
Core Functions
1. New App Planning - Create 8 planning documents for new projects 2. Existing App Analysis - Create 10 analysis documents for existing apps
Module References
1. New App Planning: skills/app-planner/new-app-planning.md 2. Existing App Analysis: skills/app-planner/existing-app-analysis.md
Approach
- Ask detailed questions about requirements
- Consider macOS 26 Tahoe features
- Recommend SwiftData, SwiftUI where appropriate
- Plan for SOLID/DRY principles
- Consider accessibility and performance
Begin by asking whether this is a new app or existing app analysis.
Existing macOS App Analysis
Create 10 comprehensive analysis documents for existing apps.
1. Current Architecture Audit
- Current tech stack
- Architecture pattern used
- Code organization
- Dependencies
2. Code Quality Assessment
- SOLID compliance
- DRY violations
- Code duplication
- Design patterns used
3. UI/UX Tahoe Compatibility
- Liquid Glass adoption
- HIG compliance
- macOS 26 features used
- Accessibility status
4. Data Layer Analysis
- Current persistence (Core Data, etc.)
- SwiftData migration potential
- Data model review
5. Performance Profiling
- Bottlenecks identified
- Memory usage
- Launch time
- Optimization opportunities
6. Accessibility Audit
- VoiceOver support
- Keyboard navigation
- Dynamic Type
- Color contrast
7. Security and Sandboxing
- Current entitlements
- Security vulnerabilities
- Sandbox compatibility
8. Dependency Analysis
- Third-party dependencies
- Version currency
- Security audits
9. Test Coverage Report
- Current test coverage
- Missing tests
- Quality metrics
10. Modernization Roadmap
- Intel → Apple Silicon
- Core Data → SwiftData
- Objective-C → Swift
- Liquid Glass adoption
- Priority timeline
New macOS App Planning
Create 8 comprehensive planning documents for new macOS projects.
1. Feature Specification
- Core features list
- User stories
- Feature prioritization (MVP vs future)
- Target audience
- Success criteria
2. Architecture Decision
- SwiftUI vs AppKit vs Hybrid
- MVVM architecture
- Modular design approach
- SwiftData for persistence
- Dependency injection strategy
3. Data Model Design
- SwiftData models and relationships
- Schema design
- Migration strategy
- CloudKit sync (if needed)
4. UI/UX Wireframes
- Window structure
- Navigation pattern (sidebar, tabs)
- Liquid Glass design integration
- Accessibility considerations
- Responsive layouts
5. Technology Stack
- Swift 6
- SwiftUI/AppKit
- SwiftData
- Frameworks needed
- Third-party dependencies
6. Project Structure
- Feature-based modules
- Swift Package organization
- File structure
- Testing strategy
7. Testing Strategy
- Unit tests
- UI tests
- Integration tests
- Test coverage goals
8. Distribution Plan
- Mac App Store vs direct
- Code signing
- Notarization
- Update mechanism
Hosting Controllers
Embedding SwiftUI views inside AppKit applications using NSHostingView and NSHostingController. This is the primary pattern for incrementally adopting SwiftUI in existing AppKit apps.
NSHostingView
Wraps a SwiftUI view as an NSView. Use when you need to embed SwiftUI in an existing NSView hierarchy.
Basic Usage
import SwiftUI
let swiftUIView = MySwiftUIView(viewModel: viewModel)
let hostingView = NSHostingView(rootView: swiftUIView)
// Add to existing view hierarchy
parentView.addSubview(hostingView)
// With Auto Layout
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
hostingView.trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
hostingView.topAnchor.constraint(equalTo: parentView.topAnchor),
hostingView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor)
])Sizing Behavior
NSHostingView calculates its intrinsicContentSize from the SwiftUI view. Control this with sizingOptions (macOS 13+):
let hostingView = NSHostingView(rootView: myView)
// Default: hosting view has intrinsic size from SwiftUI content
hostingView.sizingOptions = .intrinsicContentSize
// Prefer minimal size (useful for fixed-size badges, indicators)
hostingView.sizingOptions = .minSize
// Both (most flexible)
hostingView.sizingOptions = [.intrinsicContentSize, .minSize]Updating the Root View
When your data model changes, update the hosted view:
// With @Observable (macOS 14+) - automatic updates, no manual refresh needed
@Observable class ViewModel {
var title = "Hello"
}
let viewModel = ViewModel()
let hostingView = NSHostingView(rootView: ContentView(viewModel: viewModel))
// Changes to viewModel.title automatically update the hosted SwiftUI view
// Without @Observable - manually update rootView
hostingView.rootView = MySwiftUIView(updatedData: newData)NSHostingController
Wraps a SwiftUI view as an NSViewController. Use when you need a full view controller (e.g., in NSSplitViewController, tab views, sheets).
Basic Usage
let hostingController = NSHostingController(rootView: SettingsView())
// Present as sheet
parentViewController.presentAsSheet(hostingController)
// Add as child view controller
parentVC.addChild(hostingController)
parentVC.view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: parentVC.view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: parentVC.view.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: parentVC.view.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: parentVC.view.bottomAnchor)
])Window Management
Create a new window with SwiftUI content:
func showSwiftUIWindow() {
let hostingController = NSHostingController(rootView: DetailView())
let window = NSWindow(contentViewController: hostingController)
window.title = "Detail"
window.setContentSize(NSSize(width: 600, height: 400))
window.styleMask = [.titled, .closable, .resizable, .miniaturizable]
window.center()
window.makeKeyAndOrderFront(nil)
// Retain the window controller
let windowController = NSWindowController(window: window)
windowController.showWindow(nil)
}In NSSplitViewController
class MainSplitViewController: NSSplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Sidebar in SwiftUI
let sidebarItem = NSSplitViewItem(
sidebarWithViewController: NSHostingController(rootView: SidebarView())
)
sidebarItem.minimumThickness = 200
sidebarItem.canCollapse = true
// Content in SwiftUI
let contentItem = NSSplitViewItem(
viewController: NSHostingController(rootView: ContentView())
)
contentItem.minimumThickness = 300
addSplitViewItem(sidebarItem)
addSplitViewItem(contentItem)
}
}Incremental Adoption Strategy
Phase 1: Leaf Views
Start by replacing simple, self-contained views:
- Settings panels
- Detail views
- Empty states
- Status indicators
// Replace an AppKit detail view with SwiftUI
class DetailViewController: NSViewController {
private var hostingView: NSHostingView<DetailSwiftUIView>!
override func loadView() {
let swiftUIView = DetailSwiftUIView(item: item)
hostingView = NSHostingView(rootView: swiftUIView)
self.view = hostingView
}
}Phase 2: Container Views
Move to views that contain other views:
- Tab containers
- Split view panels
- List/detail patterns
Phase 3: Window-Level
Eventually host entire windows in SwiftUI:
- New windows as SwiftUI
WindowGroup - Settings via SwiftUI
Settingsscene - Menu bar with
MenuBarExtra
Phase 4: Full Migration
- Replace the App Delegate entry point with SwiftUI
@main App - Use
NSApplicationDelegateAdaptorfor remaining AppKit lifecycle needs
@main
struct MyApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
Settings {
SettingsView()
}
}
}SwiftUI Environment in Hosted Views
Hosted SwiftUI views have access to the full SwiftUI environment:
let hostingController = NSHostingController(
rootView: MyView()
.environment(\.managedObjectContext, persistentContainer.viewContext)
.environment(appState)
)Toolbar Integration
NSHostingController integrates with NSWindow toolbars. Use SwiftUI's .toolbar modifier:
struct ContentView: View {
var body: some View {
MainContent()
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Add", systemImage: "plus") { }
}
}
}
}
// When hosted in NSHostingController, toolbar items appear in the window toolbarBest Practices
1. Use @Observable for shared state - Automatic updates across the bridge (macOS 14+) 2. Set sizing options explicitly - Don't rely on default sizing for complex layouts 3. Adopt incrementally - Start with leaf views, work up to containers 4. Keep the bridge thin - Don't build complex logic at the boundary 5. Use NSHostingController for view controller contexts - Sheets, split views, tab views 6. Use NSHostingView for view-level embedding - Cells, decorations, inline content 7. Test resizing behavior - Verify hosted views respond correctly to window resizing
NSViewRepresentable
Wrapping AppKit views for use in SwiftUI. This is the primary mechanism for using AppKit views within a SwiftUI view hierarchy.
Protocol Requirements
struct MyAppKitView: NSViewRepresentable {
// 1. Create the AppKit view
func makeNSView(context: Context) -> NSTextField {
let textField = NSTextField()
textField.delegate = context.coordinator
return textField
}
// 2. Update when SwiftUI state changes
func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.stringValue = text
}
// 3. Optional: Provide a coordinator for delegation
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// 4. Optional: Clean up resources
static func dismantleNSView(_ nsView: NSTextField, coordinator: Coordinator) {
// Remove observers, cancel timers, etc.
}
// 5. Optional: Control sizing (macOS 13+)
func sizeThatFits(_ proposal: ProposedViewSize, nsView: NSTextField, context: Context) -> CGSize? {
nsView.intrinsicContentSize
}
}The Coordinator Pattern
The coordinator is a long-lived object that survives SwiftUI view re-creation. Use it for:
- Delegation: AppKit delegate callbacks
- Target-action: Button targets, gesture recognizers
- Observation: KVO, NotificationCenter
class Coordinator: NSObject, NSTextFieldDelegate {
var parent: MyAppKitView
var observation: NSKeyValueObservation?
init(_ parent: MyAppKitView) {
self.parent = parent
}
func controlTextDidChange(_ obj: Notification) {
guard let textField = obj.object as? NSTextField else { return }
parent.text = textField.stringValue
}
}Updating the Parent Reference
The coordinator's parent reference becomes stale when SwiftUI recreates the view struct. Update it in updateNSView:
func updateNSView(_ nsView: NSTextField, context: Context) {
context.coordinator.parent = self // Keep parent reference fresh
nsView.stringValue = text
}Wrapping Common AppKit Views
NSTextView (Rich Text Editing)
struct RichTextEditor: NSViewRepresentable {
@Binding var attributedText: NSAttributedString
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSTextView.scrollableTextView()
let textView = scrollView.documentView as! NSTextView
textView.isRichText = true
textView.allowsUndo = true
textView.delegate = context.coordinator
textView.textStorage?.setAttributedString(attributedText)
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
guard let textView = scrollView.documentView as? NSTextView else { return }
context.coordinator.parent = self
if textView.textStorage?.attributedString() != attributedText {
textView.textStorage?.setAttributedString(attributedText)
}
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, NSTextViewDelegate {
var parent: RichTextEditor
init(_ parent: RichTextEditor) { self.parent = parent }
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView,
let storage = textView.textStorage else { return }
parent.attributedText = NSAttributedString(attributedString: storage)
}
}
}NSTableView (High-Performance Lists)
Use when List or Table performance is insufficient (100k+ rows):
struct HighPerformanceList: NSViewRepresentable {
let items: [ListItem]
var onSelect: (ListItem) -> Void
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSScrollView()
let tableView = NSTableView()
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("main"))
column.title = "Items"
tableView.addTableColumn(column)
tableView.headerView = nil
tableView.style = .plain
tableView.dataSource = context.coordinator
tableView.delegate = context.coordinator
scrollView.documentView = tableView
scrollView.hasVerticalScroller = true
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
context.coordinator.parent = self
(scrollView.documentView as? NSTableView)?.reloadData()
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, NSTableViewDataSource, NSTableViewDelegate {
var parent: HighPerformanceList
init(_ parent: HighPerformanceList) { self.parent = parent }
func numberOfRows(in tableView: NSTableView) -> Int {
parent.items.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: nil) as? NSTextField
?? NSTextField(labelWithString: "")
cell.identifier = tableColumn!.identifier
cell.stringValue = parent.items[row].title
return cell
}
func tableViewSelectionDidChange(_ notification: Notification) {
guard let tableView = notification.object as? NSTableView else { return }
let row = tableView.selectedRow
guard row >= 0 else { return }
parent.onSelect(parent.items[row])
}
}
}Drag and Drop
struct DragDropView: NSViewRepresentable {
var onDrop: ([URL]) -> Void
func makeNSView(context: Context) -> NSView {
let view = DropTargetView()
view.coordinator = context.coordinator
view.registerForDraggedTypes([.fileURL])
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
context.coordinator.parent = self
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class DropTargetView: NSView {
weak var coordinator: Coordinator?
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { .copy }
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
guard let urls = sender.draggingPasteboard.readObjects(forClasses: [NSURL.self]) as? [URL] else {
return false
}
coordinator?.parent.onDrop(urls)
return true
}
}
class Coordinator: NSObject {
var parent: DragDropView
init(_ parent: DragDropView) { self.parent = parent }
}
}Layout Integration
sizeThatFits (macOS 13+)
Control how the wrapped view participates in SwiftUI layout:
func sizeThatFits(_ proposal: ProposedViewSize, nsView: NSTextField, context: Context) -> CGSize? {
// Return nil to use SwiftUI's default sizing
// Return a size to override
// Respect proposed width, calculate height
let width = proposal.width ?? nsView.intrinsicContentSize.width
let height = nsView.intrinsicContentSize.height
return CGSize(width: width, height: height)
}Intrinsic Content Size
For custom NSView subclasses, override intrinsicContentSize:
class CustomView: NSView {
override var intrinsicContentSize: NSSize {
NSSize(width: NSView.noIntrinsicMetric, height: 44)
}
}Animation Context
Access SwiftUI animation state in updateNSView:
func updateNSView(_ nsView: NSView, context: Context) {
if context.transaction.animation != nil {
NSAnimationContext.runAnimationGroup { animContext in
animContext.duration = 0.25
animContext.allowsImplicitAnimation = true
nsView.animator().alphaValue = isVisible ? 1.0 : 0.0
}
} else {
nsView.alphaValue = isVisible ? 1.0 : 0.0
}
}Best Practices
1. Keep makeNSView minimal - Only create and configure. Don't set data. 2. Guard against redundant updates - Check if values actually changed in updateNSView 3. Always clean up in dismantleNSView - Remove observers, invalidate timers, cancel tasks 4. Update coordinator.parent - Refresh the parent reference in updateNSView 5. Use sizeThatFits for layout - Don't set frames manually; let SwiftUI handle layout 6. Avoid storing state in the coordinator - Use @Binding and @State in the parent
State Management Across Frameworks
Bridging state between AppKit and SwiftUI. The key challenge is keeping both sides synchronized without retain cycles or stale data.
Approach 1: @Observable (Recommended, macOS 14+)
The simplest and most modern approach. Both AppKit and SwiftUI can observe the same @Observable class.
@Observable
class AppState {
var currentDocument: Document?
var isEditing = false
var statusMessage = ""
}SwiftUI Side
struct ContentView: View {
var appState: AppState
var body: some View {
VStack {
if let doc = appState.currentDocument {
DocumentView(document: doc)
}
Text(appState.statusMessage)
.foregroundStyle(.secondary)
}
}
}AppKit Side
Use withObservationTracking to react to changes:
class AppKitController: NSViewController {
let appState: AppState
private var isObserving = true
init(appState: AppState) {
self.appState = appState
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
observeState()
}
private func observeState() {
guard isObserving else { return }
withObservationTracking {
// Access properties you want to observe
let message = appState.statusMessage
updateStatusBar(message)
} onChange: {
// Re-observe on next change (must re-register)
DispatchQueue.main.async { [weak self] in
self?.observeState()
}
}
}
deinit {
isObserving = false
}
}Hosting with @Observable
let appState = AppState()
// SwiftUI side - pass as environment
let hostingView = NSHostingView(
rootView: ContentView()
.environment(appState)
)
// AppKit side - use the same instance
let appKitController = AppKitController(appState: appState)
// Changes from either side propagate automatically
appState.statusMessage = "Updated from AppKit" // SwiftUI view updatesApproach 2: Combine (macOS 10.15+)
Use Combine publishers for cross-framework communication when targeting older macOS versions.
Shared ViewModel with Combine
class SharedViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var selectedItemID: UUID?
@Published var isLoading = false
}SwiftUI Side
struct ItemListView: View {
@ObservedObject var viewModel: SharedViewModel
var body: some View {
List(viewModel.items, selection: $viewModel.selectedItemID) { item in
Text(item.name)
}
}
}AppKit Side
class AppKitSidebarController: NSViewController {
let viewModel: SharedViewModel
private var cancellables = Set<AnyCancellable>()
init(viewModel: SharedViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$selectedItemID
.receive(on: DispatchQueue.main)
.sink { [weak self] selectedID in
self?.highlightItem(selectedID)
}
.store(in: &cancellables)
viewModel.$items
.receive(on: DispatchQueue.main)
.sink { [weak self] items in
self?.reloadTable(with: items)
}
.store(in: &cancellables)
}
func userSelectedItem(_ id: UUID) {
viewModel.selectedItemID = id // SwiftUI view updates automatically
}
}Approach 3: NotificationCenter
Best for loosely coupled, fire-and-forget communication between distant parts of the app.
// Define notification names
extension Notification.Name {
static let documentDidSave = Notification.Name("documentDidSave")
static let themeDidChange = Notification.Name("themeDidChange")
}
// AppKit posts
NotificationCenter.default.post(
name: .documentDidSave,
object: nil,
userInfo: ["documentID": document.id]
)
// SwiftUI receives
struct ContentView: View {
var body: some View {
Text("Content")
.onReceive(NotificationCenter.default.publisher(for: .documentDidSave)) { notification in
if let docID = notification.userInfo?["documentID"] as? UUID {
handleSave(docID)
}
}
}
}Approach 4: Shared UserDefaults / App Storage
For simple preferences shared between both frameworks:
// SwiftUI side
@AppStorage("sidebarWidth") private var sidebarWidth: Double = 250
// AppKit side
UserDefaults.standard.addObserver(self, forKeyPath: "sidebarWidth", context: nil)
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "sidebarWidth" {
let width = UserDefaults.standard.double(forKey: "sidebarWidth")
updateSidebarWidth(width)
}
}Approach 5: NSResponder Chain
Pass actions up through the responder chain from SwiftUI to AppKit:
// SwiftUI sends an action up the responder chain
struct ToolbarView: View {
var body: some View {
Button("Save") {
NSApp.sendAction(#selector(DocumentController.saveDocument(_:)), to: nil, from: nil)
}
}
}
// AppKit receives via responder chain
class DocumentController: NSDocumentController {
@objc func saveDocument(_ sender: Any?) {
currentDocument?.save(nil)
}
}Choosing the Right Approach
| Approach | macOS Version | Coupling | Best For |
|---|---|---|---|
| @Observable | 14+ | Tight | Shared view models, closely related views |
| Combine | 10.15+ | Medium | Reactive data streams, async updates |
| NotificationCenter | Any | Loose | Cross-module events, fire-and-forget |
| UserDefaults | Any | Loose | Simple preferences, settings |
| Responder Chain | Any | Loose | Menu actions, commands |
Common Mistakes
Retain Cycles
// Wrong - strong reference cycle
class Coordinator: NSObject {
let viewModel: SharedViewModel
var cancellable: AnyCancellable?
init(viewModel: SharedViewModel) {
self.viewModel = viewModel
cancellable = viewModel.$items.sink { items in
self.update(items) // Strong capture of self!
}
}
}
// Right - weak capture
cancellable = viewModel.$items.sink { [weak self] items in
self?.update(items)
}Thread Safety
// Wrong - updating UI from background thread
viewModel.$data
.sink { data in
self.tableView.reloadData() // May be on background thread!
}
// Right - ensure main thread
viewModel.$data
.receive(on: DispatchQueue.main)
.sink { [weak self] data in
self?.tableView.reloadData()
}Stale Coordinator References
// Wrong - coordinator captures old parent struct
func makeCoordinator() -> Coordinator {
Coordinator(text: text) // Captures current value, never updates
}
// Right - coordinator references parent, updated in updateNSView
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
func updateNSView(_ nsView: NSView, context: Context) {
context.coordinator.parent = self // Keep reference fresh
}Best Practices
1. Use @Observable for macOS 14+ - Simplest approach, works automatically across both frameworks 2. Avoid mixing approaches - Pick one primary mechanism per data flow 3. Always use weak self in closures - Prevent retain cycles in Combine sinks and callbacks 4. Dispatch to main thread - All UI updates must happen on the main thread 5. Clean up subscriptions - Cancel Combine subscriptions and remove observers in deinit/dismantle 6. Keep shared state minimal - Only share what both frameworks actually need 7. Test bidirectional updates - Verify changes from either side propagate correctly
Design Patterns for macOS
Common design patterns implemented in modern Swift for macOS applications. Each pattern includes when to use it, implementation, and real-world examples.
MVVM (Model-View-ViewModel)
The primary pattern for SwiftUI macOS apps. The ViewModel owns business logic and exposes state the View observes.
Implementation with @Observable (macOS 14+)
// Model
struct Task: Identifiable, Codable {
let id: UUID
var title: String
var isCompleted: Bool
var dueDate: Date?
}
// ViewModel
@Observable class TaskListViewModel {
private let repository: TaskRepository
var tasks: [Task] = []
var filterOption: FilterOption = .all
var errorMessage: String?
var filteredTasks: [Task] {
switch filterOption {
case .all: tasks
case .active: tasks.filter { !$0.isCompleted }
case .completed: tasks.filter { $0.isCompleted }
}
}
init(repository: TaskRepository) {
self.repository = repository
}
func loadTasks() async {
do {
tasks = try await repository.fetchAll()
} catch {
errorMessage = error.localizedDescription
}
}
func toggleCompletion(_ task: Task) async {
guard var updated = tasks.first(where: { $0.id == task.id }) else { return }
updated.isCompleted.toggle()
do {
try await repository.save(updated)
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
tasks[index] = updated
}
} catch {
errorMessage = error.localizedDescription
}
}
}
// View
struct TaskListView: View {
@State private var viewModel: TaskListViewModel
init(repository: TaskRepository) {
_viewModel = State(initialValue: TaskListViewModel(repository: repository))
}
var body: some View {
List(viewModel.filteredTasks) { task in
TaskRow(task: task) {
Task { await viewModel.toggleCompletion(task) }
}
}
.task { await viewModel.loadTasks() }
}
}When to Use MVVM
- Any SwiftUI app with business logic beyond simple data display
- When you need testable business logic separate from the view
- When multiple views share the same state transformations
Repository Pattern
Abstracts data access behind a protocol. The ViewModel doesn't know if data comes from a database, network, or cache.
protocol TaskRepository {
func fetchAll() async throws -> [Task]
func fetch(id: UUID) async throws -> Task?
func save(_ task: Task) async throws
func delete(_ task: Task) async throws
}
// SwiftData implementation
struct SwiftDataTaskRepository: TaskRepository {
let modelContext: ModelContext
func fetchAll() async throws -> [Task] {
let descriptor = FetchDescriptor<TaskModel>(sortBy: [SortDescriptor(\.dueDate)])
return try modelContext.fetch(descriptor).map(\.toTask)
}
func save(_ task: Task) async throws {
if let existing = try await fetch(id: task.id) as? TaskModel {
existing.update(from: task)
} else {
modelContext.insert(TaskModel(from: task))
}
try modelContext.save()
}
func fetch(id: UUID) async throws -> Task? {
let descriptor = FetchDescriptor<TaskModel>(predicate: #Predicate { $0.id == id })
return try modelContext.fetch(descriptor).first?.toTask
}
func delete(_ task: Task) async throws {
let descriptor = FetchDescriptor<TaskModel>(predicate: #Predicate { $0.id == task.id })
if let model = try modelContext.fetch(descriptor).first {
modelContext.delete(model)
try modelContext.save()
}
}
}
// In-memory implementation for tests and previews
struct InMemoryTaskRepository: TaskRepository {
var tasks: [Task] = []
func fetchAll() async throws -> [Task] { tasks }
func fetch(id: UUID) async throws -> Task? { tasks.first { $0.id == id } }
mutating func save(_ task: Task) async throws {
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
tasks[index] = task
} else {
tasks.append(task)
}
}
mutating func delete(_ task: Task) async throws {
tasks.removeAll { $0.id == task.id }
}
}When to Use Repository
- Apps with persistence (SwiftData, Core Data, files)
- When you need to swap data sources (network vs. local)
- When you want testable data access without real databases
Factory Pattern
Creates objects without exposing creation logic. Useful for building complex objects with varying configurations.
// Protocol for the product
protocol AlertPresenter {
func show(title: String, message: String)
}
// Factory
enum AlertPresenterFactory {
static func make(for context: AlertContext) -> AlertPresenter {
switch context {
case .modal:
return ModalAlertPresenter()
case .notification:
return NotificationAlertPresenter()
case .statusBar:
return StatusBarAlertPresenter()
}
}
}
// More practical: ViewModel factory with dependencies
enum ViewModelFactory {
static func makeTaskList(modelContext: ModelContext) -> TaskListViewModel {
let repository = SwiftDataTaskRepository(modelContext: modelContext)
return TaskListViewModel(repository: repository)
}
static func makeSettings(store: UserDefaults = .standard) -> SettingsViewModel {
let preferences = UserDefaultsPreferences(store: store)
return SettingsViewModel(preferences: preferences)
}
}When to Use Factory
- Complex object creation with multiple dependencies
- When creation logic varies based on context or configuration
- Centralizing dependency wiring
Observer Pattern
Built into Swift via Combine, @Observable, and NotificationCenter. Choose the right mechanism for the coupling level.
@Observable (Tight Coupling, macOS 14+)
@Observable class DownloadManager {
var activeDownloads: [Download] = []
var totalProgress: Double = 0.0
var isDownloading: Bool { !activeDownloads.isEmpty }
}
// Views automatically observe accessed properties
struct DownloadStatusView: View {
var manager: DownloadManager
var body: some View {
if manager.isDownloading {
ProgressView(value: manager.totalProgress)
}
}
}Combine (Medium Coupling)
class FileWatcher {
let fileChanged = PassthroughSubject<URL, Never>()
func startWatching(_ directory: URL) {
// FSEvents or DispatchSource monitoring
// When file changes:
fileChanged.send(changedURL)
}
}
// Consumer
class EditorController {
private var cancellables = Set<AnyCancellable>()
init(watcher: FileWatcher) {
watcher.fileChanged
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { [weak self] url in
self?.reloadFile(at: url)
}
.store(in: &cancellables)
}
}When to Use Which Observer Mechanism
| Mechanism | Coupling | Use Case |
|---|---|---|
| @Observable | Tight | ViewModel-to-View data binding |
| Combine | Medium | Async streams, data transformation pipelines |
| NotificationCenter | Loose | App-wide events, system notifications |
| AsyncSequence | Medium | Streaming data, server-sent events |
Coordinator Pattern
Manages navigation flow, keeping ViewModels free of navigation logic. Most useful in large apps with complex flows.
@Observable class AppCoordinator {
var selectedTab: Tab = .documents
var navigationPath = NavigationPath()
var presentedSheet: SheetDestination?
var presentedAlert: AlertDestination?
func showDocument(_ document: Document) {
selectedTab = .documents
navigationPath.append(document)
}
func showSettings() {
presentedSheet = .settings
}
func showDeleteConfirmation(for document: Document) {
presentedAlert = .deleteConfirmation(document)
}
func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
switch components.host {
case "document":
if let id = components.queryItems?.first(where: { $0.name == "id" })?.value {
navigationPath.append(DocumentRoute(id: id))
}
case "settings":
showSettings()
default:
break
}
}
}
// Usage in the root view
struct ContentView: View {
@State private var coordinator = AppCoordinator()
var body: some View {
TabView(selection: $coordinator.selectedTab) {
NavigationStack(path: $coordinator.navigationPath) {
DocumentListView()
.navigationDestination(for: Document.self) { doc in
DocumentDetailView(document: doc)
}
}
.tag(Tab.documents)
}
.sheet(item: $coordinator.presentedSheet) { destination in
switch destination {
case .settings: SettingsView()
}
}
.environment(coordinator)
}
}When to Use Coordinator
- Apps with complex navigation flows (onboarding, multi-step forms)
- Deep linking support
- When navigation logic is cluttering ViewModels
- Multi-window macOS apps
Service Locator (Lightweight DI)
For small-to-medium apps where a full DI container is overkill.
@Observable class ServiceContainer {
lazy var taskRepository: TaskRepository = SwiftDataTaskRepository(modelContext: modelContext)
lazy var analytics: AnalyticsTracking = FirebaseTracker()
lazy var networkMonitor: NetworkMonitor = SystemNetworkMonitor()
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
}
// Inject via SwiftUI environment
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(ServiceContainer(modelContext: modelContext))
}
}
}
// Consume in views
struct TaskListView: View {
@Environment(ServiceContainer.self) private var services
var body: some View {
TaskListContent(repository: services.taskRepository)
}
}Choosing the Right Pattern
| Scenario | Recommended Patterns |
|---|---|
| Simple CRUD app | MVVM + Repository |
| Multi-screen app with navigation | MVVM + Repository + Coordinator |
| App with swappable backends | Repository + Factory |
| Plugin/extension architecture | Factory + Observer |
| Menu bar utility | MVVM (lightweight, single ViewModel) |
Modular Design
Swift Package Manager organization, feature modules, and code organization strategies for macOS applications.
Project Organization Strategies
Strategy 1: Group by Feature (Recommended for Most Apps)
MyApp/
├── Features/
│ ├── Documents/
│ │ ├── DocumentListView.swift
│ │ ├── DocumentDetailView.swift
│ │ ├── DocumentViewModel.swift
│ │ └── DocumentModel.swift
│ ├── Settings/
│ │ ├── SettingsView.swift
│ │ ├── GeneralSettingsTab.swift
│ │ ├── AppearanceSettingsTab.swift
│ │ └── SettingsViewModel.swift
│ └── Search/
│ ├── SearchView.swift
│ ├── SearchResultRow.swift
│ └── SearchViewModel.swift
├── Core/
│ ├── Models/
│ │ └── SharedModels.swift
│ ├── Services/
│ │ ├── PersistenceService.swift
│ │ └── NetworkService.swift
│ └── Extensions/
│ └── Date+Formatting.swift
├── App/
│ ├── MyApp.swift
│ ├── ContentView.swift
│ └── AppState.swift
└── Resources/
└── Assets.xcassetsPros: Related files together, easy to find things, scales well Cons: Shared dependencies can create circular references
Strategy 2: Group by Layer
MyApp/
├── Views/
│ ├── DocumentListView.swift
│ ├── DocumentDetailView.swift
│ ├── SettingsView.swift
│ └── SearchView.swift
├── ViewModels/
│ ├── DocumentViewModel.swift
│ ├── SettingsViewModel.swift
│ └── SearchViewModel.swift
├── Models/
│ ├── Document.swift
│ ├── Settings.swift
│ └── SearchResult.swift
├── Services/
│ ├── DocumentRepository.swift
│ └── SearchService.swift
└── App/
└── MyApp.swiftPros: Clear separation of concerns, familiar to MVVM developers Cons: Related files scattered across folders, doesn't scale as well
Recommendation
- Small apps (< 15 files): Group by layer — simpler, less folder nesting
- Medium+ apps (15+ files): Group by feature — better discoverability and modularity
Swift Package Manager Modularization
For large apps, extract code into local Swift packages for build isolation, clear API boundaries, and faster incremental builds.
Package Structure
MyApp/
├── MyApp.xcodeproj
├── MyApp/ # App target (thin shell)
│ ├── MyApp.swift
│ └── ContentView.swift
└── Packages/
├── Core/ # Shared models, protocols, utilities
│ ├── Package.swift
│ └── Sources/Core/
│ ├── Models/
│ ├── Protocols/
│ └── Extensions/
├── DocumentFeature/ # Document management feature
│ ├── Package.swift
│ └── Sources/DocumentFeature/
│ ├── DocumentListView.swift
│ ├── DocumentDetailView.swift
│ └── DocumentViewModel.swift
├── Persistence/ # Data layer
│ ├── Package.swift
│ └── Sources/Persistence/
│ ├── SwiftDataModels/
│ └── Repositories/
└── Networking/ # Network layer
├── Package.swift
└── Sources/Networking/
├── APIClient.swift
└── Endpoints/Package.swift for a Feature Module
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "DocumentFeature",
platforms: [.macOS(.v14)],
products: [
.library(name: "DocumentFeature", targets: ["DocumentFeature"]),
],
dependencies: [
.package(path: "../Core"),
.package(path: "../Persistence"),
],
targets: [
.target(
name: "DocumentFeature",
dependencies: ["Core", "Persistence"]
),
.testTarget(
name: "DocumentFeatureTests",
dependencies: ["DocumentFeature"]
),
]
)Dependency Graph Rules
1. Core depends on nothing — shared models, protocols, utilities 2. Service layers (Persistence, Networking) depend on Core only 3. Feature modules depend on Core and relevant service layers 4. App target depends on all feature modules and wires them together 5. No circular dependencies — if two modules need each other, extract the shared part into Core
App → DocumentFeature → Persistence → Core
→ SettingsFeature → Core
→ SearchFeature → Networking → CoreAccess Control for Module Boundaries
Use Swift's access levels to enforce clean module APIs:
// In Persistence module
// Public: API surface used by other modules
public protocol TaskRepository: Sendable {
func fetchAll() async throws -> [Task]
func save(_ task: Task) async throws
}
// Public: consumers need to create this
public struct SwiftDataTaskRepository: TaskRepository {
public init(modelContext: ModelContext) { ... }
public func fetchAll() async throws -> [Task] { ... }
public func save(_ task: Task) async throws { ... }
}
// Internal: implementation detail, not visible outside module
struct CacheManager {
func invalidate() { ... }
}
// Package: visible to other targets in same package, not outside
package struct MigrationHelper {
package func migrate(from oldSchema: Schema) { ... }
}Access Level Summary
| Level | Visible To |
|---|---|
private | Enclosing declaration only |
fileprivate | Same source file |
internal (default) | Same module/target |
package | Same package (Swift 5.9+) |
public | Any importing module |
open | Any module (can subclass/override) |
DRY: Reducing Duplication
Extract Shared Views
// Reusable components in Core or a shared UI module
struct EmptyStateView: View {
let title: String
let systemImage: String
let description: String
var body: some View {
ContentUnavailableView(title, systemImage: systemImage, description: Text(description))
}
}
struct LoadingOverlay: View {
let isLoading: Bool
var body: some View {
if isLoading {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.ultraThinMaterial)
}
}
}Extract Common Logic into Extensions
// Instead of duplicating date formatting across features
extension Date {
var relativeDisplay: String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .abbreviated
return formatter.localizedString(for: self, relativeTo: .now)
}
var shortDisplay: String {
formatted(date: .abbreviated, time: .shortened)
}
}Protocol with Default Implementations
protocol Searchable {
var searchableText: String { get }
}
extension Array where Element: Searchable {
func search(query: String) -> [Element] {
guard !query.isEmpty else { return self }
let lowered = query.lowercased()
return filter { $0.searchableText.lowercased().contains(lowered) }
}
}
// Any model can opt-in
struct Document: Searchable {
let title: String
let content: String
var searchableText: String { "\(title) \(content)" }
}
struct Contact: Searchable {
let name: String
let email: String
var searchableText: String { "\(name) \(email)" }
}When to Modularize
| Signal | Action |
|---|---|
| Build times getting slow | Extract stable code into packages |
| Multiple developers working on same files | Split into feature modules |
| Want to share code between app and extension | Extract into shared package |
| Tests require the full app to compile | Extract testable code into packages |
| Hard to find files | Reorganize by feature |
Don't modularize prematurely — start with feature folders in the app target and extract to packages when there's a concrete benefit.
Testing Across Modules
Each package has its own test target with fast, isolated tests:
// In DocumentFeatureTests/
@testable import DocumentFeature
import Core
struct MockTaskRepository: TaskRepository {
var tasks: [Task] = []
func fetchAll() async throws -> [Task] { tasks }
func save(_ task: Task) async throws { }
}
@Test func testFilteredTasks() async {
let repo = MockTaskRepository(tasks: [
Task(id: UUID(), title: "Active", isCompleted: false),
Task(id: UUID(), title: "Done", isCompleted: true),
])
let viewModel = TaskListViewModel(repository: repo)
await viewModel.loadTasks()
viewModel.filterOption = .active
#expect(viewModel.filteredTasks.count == 1)
#expect(viewModel.filteredTasks[0].title == "Active")
}SOLID Principles in Detail
Real-world Swift examples for each SOLID principle with refactoring patterns. Focus on practical application, not academic theory.
Single Responsibility Principle (SRP)
A type should have one reason to change.
Violation
class DocumentManager {
var documents: [Document] = []
// Responsibility 1: Document CRUD
func createDocument(title: String) -> Document { ... }
func deleteDocument(_ doc: Document) { ... }
// Responsibility 2: Persistence
func saveToFile(_ doc: Document) throws { ... }
func loadFromFile(_ url: URL) throws -> Document { ... }
// Responsibility 3: Export
func exportToPDF(_ doc: Document) -> Data { ... }
func exportToHTML(_ doc: Document) -> String { ... }
// Responsibility 4: Search
func search(query: String) -> [Document] { ... }
}Refactored
// Each class has one reason to change
@Observable class DocumentStore {
var documents: [Document] = []
func create(title: String) -> Document { ... }
func delete(_ doc: Document) { ... }
}
struct DocumentPersistence {
func save(_ doc: Document, to url: URL) throws { ... }
func load(from url: URL) throws -> Document { ... }
}
struct DocumentExporter {
func toPDF(_ doc: Document) -> Data { ... }
func toHTML(_ doc: Document) -> String { ... }
}
struct DocumentSearch {
func search(_ documents: [Document], query: String) -> [Document] { ... }
}When to Bend SRP
- Tiny apps: A single ViewModel handling fetch + display is fine for 1-2 screens
- Data models:
@Modelclasses naturally combine data + persistence — that's SwiftData's design - Value types: Small structs with 2-3 responsibilities are often clearer than over-split types
Open/Closed Principle (OCP)
Open for extension, closed for modification.
Use protocols and enums to add behavior without changing existing code.
Violation
class ReportGenerator {
func generate(type: String, data: ReportData) -> String {
switch type {
case "pdf": return generatePDF(data)
case "html": return generateHTML(data)
case "csv": return generateCSV(data)
// Adding "markdown" requires modifying this class
default: fatalError()
}
}
}Refactored
protocol ReportFormat {
func generate(from data: ReportData) -> String
}
struct PDFReport: ReportFormat {
func generate(from data: ReportData) -> String { ... }
}
struct HTMLReport: ReportFormat {
func generate(from data: ReportData) -> String { ... }
}
// Adding Markdown requires no changes to existing code
struct MarkdownReport: ReportFormat {
func generate(from data: ReportData) -> String { ... }
}
class ReportGenerator {
func generate(format: ReportFormat, data: ReportData) -> String {
format.generate(from: data)
}
}Swift-Specific OCP: Protocol Extensions
protocol Cacheable {
var cacheKey: String { get }
var cacheExpiry: TimeInterval { get }
}
// Default behavior via extension — open for override, closed for modification
extension Cacheable {
var cacheExpiry: TimeInterval { 300 } // 5 minutes default
}Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without breaking behavior.
Violation
class FileStorage {
func save(_ data: Data, to path: String) throws { ... }
func load(from path: String) throws -> Data { ... }
func delete(at path: String) throws { ... }
}
class ReadOnlyStorage: FileStorage {
override func save(_ data: Data, to path: String) throws {
throw StorageError.readOnly // Breaks the contract!
}
override func delete(at path: String) throws {
throw StorageError.readOnly // Breaks the contract!
}
}Refactored
protocol ReadableStorage {
func load(from path: String) throws -> Data
}
protocol WritableStorage: ReadableStorage {
func save(_ data: Data, to path: String) throws
func delete(at path: String) throws
}
struct FileStorage: WritableStorage {
func load(from path: String) throws -> Data { ... }
func save(_ data: Data, to path: String) throws { ... }
func delete(at path: String) throws { ... }
}
struct BundleStorage: ReadableStorage {
func load(from path: String) throws -> Data { ... }
// No save/delete — not part of the contract
}Interface Segregation Principle (ISP)
Clients shouldn't depend on methods they don't use.
Violation
protocol DataService {
func fetchUsers() async throws -> [User]
func fetchPosts() async throws -> [Post]
func fetchComments() async throws -> [Comment]
func createUser(_ user: User) async throws
func createPost(_ post: Post) async throws
func deleteUser(_ id: UUID) async throws
}
// UserListView only needs fetchUsers but depends on the entire protocol
struct UserListViewModel {
let service: DataService // Forced to depend on posts, comments, etc.
}Refactored
protocol UserFetching {
func fetchUsers() async throws -> [User]
}
protocol UserManaging: UserFetching {
func createUser(_ user: User) async throws
func deleteUser(_ id: UUID) async throws
}
protocol PostFetching {
func fetchPosts() async throws -> [Post]
}
// A single concrete type can conform to all
class APIService: UserManaging, PostFetching {
func fetchUsers() async throws -> [User] { ... }
func createUser(_ user: User) async throws { ... }
func deleteUser(_ id: UUID) async throws { ... }
func fetchPosts() async throws -> [Post] { ... }
}
// Each consumer depends only on what it needs
struct UserListViewModel {
let userFetcher: UserFetching // Minimal dependency
}Dependency Inversion Principle (DIP)
High-level modules shouldn't depend on low-level modules. Both should depend on abstractions.
Violation
class AnalyticsViewModel {
let tracker = FirebaseAnalytics() // Direct dependency on concrete type
func trackEvent(_ name: String) {
tracker.logEvent(name, parameters: nil)
}
}Refactored
protocol AnalyticsTracking {
func trackEvent(_ name: String, properties: [String: Any])
}
struct FirebaseTracker: AnalyticsTracking {
func trackEvent(_ name: String, properties: [String: Any]) { ... }
}
struct MockTracker: AnalyticsTracking {
var trackedEvents: [(String, [String: Any])] = []
mutating func trackEvent(_ name: String, properties: [String: Any]) {
trackedEvents.append((name, properties))
}
}
@Observable class AnalyticsViewModel {
private let tracker: AnalyticsTracking
init(tracker: AnalyticsTracking) {
self.tracker = tracker
}
}DIP with SwiftUI Environment
// Define the abstraction
protocol ImageLoader {
func load(url: URL) async throws -> NSImage
}
// Environment key
struct ImageLoaderKey: EnvironmentKey {
static let defaultValue: ImageLoader = URLSessionImageLoader()
}
extension EnvironmentValues {
var imageLoader: ImageLoader {
get { self[ImageLoaderKey.self] }
set { self[ImageLoaderKey.self] = newValue }
}
}
// Inject via environment
ContentView()
.environment(\.imageLoader, CachedImageLoader())
// Consume in views
struct ThumbnailView: View {
@Environment(\.imageLoader) private var imageLoader
}Pragmatic SOLID
SOLID principles are guidelines, not laws. Apply them proportionally:
| App Size | SRP | OCP | LSP | ISP | DIP |
|---|---|---|---|---|---|
| Prototype | Loose | Skip | Follow | Skip | Skip |
| Small (1-3 screens) | Moderate | Where natural | Follow | Light | For testing |
| Medium (4-10 screens) | Strict | For extensible areas | Follow | Moderate | For services |
| Large (10+ screens) | Strict | Everywhere | Follow | Strict | Everywhere |
Rules of thumb:
- If a class is under 100 lines, SRP violations are probably fine
- If you'll never extend a type, OCP is unnecessary overhead
- LSP should always be followed — it prevents bugs
- ISP matters most at module boundaries
- DIP matters most for testability and swappable implementations
Architecture & Design Principles
SOLID principles, DRY, Clean Architecture, and design patterns for macOS development.
SOLID Principles
S - Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change.
// ❌ BAD: Multiple responsibilities
class UserViewController: NSViewController {
func loadUser() {
// Network call
let url = URL(string: "https://api.example.com/user")!
URLSession.shared.dataTask(with: url) { data, _, _ in
// Parsing
let user = try? JSONDecoder().decode(User.self, from: data!)
// Database saving
try? self.saveToDatabase(user!)
// UI update
DispatchQueue.main.async {
self.nameLabel.stringValue = user!.name
}
}.resume()
}
}
// ✅ GOOD: Separated responsibilities
protocol UserRepository {
func fetchUser(id: UUID) async throws -> User
}
class NetworkUserRepository: UserRepository {
func fetchUser(id: UUID) async throws -> User {
// Only handles network calls
}
}
@MainActor
class UserViewModel: ObservableObject {
@Published var user: User?
private let repository: UserRepository
init(repository: UserRepository) {
self.repository = repository
}
func loadUser(id: UUID) async {
do {
user = try await repository.fetchUser(id: id)
} catch {
// Handle error
}
}
}
class UserViewController: NSViewController {
private let viewModel: UserViewModel
// Only handles UI updates
func updateUI() {
nameLabel.stringValue = viewModel.user?.name ?? ""
}
}O - Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
// ❌ BAD: Must modify class to add new export formats
class DocumentExporter {
func export(_ document: Document, format: String) throws -> Data {
switch format {
case "pdf":
return exportToPDF(document)
case "html":
return exportToHTML(document)
default:
throw ExportError.unsupportedFormat
}
}
}
// ✅ GOOD: Open for extension via protocols
protocol DocumentExportStrategy {
func export(_ document: Document) throws -> Data
}
class PDFExportStrategy: DocumentExportStrategy {
func export(_ document: Document) throws -> Data {
// PDF export logic
}
}
class HTMLExportStrategy: DocumentExportStrategy {
func export(_ document: Document) throws -> Data {
// HTML export logic
}
}
class MarkdownExportStrategy: DocumentExportStrategy {
func export(_ document: Document) throws -> Data {
// Markdown export logic - new format added without modifying existing code
}
}
class DocumentExporter {
func export(_ document: Document, using strategy: DocumentExportStrategy) throws -> Data {
try strategy.export(document)
}
}L - Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types.
// ❌ BAD: Violates LSP - ReadOnlyDocument can't fulfill Document contract
class Document {
var content: String
func save() throws {
// Save to disk
}
}
class ReadOnlyDocument: Document {
override func save() throws {
throw DocumentError.readOnly // Violates contract!
}
}
// ✅ GOOD: Proper abstraction
protocol Readable {
var content: String { get }
}
protocol Writable {
var content: String { get set }
func save() throws
}
class Document: Readable, Writable {
var content: String
func save() throws {
// Save to disk
}
}
class ReadOnlyDocument: Readable {
let content: String // Immutable, as expected
init(content: String) {
self.content = content
}
}
// Now functions can require only what they need
func displayContent(_ document: Readable) {
print(document.content) // Works with both types
}
func editContent(_ document: Writable) {
// Works only with writable documents
}I - Interface Segregation Principle (ISP)
Definition: Clients should not be forced to depend on interfaces they don't use.
// ❌ BAD: Fat protocol forcing unnecessary implementations
protocol MediaPlayer {
func play()
func pause()
func stop()
func adjustVolume(_ volume: Int)
func showSubtitles(_ show: Bool)
func setPlaybackSpeed(_ speed: Double)
}
class AudioPlayer: MediaPlayer {
func play() { /* ... */ }
func pause() { /* ... */ }
func stop() { /* ... */ }
func adjustVolume(_ volume: Int) { /* ... */ }
func showSubtitles(_ show: Bool) { /* Not applicable! */ }
func setPlaybackSpeed(_ speed: Double) { /* Not applicable! */ }
}
// ✅ GOOD: Segregated interfaces
protocol Playable {
func play()
func pause()
func stop()
}
protocol VolumeControllable {
func adjustVolume(_ volume: Int)
}
protocol SubtitleSupporting {
func showSubtitles(_ show: Bool)
}
protocol PlaybackSpeedControllable {
func setPlaybackSpeed(_ speed: Double)
}
class AudioPlayer: Playable, VolumeControllable {
func play() { /* ... */ }
func pause() { /* ... */ }
func stop() { /* ... */ }
func adjustVolume(_ volume: Int) { /* ... */ }
// No forced subtitle implementation!
}
class VideoPlayer: Playable, VolumeControllable, SubtitleSupporting, PlaybackSpeedControllable {
// Implements all relevant protocols
}D - Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.
// ❌ BAD: High-level class depends on concrete implementation
class ArticleViewController: NSViewController {
private let database = CoreDataManager() // Concrete dependency!
func loadArticles() {
let articles = database.fetchArticles()
// Update UI
}
}
// ✅ GOOD: Depend on abstraction
protocol ArticleRepository {
func fetchArticles() async throws -> [Article]
func save(_ article: Article) async throws
}
class CoreDataArticleRepository: ArticleRepository {
func fetchArticles() async throws -> [Article] {
// Core Data implementation
}
func save(_ article: Article) async throws {
// Core Data implementation
}
}
class SwiftDataArticleRepository: ArticleRepository {
func fetchArticles() async throws -> [Article] {
// SwiftData implementation
}
func save(_ article: Article) async throws {
// SwiftData implementation
}
}
@MainActor
class ArticleViewModel: ObservableObject {
@Published var articles: [Article] = []
private let repository: ArticleRepository // Depends on abstraction!
init(repository: ArticleRepository) {
self.repository = repository
}
func loadArticles() async {
do {
articles = try await repository.fetchArticles()
} catch {
// Handle error
}
}
}
// Easy to swap implementations or mock for testing
let viewModel = ArticleViewModel(repository: SwiftDataArticleRepository())
let testViewModel = ArticleViewModel(repository: MockArticleRepository())DRY Principle (Don't Repeat Yourself)
Definition: Every piece of knowledge must have a single, unambiguous representation.
Code Duplication
// ❌ BAD: Repeated validation logic
func createUser(name: String, email: String) throws {
if name.isEmpty {
throw ValidationError.emptyName
}
if !email.contains("@") {
throw ValidationError.invalidEmail
}
// Create user
}
func updateUser(name: String, email: String) throws {
if name.isEmpty {
throw ValidationError.emptyName
}
if !email.contains("@") {
throw ValidationError.invalidEmail
}
// Update user
}
// ✅ GOOD: Extract common validation
struct UserValidator {
static func validate(name: String, email: String) throws {
guard !name.isEmpty else {
throw ValidationError.emptyName
}
guard email.contains("@") else {
throw ValidationError.invalidEmail
}
}
}
func createUser(name: String, email: String) throws {
try UserValidator.validate(name: name, email: email)
// Create user
}
func updateUser(name: String, email: String) throws {
try UserValidator.validate(name: name, email: email)
// Update user
}SwiftUI View Duplication
// ❌ BAD: Repeated UI components
struct ProfileView: View {
var body: some View {
VStack {
HStack {
Image(systemName: "person")
Text("John Doe")
.font(.headline)
}
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
HStack {
Image(systemName: "envelope")
Text("john@example.com")
.font(.headline)
}
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
}
}
}
// ✅ GOOD: Reusable component
struct InfoRow: View {
let icon: String
let text: String
var body: some View {
HStack {
Image(systemName: icon)
Text(text)
.font(.headline)
}
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
}
}
struct ProfileView: View {
var body: some View {
VStack {
InfoRow(icon: "person", text: "John Doe")
InfoRow(icon: "envelope", text: "john@example.com")
}
}
}Configuration Duplication
// ❌ BAD: Hardcoded values everywhere
class NetworkManager {
func fetchUsers() async throws -> [User] {
let url = URL(string: "https://api.example.com/users")!
// ...
}
func fetchPosts() async throws -> [Post] {
let url = URL(string: "https://api.example.com/posts")!
// ...
}
}
// ✅ GOOD: Centralized configuration
enum APIEndpoint {
case users
case posts
case articles
var url: URL {
let baseURL = "https://api.example.com"
switch self {
case .users: return URL(string: "\(baseURL)/users")!
case .posts: return URL(string: "\(baseURL)/posts")!
case .articles: return URL(string: "\(baseURL)/articles")!
}
}
}
class NetworkManager {
func fetch<T: Decodable>(from endpoint: APIEndpoint) async throws -> T {
let url = endpoint.url
// Single fetch implementation
}
func fetchUsers() async throws -> [User] {
try await fetch(from: .users)
}
func fetchPosts() async throws -> [Post] {
try await fetch(from: .posts)
}
}Clean Architecture
Layer Separation
// Domain Layer - Business logic, no framework dependencies
struct Article {
let id: UUID
let title: String
let content: String
let author: Author
}
protocol ArticleRepository {
func fetch(id: UUID) async throws -> Article
func save(_ article: Article) async throws
}
class ArticleUseCase {
private let repository: ArticleRepository
init(repository: ArticleRepository) {
self.repository = repository
}
func publishArticle(_ article: Article) async throws {
// Business logic
guard article.content.count > 100 else {
throw ValidationError.contentTooShort
}
try await repository.save(article)
}
}
// Data Layer - Framework-specific implementations
import SwiftData
@Model
class ArticleEntity {
@Attribute(.unique) var id: UUID
var title: String
var content: String
// SwiftData specific
}
class SwiftDataArticleRepository: ArticleRepository {
private let modelContext: ModelContext
func fetch(id: UUID) async throws -> Article {
// Convert ArticleEntity to Article
}
func save(_ article: Article) async throws {
// Convert Article to ArticleEntity and save
}
}
// Presentation Layer - SwiftUI/AppKit
@MainActor
class ArticleViewModel: ObservableObject {
@Published var article: Article?
private let useCase: ArticleUseCase
init(useCase: ArticleUseCase) {
self.useCase = useCase
}
func publish() async {
guard let article else { return }
do {
try await useCase.publishArticle(article)
} catch {
// Handle error
}
}
}Dependency Injection
Constructor Injection (Preferred)
// ✅ GOOD: Dependencies injected via initializer
class ArticleService {
private let repository: ArticleRepository
private let logger: Logger
private let validator: ArticleValidator
init(
repository: ArticleRepository,
logger: Logger,
validator: ArticleValidator
) {
self.repository = repository
self.logger = logger
self.validator = validator
}
}Property Injection (Use Sparingly)
// Use for SwiftUI Environment
struct ArticleListView: View {
@Environment(\.articleRepository) var repository
var body: some View {
// Use repository
}
}
// Define environment key
private struct ArticleRepositoryKey: EnvironmentKey {
static let defaultValue: ArticleRepository = MockArticleRepository()
}
extension EnvironmentValues {
var articleRepository: ArticleRepository {
get { self[ArticleRepositoryKey.self] }
set { self[ArticleRepositoryKey.self] = newValue }
}
}Service Locator (Avoid When Possible)
// ⚠️ Use sparingly - hides dependencies
class ServiceLocator {
static let shared = ServiceLocator()
private var services: [String: Any] = [:]
func register<T>(_ service: T, for type: T.Type) {
let key = String(describing: type)
services[key] = service
}
func resolve<T>(_ type: T.Type) -> T? {
let key = String(describing: type)
return services[key] as? T
}
}
// Better: Use constructor injection insteadComposition Over Inheritance
// ❌ BAD: Deep inheritance hierarchy
class Vehicle {
func start() { }
}
class Car: Vehicle {
func honk() { }
}
class ElectricCar: Car {
func charge() { }
}
class TeslaModelS: ElectricCar {
// Too deep!
}
// ✅ GOOD: Composition with protocols
protocol Startable {
func start()
}
protocol Honkable {
func honk()
}
protocol Chargeable {
func charge()
}
struct ElectricCar: Startable, Honkable, Chargeable {
private let engine: ElectricEngine
private let horn: Horn
private let battery: Battery
func start() {
engine.start()
}
func honk() {
horn.makeSound()
}
func charge() {
battery.charge()
}
}Architecture Patterns Checklist
- [ ] SOLID: Each class has single responsibility
- [ ] SOLID: Code open for extension, closed for modification
- [ ] SOLID: Subtypes properly substitutable
- [ ] SOLID: Interfaces are segregated and focused
- [ ] SOLID: Depend on abstractions, not concretions
- [ ] DRY: No duplicated logic or configuration
- [ ] Clean Architecture: Clear layer separation
- [ ] DI: Dependencies injected, not created internally
- [ ] Composition: Prefer composition over inheritance
- [ ] Testability: Easy to mock and test
Resources
Code Organization Best Practices
Modular architecture, project structure, and separation of concerns for macOS development.
Project Structure
Feature-Based Organization (Recommended)
MyMacApp/
├── App/
│ ├── MyMacApp.swift
│ └── AppDelegate.swift
├── Features/
│ ├── Articles/
│ │ ├── Views/
│ │ │ ├── ArticleListView.swift
│ │ │ ├── ArticleDetailView.swift
│ │ │ └── ArticleEditorView.swift
│ │ ├── ViewModels/
│ │ │ ├── ArticleListViewModel.swift
│ │ │ └── ArticleEditorViewModel.swift
│ │ ├── Models/
│ │ │ └── Article.swift
│ │ └── Services/
│ │ └── ArticleRepository.swift
│ ├── Authors/
│ │ ├── Views/
│ │ ├── ViewModels/
│ │ ├── Models/
│ │ └── Services/
│ └── Settings/
│ ├── Views/
│ ├── ViewModels/
│ └── Models/
├── Core/
│ ├── Data/
│ │ ├── SwiftDataStack.swift
│ │ └── ModelContainer+Extensions.swift
│ ├── Networking/
│ │ ├── NetworkManager.swift
│ │ ├── APIEndpoint.swift
│ │ └── NetworkError.swift
│ ├── Extensions/
│ │ ├── Date+Extensions.swift
│ │ ├── String+Extensions.swift
│ │ └── View+Extensions.swift
│ └── Utilities/
│ ├── Logger.swift
│ └── Validator.swift
├── Resources/
│ ├── Assets.xcassets
│ ├── Localization/
│ └── Fonts/
└── Tests/
├── ArticlesTests/
├── AuthorsTests/
└── CoreTests/Layer-Based Organization (Alternative)
MyMacApp/
├── Presentation/
│ ├── SwiftUI/
│ │ ├── Articles/
│ │ └── Settings/
│ ├── AppKit/
│ │ └── CustomViews/
│ └── ViewModels/
├── Domain/
│ ├── Models/
│ ├── UseCases/
│ └── Interfaces/
├── Data/
│ ├── Repositories/
│ ├── DataSources/
│ └── SwiftData/
└── Infrastructure/
├── Networking/
└── Utilities/Swift Package Manager Organization
Multi-Module Architecture
// Package.swift
let package = Package(
name: "MyMacApp",
platforms: [.macOS(.v14)],
products: [
.library(name: "ArticleFeature", targets: ["ArticleFeature"]),
.library(name: "CoreKit", targets: ["CoreKit"]),
.library(name: "NetworkKit", targets: ["NetworkKit"]),
],
dependencies: [
// External dependencies
],
targets: [
// Feature modules
.target(
name: "ArticleFeature",
dependencies: ["CoreKit", "NetworkKit"]
),
.testTarget(
name: "ArticleFeatureTests",
dependencies: ["ArticleFeature"]
),
// Core module
.target(
name: "CoreKit",
dependencies: []
),
// Network module
.target(
name: "NetworkKit",
dependencies: ["CoreKit"]
),
]
)Benefits of SPM Modules
// ✅ GOOD: Clear dependencies and boundaries
import ArticleFeature // Only imports what's needed
import CoreKit
// Each module can be:
// - Built independently
// - Tested in isolation
// - Reused across targets
// - Versioned separatelySeparation of Concerns
MVVM Pattern for SwiftUI
// ❌ BAD: View doing too much
struct ArticleListView: View {
@State private var articles: [Article] = []
var body: some View {
List(articles) { article in
Text(article.title)
}
.task {
// Bad: Networking in view
let url = URL(string: "https://api.example.com/articles")!
let (data, _) = try! await URLSession.shared.data(from: url)
articles = try! JSONDecoder().decode([Article].self, from: data)
}
}
}
// ✅ GOOD: Proper separation
@MainActor
class ArticleListViewModel: ObservableObject {
@Published var articles: [Article] = []
@Published var isLoading = false
@Published var error: Error?
private let repository: ArticleRepository
init(repository: ArticleRepository) {
self.repository = repository
}
func loadArticles() async {
isLoading = true
defer { isLoading = false }
do {
articles = try await repository.fetchArticles()
} catch {
self.error = error
}
}
}
struct ArticleListView: View {
@StateObject private var viewModel: ArticleListViewModel
init(repository: ArticleRepository) {
_viewModel = StateObject(
wrappedValue: ArticleListViewModel(repository: repository)
)
}
var body: some View {
Group {
if viewModel.isLoading {
ProgressView()
} else {
List(viewModel.articles) { article in
Text(article.title)
}
}
}
.task {
await viewModel.loadArticles()
}
}
}Repository Pattern
// ✅ GOOD: Protocol defines contract
protocol ArticleRepository {
func fetchArticles() async throws -> [Article]
func fetchArticle(id: UUID) async throws -> Article?
func save(_ article: Article) async throws
func delete(id: UUID) async throws
}
// Implementation 1: SwiftData
class SwiftDataArticleRepository: ArticleRepository {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func fetchArticles() async throws -> [Article] {
let descriptor = FetchDescriptor<Article>(
sortBy: [SortDescriptor(\.publishedDate, order: .reverse)]
)
return try modelContext.fetch(descriptor)
}
func save(_ article: Article) async throws {
modelContext.insert(article)
try modelContext.save()
}
// ... other methods
}
// Implementation 2: Network
class NetworkArticleRepository: ArticleRepository {
private let networkManager: NetworkManager
init(networkManager: NetworkManager) {
self.networkManager = networkManager
}
func fetchArticles() async throws -> [Article] {
try await networkManager.fetch(from: .articles)
}
// ... other methods
}
// Implementation 3: Mock for testing
class MockArticleRepository: ArticleRepository {
var articles: [Article] = []
func fetchArticles() async throws -> [Article] {
articles
}
func save(_ article: Article) async throws {
articles.append(article)
}
// ... other methods
}File Organization Best Practices
Single Responsibility per File
// ✅ GOOD: One type per file
// Article.swift
struct Article: Identifiable {
let id: UUID
var title: String
var content: String
}
// ArticleValidator.swift
struct ArticleValidator {
static func validate(_ article: Article) throws {
// Validation logic
}
}
// ArticleFormatter.swift
struct ArticleFormatter {
static func format(_ article: Article) -> String {
// Formatting logic
}
}
// ❌ BAD: Multiple unrelated types in one file
// ArticleHelpers.swift
struct Article { }
struct ArticleValidator { }
struct ArticleFormatter { }
class ArticleManager { }Extension Organization
// Article.swift - Main definition
struct Article: Identifiable {
let id: UUID
var title: String
var content: String
}
// Article+Validation.swift - Validation logic
extension Article {
func validate() throws {
guard !title.isEmpty else {
throw ValidationError.emptyTitle
}
}
}
// Article+Formatting.swift - Formatting logic
extension Article {
var formattedPublishDate: String {
publishedDate.formatted(date: .long, time: .omitted)
}
}
// Article+Codable.swift - Codable conformance
extension Article: Codable { }Dependency Management
Composition Root
// ✅ GOOD: Single place for dependency setup
@main
struct MyMacApp: App {
let dependencyContainer: DependencyContainer
init() {
dependencyContainer = DependencyContainer()
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.articleRepository, dependencyContainer.articleRepository)
.environment(\.networkManager, dependencyContainer.networkManager)
}
}
}
class DependencyContainer {
// Singletons
lazy var networkManager: NetworkManager = {
NetworkManager()
}()
lazy var modelContainer: ModelContainer = {
try! ModelContainer(for: Article.self, Author.self)
}()
// Factories
var articleRepository: ArticleRepository {
SwiftDataArticleRepository(
modelContext: modelContainer.mainContext
)
}
var articleListViewModel: ArticleListViewModel {
ArticleListViewModel(repository: articleRepository)
}
}Environment for SwiftUI
// Define environment keys
private struct ArticleRepositoryKey: EnvironmentKey {
static let defaultValue: ArticleRepository = MockArticleRepository()
}
private struct NetworkManagerKey: EnvironmentKey {
static let defaultValue: NetworkManager = NetworkManager()
}
extension EnvironmentValues {
var articleRepository: ArticleRepository {
get { self[ArticleRepositoryKey.self] }
set { self[ArticleRepositoryKey.self] = newValue }
}
var networkManager: NetworkManager {
get { self[NetworkManagerKey.self] }
set { self[NetworkManagerKey.self] = newValue }
}
}
// Usage in views
struct ArticleListView: View {
@Environment(\.articleRepository) var repository
var body: some View {
// Use repository
}
}Code Reusability (DRY)
Extracting Common UI Components
// ✅ GOOD: Reusable components
struct PrimaryButton: View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.frame(maxWidth: .infinity)
.padding()
.background(Color.accentColor)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
struct SecondaryButton: View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.frame(maxWidth: .infinity)
.padding()
.background(Color.secondary.opacity(0.2))
.foregroundColor(.primary)
.cornerRadius(8)
}
}
}
// Usage
VStack {
PrimaryButton(title: "Save") { save() }
SecondaryButton(title: "Cancel") { cancel() }
}View Modifiers for Common Styles
// ✅ GOOD: Custom view modifiers
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.secondary.opacity(0.1))
.cornerRadius(12)
.shadow(radius: 2)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardStyle())
}
}
// Usage
Text("Content")
.cardStyle()Testing Organization
// Tests mirror source structure
MyMacAppTests/
├── Features/
│ ├── Articles/
│ │ ├── ArticleListViewModelTests.swift
│ │ ├── ArticleRepositoryTests.swift
│ │ └── ArticleValidatorTests.swift
│ └── Settings/
│ └── SettingsViewModelTests.swift
└── Core/
├── NetworkManagerTests.swift
└── ValidationTests.swift
// ✅ GOOD: Test structure
import XCTest
@testable import MyMacApp
final class ArticleListViewModelTests: XCTestCase {
var sut: ArticleListViewModel!
var mockRepository: MockArticleRepository!
override func setUp() {
super.setUp()
mockRepository = MockArticleRepository()
sut = ArticleListViewModel(repository: mockRepository)
}
override func tearDown() {
sut = nil
mockRepository = nil
super.tearDown()
}
func testLoadArticles_Success() async {
// Given
let expectedArticles = [
Article(title: "Test 1", content: "Content 1"),
Article(title: "Test 2", content: "Content 2")
]
mockRepository.articles = expectedArticles
// When
await sut.loadArticles()
// Then
XCTAssertEqual(sut.articles.count, 2)
XCTAssertEqual(sut.articles[0].title, "Test 1")
}
}Configuration Management
// ✅ GOOD: Environment-based configuration
enum Environment {
case development
case staging
case production
static var current: Environment {
#if DEBUG
return .development
#else
return .production
#endif
}
}
struct Configuration {
let apiBaseURL: String
let apiKey: String
let enableLogging: Bool
static var current: Configuration {
switch Environment.current {
case .development:
return Configuration(
apiBaseURL: "https://dev-api.example.com",
apiKey: "dev-key",
enableLogging: true
)
case .staging:
return Configuration(
apiBaseURL: "https://staging-api.example.com",
apiKey: "staging-key",
enableLogging: true
)
case .production:
return Configuration(
apiBaseURL: "https://api.example.com",
apiKey: "prod-key",
enableLogging: false
)
}
}
}Code Organization Checklist
- [ ] Clear project structure (feature or layer-based)
- [ ] One type per file (with exceptions for tiny related types)
- [ ] Logical grouping of related files
- [ ] Consistent naming conventions
- [ ] Use of Swift Package Manager for modularity
- [ ] Proper separation of concerns (MVVM/VIPER/etc.)
- [ ] Repository pattern for data access
- [ ] Dependency injection at composition root
- [ ] Reusable UI components
- [ ] Tests mirror source structure
- [ ] Configuration management per environment
Resources
Data Persistence Best Practices
SwiftData-first approach with Core Data guidance for legacy scenarios.
SwiftData (Modern Approach)
Model Definition
import SwiftData
// ✅ GOOD: Clean SwiftData model
@Model
final class Article {
@Attribute(.unique) var id: UUID
var title: String
var content: String
var publishedDate: Date
var author: Author?
var tags: [Tag]
init(title: String, content: String, author: Author? = nil) {
self.id = UUID()
self.title = title
self.content = content
self.publishedDate = Date()
self.author = author
self.tags = []
}
}
@Model
final class Author {
@Attribute(.unique) var id: UUID
var name: String
var email: String
@Relationship(deleteRule: .cascade, inverse: \Article.author)
var articles: [Article]
init(name: String, email: String) {
self.id = UUID()
self.name = name
self.email = email
self.articles = []
}
}
@Model
final class Tag {
@Attribute(.unique) var name: String
var articles: [Article]
init(name: String) {
self.name = name
self.articles = []
}
}Relationships
// One-to-Many with cascade delete
@Model
final class Project {
var name: String
@Relationship(deleteRule: .cascade)
var tasks: [Task]
}
@Model
final class Task {
var title: String
var project: Project?
}
// Many-to-Many
@Model
final class Student {
var name: String
var courses: [Course]
}
@Model
final class Course {
var title: String
var students: [Student]
}
// One-to-One
@Model
final class User {
var username: String
@Relationship(deleteRule: .cascade)
var profile: UserProfile?
}
@Model
final class UserProfile {
var bio: String
var avatarURL: URL?
var user: User?
}Model Container Setup
import SwiftUI
import SwiftData
@main
struct MyApp: App {
let modelContainer: ModelContainer
init() {
do {
let schema = Schema([
Article.self,
Author.self,
Tag.self
])
let configuration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
allowsSave: true
)
modelContainer = try ModelContainer(
for: schema,
configurations: [configuration]
)
} catch {
fatalError("Failed to create ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(modelContainer)
}
}
// ✅ GOOD: In-memory container for testing
extension ModelContainer {
static func preview() throws -> ModelContainer {
let schema = Schema([Article.self, Author.self, Tag.self])
let configuration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: true
)
return try ModelContainer(for: schema, configurations: [configuration])
}
}Querying Data
import SwiftUI
import SwiftData
// ✅ GOOD: Simple query
struct ArticleListView: View {
@Query(sort: \Article.publishedDate, order: .reverse)
private var articles: [Article]
var body: some View {
List(articles) { article in
Text(article.title)
}
}
}
// ✅ GOOD: Filtered query
struct ArticleListView: View {
@Query(
filter: #Predicate<Article> { article in
article.publishedDate > Date().addingTimeInterval(-86400 * 7)
},
sort: \Article.publishedDate,
order: .reverse
)
private var recentArticles: [Article]
var body: some View {
List(recentArticles) { article in
Text(article.title)
}
}
}
// ✅ GOOD: Dynamic query with init
struct ArticleListView: View {
@Query private var articles: [Article]
init(authorName: String) {
let predicate = #Predicate<Article> { article in
article.author?.name == authorName
}
_articles = Query(
filter: predicate,
sort: \.publishedDate,
order: .reverse
)
}
var body: some View {
List(articles) { article in
Text(article.title)
}
}
}Model Context Operations
import SwiftData
@MainActor
class ArticleViewModel: ObservableObject {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
// ✅ GOOD: Insert
func createArticle(title: String, content: String) {
let article = Article(title: title, content: content)
modelContext.insert(article)
do {
try modelContext.save()
} catch {
print("Error saving article: \(error)")
}
}
// ✅ GOOD: Update
func updateArticle(_ article: Article, title: String) {
article.title = title
do {
try modelContext.save()
} catch {
print("Error updating article: \(error)")
}
}
// ✅ GOOD: Delete
func deleteArticle(_ article: Article) {
modelContext.delete(article)
do {
try modelContext.save()
} catch {
print("Error deleting article: \(error)")
}
}
// ✅ GOOD: Batch fetch
func fetchArticles(matching searchText: String) throws -> [Article] {
let predicate = #Predicate<Article> { article in
article.title.localizedStandardContains(searchText) ||
article.content.localizedStandardContains(searchText)
}
let descriptor = FetchDescriptor<Article>(
predicate: predicate,
sortBy: [SortDescriptor(\.publishedDate, order: .reverse)]
)
return try modelContext.fetch(descriptor)
}
}Advanced Predicates
import Foundation
import SwiftData
// ✅ Complex filtering
let predicate = #Predicate<Article> { article in
article.publishedDate > Date().addingTimeInterval(-86400 * 30) &&
article.author?.name == "John Doe" &&
article.tags.contains { $0.name == "Swift" }
}
// ✅ Text search
let searchPredicate = #Predicate<Article> { article in
article.title.localizedStandardContains("SwiftData")
}
// ✅ Range filtering
let rangePredicate = #Predicate<Article> { article in
article.publishedDate >= startDate &&
article.publishedDate <= endDate
}
// ✅ Combining predicates
let combinedPredicate = #Predicate<Article> { article in
(article.title.localizedStandardContains("Swift") ||
article.content.localizedStandardContains("Swift")) &&
article.publishedDate > Date().addingTimeInterval(-86400 * 7)
}Migration and Versioning
import SwiftData
// Version 1
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Article.self, Author.self]
}
@Model
final class Article {
var title: String
var content: String
}
@Model
final class Author {
var name: String
}
}
// Version 2 - Added fields
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Article.self, Author.self]
}
@Model
final class Article {
var title: String
var content: String
var publishedDate: Date // New field
var tags: [String] // New field
}
@Model
final class Author {
var name: String
var email: String // New field
}
}
// Migration plan
enum ArticleMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: nil,
didMigrate: { context in
// Custom migration logic
let articles = try context.fetch(FetchDescriptor<SchemaV2.Article>())
for article in articles {
article.publishedDate = Date()
article.tags = []
}
try context.save()
}
)
}Performance Optimization
// ✅ GOOD: Batch operations
func batchInsert(articles: [ArticleData]) {
let modelContext = ModelContext(modelContainer)
for articleData in articles {
let article = Article(
title: articleData.title,
content: articleData.content
)
modelContext.insert(article)
}
do {
try modelContext.save() // Single save for all inserts
} catch {
print("Batch insert error: \(error)")
}
}
// ✅ GOOD: Lazy loading with limits
func fetchRecentArticles(limit: Int = 20) throws -> [Article] {
let descriptor = FetchDescriptor<Article>(
sortBy: [SortDescriptor(\.publishedDate, order: .reverse)]
)
descriptor.fetchLimit = limit
return try modelContext.fetch(descriptor)
}
// ✅ GOOD: Background context for heavy operations
func processArticles() async {
await Task.detached {
let backgroundContext = ModelContext(modelContainer)
let articles = try? backgroundContext.fetch(FetchDescriptor<Article>())
// Process articles...
try? backgroundContext.save()
}.value
}CloudKit Integration
import SwiftData
// ✅ Configure CloudKit sync
let configuration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
allowsSave: true,
cloudKitDatabase: .automatic // Enables CloudKit sync
)
let container = try ModelContainer(
for: schema,
configurations: [configuration]
)
// ✅ Handle sync conflicts
@Model
final class Article {
var title: String
var content: String
// CloudKit metadata
@Attribute(.cloudKitSystemFields)
var cloudKitMetadata: Data?
}Core Data (Legacy Scenarios)
When to Use Core Data Instead of SwiftData
- Complex migrations from existing Core Data apps
- Need for advanced Core Data features not yet in SwiftData
- Fetched Results Controllers with complex predicates
- Custom NSManagedObject subclasses with complex logic
Core Data Best Practices
import CoreData
// ✅ GOOD: Core Data stack
class CoreDataStack {
static let shared = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyApp")
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
return container
}()
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func saveContext() {
let context = viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
// ✅ GOOD: Background operations
extension CoreDataStack {
func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
persistentContainer.performBackgroundTask(block)
}
}Migration from Core Data to SwiftData
// Step 1: Create SwiftData models matching Core Data entities
@Model
final class Article {
var title: String
var content: String
var publishedDate: Date
init(from managedObject: NSManagedObject) {
self.title = managedObject.value(forKey: "title") as? String ?? ""
self.content = managedObject.value(forKey: "content") as? String ?? ""
self.publishedDate = managedObject.value(forKey: "publishedDate") as? Date ?? Date()
}
}
// Step 2: Migration utility
class CoreDataToSwiftDataMigration {
static func migrate() async throws {
let coreDataContext = CoreDataStack.shared.viewContext
let swiftDataContainer = try ModelContainer(for: Article.self)
let swiftDataContext = ModelContext(swiftDataContainer)
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Article")
let coreDataArticles = try coreDataContext.fetch(fetchRequest)
for managedObject in coreDataArticles {
let article = Article(from: managedObject)
swiftDataContext.insert(article)
}
try swiftDataContext.save()
}
}UserDefaults for Simple Data
// ✅ GOOD: Property wrapper for UserDefaults
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
// ✅ GOOD: Settings with UserDefaults
struct AppSettings {
@UserDefault(key: "theme", defaultValue: "light")
static var theme: String
@UserDefault(key: "fontSize", defaultValue: 14)
static var fontSize: Int
@UserDefault(key: "notificationsEnabled", defaultValue: true)
static var notificationsEnabled: Bool
}
// ⚠️ Don't use UserDefaults for large data or complex objects
// Use SwiftData/Core Data insteadData Persistence Checklist
- [ ] Use SwiftData for new projects
- [ ] Define clear model relationships
- [ ] Implement proper delete rules
- [ ] Use @Query in SwiftUI views
- [ ] Handle save errors gracefully
- [ ] Use background contexts for heavy operations
- [ ] Implement migrations for schema changes
- [ ] Consider CloudKit sync if needed
- [ ] Use UserDefaults only for simple preferences
- [ ] Test with realistic data volumes
Resources
Modern Concurrency Best Practices
Async/await, actors, structured concurrency, and Swift 6 concurrency patterns for macOS.
Async/Await Basics
Converting from Completion Handlers
// ❌ OLD: Completion handler
func fetchUser(id: UUID, completion: @escaping (Result<User, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
// Handle callback
}.resume()
}
// Usage with pyramid of doom
fetchUser(id: userID) { result in
switch result {
case .success(let user):
fetchPosts(for: user) { result in
switch result {
case .success(let posts):
// More nesting...
case .failure(let error):
// Handle error
}
}
case .failure(let error):
// Handle error
}
}
// ✅ GOOD: Async/await
func fetchUser(id: UUID) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
func fetchPosts(for user: User) async throws -> [Post] {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Post].self, from: data)
}
// Usage - clean and linear
do {
let user = try await fetchUser(id: userID)
let posts = try await fetchPosts(for: user)
// Process posts
} catch {
// Handle error
}Async Properties
// ✅ GOOD: Async computed property
class ImageLoader {
var image: UIImage {
get async throws {
let (data, _) = try await URLSession.shared.data(from: imageURL)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
}
}
// Usage
let image = try await imageLoader.imageTask Management
Creating and Managing Tasks
// ✅ GOOD: Unstructured task
class ViewController: NSViewController {
private var loadingTask: Task<Void, Never>?
func loadData() {
loadingTask = Task {
do {
let data = try await fetchData()
await updateUI(with: data)
} catch {
await showError(error)
}
}
}
override func viewDidDisappear() {
super.viewDidDisappear()
loadingTask?.cancel() // Cancel when view disappears
}
}
// ✅ GOOD: Detached task (runs independently)
Task.detached {
// Runs with default priority, no parent context
let result = await heavyComputation()
await MainActor.run {
// Update UI on main thread
updateUI(with: result)
}
}Task Groups for Parallel Execution
// ✅ GOOD: Parallel fetching with task group
func fetchAllArticles(ids: [UUID]) async throws -> [Article] {
try await withThrowingTaskGroup(of: Article.self) { group in
for id in ids {
group.addTask {
try await self.fetchArticle(id: id)
}
}
var articles: [Article] = []
for try await article in group {
articles.append(article)
}
return articles
}
}
// ✅ GOOD: With error handling per task
func fetchAllArticles(ids: [UUID]) async -> [Result<Article, Error>] {
await withTaskGroup(of: Result<Article, Error>.self) { group in
for id in ids {
group.addTask {
do {
let article = try await self.fetchArticle(id: id)
return .success(article)
} catch {
return .failure(error)
}
}
}
var results: [Result<Article, Error>] = []
for await result in group {
results.append(result)
}
return results
}
}Async Sequences
// ✅ GOOD: Processing async sequences
func processLines(from url: URL) async throws {
let lines = url.lines // AsyncSequence
for try await line in lines {
processLine(line)
}
}
// ✅ GOOD: Custom async sequence
struct NumberGenerator: AsyncSequence {
typealias Element = Int
let range: Range<Int>
struct AsyncIterator: AsyncIteratorProtocol {
var current: Int
let end: Int
mutating func next() async -> Int? {
guard current < end else { return nil }
let value = current
current += 1
try? await Task.sleep(for: .milliseconds(100)) // Simulate delay
return value
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(current: range.lowerBound, end: range.upperBound)
}
}
// Usage
for await number in NumberGenerator(range: 0..<10) {
print(number)
}Actors for Thread Safety
Basic Actor Usage
// ❌ BAD: Unsafe class with mutable state
class Counter {
var value = 0 // Race condition!
func increment() {
value += 1
}
}
// ✅ GOOD: Thread-safe actor
actor Counter {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
value
}
}
// Usage (async required)
let counter = Counter()
await counter.increment()
let value = await counter.getValue()Actor Isolation
actor DataManager {
private var cache: [UUID: Data] = [:]
// Isolated to actor - synchronous within actor
func updateCache(id: UUID, data: Data) {
cache[id] = data
}
// Non-isolated - can be called synchronously
nonisolated func generateID() -> UUID {
UUID() // Pure function, no actor state access
}
// Isolated - async from outside
func getData(id: UUID) -> Data? {
cache[id]
}
}
// Usage
let manager = DataManager()
let id = manager.generateID() // Synchronous - nonisolated
await manager.updateCache(id: id, data: data) // Async - isolatedGlobal Actors
// ✅ GOOD: MainActor for UI updates
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
// Runs on main thread
func updateItems(_ newItems: [Item]) {
items = newItems
}
// Explicitly run off main thread
nonisolated func processInBackground() async {
// Heavy computation off main thread
let processed = await heavyComputation()
// Switch back to main thread for UI update
await updateItems(processed)
}
}
// Individual functions can be marked
@MainActor
func updateUI() {
// Guaranteed to run on main thread
}
// Mix isolated and non-isolated in same type
class MixedClass {
@MainActor
var uiProperty: String = ""
nonisolated
func backgroundWork() {
// Runs on background thread
}
}Sendable Protocol
Sendable Types
// ✅ GOOD: Value types are automatically Sendable
struct User: Sendable {
let id: UUID
let name: String
}
// ✅ GOOD: Immutable classes can be Sendable
final class Configuration: Sendable {
let apiKey: String
let baseURL: URL
init(apiKey: String, baseURL: URL) {
self.apiKey = apiKey
self.baseURL = baseURL
}
}
// ❌ BAD: Mutable class can't be Sendable safely
final class Counter: Sendable { // Warning!
var count = 0 // Mutable property not thread-safe
}
// ✅ GOOD: Use actor instead
actor Counter {
var count = 0 // Thread-safe via actor isolation
}
// ✅ GOOD: @unchecked Sendable when you know it's safe
final class ThreadSafeCounter: @unchecked Sendable {
private let lock = NSLock()
private var _count = 0
var count: Int {
lock.lock()
defer { lock.unlock() }
return _count
}
func increment() {
lock.lock()
defer { lock.unlock() }
_count += 1
}
}Sendable Closures
// ✅ GOOD: Sendable closure
func processAsync(_ handler: @Sendable () -> Void) async {
await Task.detached {
handler()
}.value
}
// ❌ BAD: Capturing mutable state
var counter = 0
processAsync {
counter += 1 // Error: capturing mutable state
}
// ✅ GOOD: Use actor for mutable state
actor SharedCounter {
var value = 0
}
let sharedCounter = SharedCounter()
processAsync {
await sharedCounter.increment()
}Concurrency Patterns
Repository Pattern with Async/Await
protocol ArticleRepository {
func fetchArticles() async throws -> [Article]
func save(_ article: Article) async throws
}
actor SwiftDataArticleRepository: ArticleRepository {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func fetchArticles() async throws -> [Article] {
let descriptor = FetchDescriptor<Article>()
return try modelContext.fetch(descriptor)
}
func save(_ article: Article) async throws {
modelContext.insert(article)
try modelContext.save()
}
}ViewModel with Async Operations
@MainActor
class ArticleListViewModel: ObservableObject {
@Published var articles: [Article] = []
@Published var isLoading = false
@Published var error: Error?
private let repository: ArticleRepository
init(repository: ArticleRepository) {
self.repository = repository
}
func loadArticles() async {
isLoading = true
error = nil
do {
articles = try await repository.fetchArticles()
} catch {
self.error = error
}
isLoading = false
}
func refresh() async {
await loadArticles()
}
}
// Usage in SwiftUI
struct ArticleListView: View {
@StateObject private var viewModel: ArticleListViewModel
var body: some View {
List(viewModel.articles) { article in
Text(article.title)
}
.task {
await viewModel.loadArticles()
}
.refreshable {
await viewModel.refresh()
}
}
}Cancellation Handling
// ✅ GOOD: Checking for cancellation
func performLongOperation() async throws {
for i in 0..<1000 {
// Check if task was cancelled
try Task.checkCancellation()
// Or check manually
if Task.isCancelled {
cleanup()
return
}
await processItem(i)
}
}
// ✅ GOOD: Cancellation in view model
@MainActor
class SearchViewModel: ObservableObject {
@Published var results: [Result] = []
private var searchTask: Task<Void, Never>?
func search(_ query: String) {
// Cancel previous search
searchTask?.cancel()
searchTask = Task {
do {
try await Task.sleep(for: .milliseconds(300)) // Debounce
try Task.checkCancellation()
let results = try await performSearch(query)
self.results = results
} catch is CancellationError {
// Ignore cancellation
} catch {
// Handle other errors
}
}
}
}Concurrency Best Practices
Avoid Blocking Main Thread
// ❌ BAD: Blocking main thread
@MainActor
func loadData() {
let data = heavyComputation() // Blocks UI!
updateUI(with: data)
}
// ✅ GOOD: Run computation off main thread
@MainActor
func loadData() async {
let data = await Task.detached {
heavyComputation()
}.value
updateUI(with: data)
}Use Structured Concurrency
// ❌ BAD: Unstructured tasks can leak
func fetchAllData() {
Task {
let users = try await fetchUsers()
}
Task {
let posts = try await fetchPosts()
}
// Tasks may outlive this function
}
// ✅ GOOD: Structured with task group
func fetchAllData() async throws -> (users: [User], posts: [Post]) {
try await withThrowingTaskGroup(of: Void.self) { group in
var users: [User] = []
var posts: [Post] = []
group.addTask {
users = try await self.fetchUsers()
}
group.addTask {
posts = try await self.fetchPosts()
}
try await group.waitForAll()
return (users, posts)
}
}Prioritize Tasks
// ✅ GOOD: Task priorities
Task(priority: .background) {
await performBackgroundSync()
}
Task(priority: .userInitiated) {
await loadUserData()
}
Task(priority: .high) {
await handleUrgentRequest()
}AsyncStream for Continuous Updates
// ✅ GOOD: AsyncStream for real-time updates
actor NotificationCenter {
private var continuations: [UUID: AsyncStream<Notification>.Continuation] = [:]
func notifications() -> AsyncStream<Notification> {
AsyncStream { continuation in
let id = UUID()
continuations[id] = continuation
continuation.onTermination = { [weak self] _ in
Task { await self?.removeContinuation(id) }
}
}
}
func post(_ notification: Notification) {
for continuation in continuations.values {
continuation.yield(notification)
}
}
private func removeContinuation(_ id: UUID) {
continuations.removeValue(forKey: id)
}
}
// Usage
let notificationCenter = NotificationCenter()
Task {
for await notification in await notificationCenter.notifications() {
print("Received: \(notification)")
}
}
await notificationCenter.post(Notification(name: "test"))Concurrency Checklist
- [ ] Use async/await instead of completion handlers
- [ ] Mark UI-related code with @MainActor
- [ ] Use actors for mutable shared state
- [ ] Ensure types crossing concurrency boundaries are Sendable
- [ ] Handle task cancellation appropriately
- [ ] Use structured concurrency (task groups) over unstructured tasks
- [ ] Check for race conditions in mutable state
- [ ] Avoid blocking the main thread
- [ ] Set appropriate task priorities
- [ ] Clean up resources on task cancellation
Swift 6 Concurrency Migration
- [ ] Enable strict concurrency checking
- [ ] Fix non-Sendable type warnings
- [ ] Mark appropriate types as @MainActor
- [ ] Replace DispatchQueue with async/await
- [ ] Convert @escaping closures to async functions
- [ ] Use actors instead of locks
- [ ] Audit @unchecked Sendable usage
Resources
Swift Language Best Practices
Modern Swift 6+ language patterns and idioms for macOS development.
Swift 6 Features
Strict Concurrency Checking
// ✅ GOOD: Sendable conformance
struct User: Sendable {
let id: UUID
let name: String
}
// ❌ BAD: Mutable reference type without protection
class UserManager {
var users: [User] = [] // Not thread-safe!
}
// ✅ GOOD: Use actor for mutable state
actor UserManager {
private var users: [User] = []
func addUser(_ user: User) {
users.append(user)
}
}Macro System
// ✅ Use macros for reducing boilerplate
import SwiftData
@Model
class Article {
var title: String
var content: String
var publishedDate: Date
}
// Generates: Codable, Hashable, Observable, and moreTyped Throws (Swift 6+)
// ✅ GOOD: Specific error types
enum NetworkError: Error {
case invalidURL
case timeout
case serverError(Int)
}
func fetchData() throws(NetworkError) -> Data {
// Implementation
}
// Usage with specific error handling
do {
let data = try fetchData()
} catch let error as NetworkError {
switch error {
case .invalidURL:
// Handle specific error
case .timeout:
// Handle timeout
case .serverError(let code):
// Handle server error
}
}Value Types vs Reference Types
Prefer Value Types
// ✅ GOOD: Value type for data
struct Settings {
var theme: Theme
var fontSize: Int
var notifications: Bool
}
// ❌ BAD: Unnecessary class
class Settings {
var theme: Theme
var fontSize: Int
var notifications: Bool
}When to Use Reference Types
// ✅ GOOD: Reference type for identity and shared state
final class DocumentController: ObservableObject {
@Published var document: Document
private let fileManager: FileManager
// Complex lifecycle, needs identity
}
// ✅ GOOD: Reference type for inheritance
class BaseViewController: NSViewController {
// AppKit requires inheritance
}Protocol-Oriented Programming
Protocol Composition
// ✅ GOOD: Small, focused protocols
protocol Identifiable {
var id: UUID { get }
}
protocol Timestamped {
var createdAt: Date { get }
var updatedAt: Date { get }
}
protocol Searchable {
var searchableText: String { get }
}
// Compose protocols
struct Article: Identifiable, Timestamped, Searchable {
let id: UUID
let createdAt: Date
var updatedAt: Date
var title: String
var content: String
var searchableText: String {
"\(title) \(content)"
}
}Protocol Extensions
// ✅ GOOD: Default implementations
protocol Validatable {
func validate() throws
}
extension Validatable {
func isValid() -> Bool {
do {
try validate()
return true
} catch {
return false
}
}
}Protocol Witnesses (Avoid Runtime Type Checks)
// ❌ BAD: Runtime type checking
func process(_ item: Any) {
if let article = item as? Article {
print(article.title)
} else if let video = item as? Video {
print(video.title)
}
}
// ✅ GOOD: Protocol-based approach
protocol Displayable {
var displayTitle: String { get }
}
extension Article: Displayable {
var displayTitle: String { title }
}
extension Video: Displayable {
var displayTitle: String { title }
}
func process(_ item: Displayable) {
print(item.displayTitle)
}Generics and Type Safety
Generic Functions
// ✅ GOOD: Generic function with constraints
func findFirst<T: Collection>(
in collection: T,
matching predicate: (T.Element) -> Bool
) -> T.Element? where T.Element: Equatable {
collection.first(where: predicate)
}Associated Types
// ✅ GOOD: Protocol with associated type
protocol Repository {
associatedtype Entity
func fetch(id: UUID) async throws -> Entity?
func save(_ entity: Entity) async throws
func delete(id: UUID) async throws
}
struct ArticleRepository: Repository {
typealias Entity = Article
func fetch(id: UUID) async throws -> Article? {
// Implementation
}
func save(_ entity: Article) async throws {
// Implementation
}
func delete(id: UUID) async throws {
// Implementation
}
}Optionals Best Practices
Optional Binding
// ✅ GOOD: Guard for early exit
func processUser(_ user: User?) {
guard let user else { return }
// Work with unwrapped user
}
// ✅ GOOD: If-let for scoped usage
if let user = optionalUser {
print(user.name)
}
// ❌ BAD: Force unwrapping
let name = user!.name // Dangerous!
// ❌ BAD: Implicit unwrapping (use sparingly)
var user: User!Nil-Coalescing and Optional Chaining
// ✅ GOOD: Nil-coalescing with default
let displayName = user?.name ?? "Guest"
// ✅ GOOD: Optional chaining
let uppercasedName = user?.name?.uppercased()
// ✅ GOOD: Optional map and flatMap
let userID = optionalUser.map { $0.id }Property Wrappers
Built-in Property Wrappers
import SwiftUI
struct SettingsView: View {
@AppStorage("theme") private var theme: String = "light"
@State private var isEditing = false
@ObservedObject var viewModel: SettingsViewModel
@Environment(\.colorScheme) var colorScheme
var body: some View {
// View implementation
}
}Custom Property Wrappers
// ✅ GOOD: Custom property wrapper for validation
@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
private let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(range.lowerBound, wrappedValue), range.upperBound)
}
}
struct Settings {
@Clamped(0...100) var volume: Int = 50
@Clamped(10...72) var fontSize: Int = 14
}Result Builders
SwiftUI-Style DSL
// ✅ GOOD: Result builder for custom DSL
@resultBuilder
struct MenuBuilder {
static func buildBlock(_ components: MenuItem...) -> [MenuItem] {
components
}
}
struct MenuItem {
let title: String
let action: () -> Void
}
func createMenu(@MenuBuilder builder: () -> [MenuItem]) -> [MenuItem] {
builder()
}
// Usage
let menu = createMenu {
MenuItem(title: "Open") { /* action */ }
MenuItem(title: "Save") { /* action */ }
MenuItem(title: "Close") { /* action */ }
}Error Handling
Swift Error Protocol
// ✅ GOOD: Well-structured error types
enum ValidationError: Error, LocalizedError {
case emptyField(String)
case invalidFormat(field: String, expected: String)
case outOfRange(field: String, range: ClosedRange<Int>)
var errorDescription: String? {
switch self {
case .emptyField(let field):
return "\(field) cannot be empty"
case .invalidFormat(let field, let expected):
return "\(field) has invalid format. Expected: \(expected)"
case .outOfRange(let field, let range):
return "\(field) must be between \(range.lowerBound) and \(range.upperBound)"
}
}
}Do-Catch Best Practices
// ✅ GOOD: Specific error handling
func saveDocument(_ document: Document) async {
do {
try await repository.save(document)
showSuccess()
} catch let error as ValidationError {
showValidationError(error)
} catch let error as NetworkError {
showNetworkError(error)
} catch {
showGenericError(error)
}
}
// ✅ GOOD: Using Result type
func loadDocument(id: UUID) -> Result<Document, Error> {
do {
let document = try repository.fetch(id: id)
return .success(document)
} catch {
return .failure(error)
}
}Collections and Algorithms
Use Appropriate Collection Types
// ✅ GOOD: Array for ordered collections
var items: [Item] = []
// ✅ GOOD: Set for unique values and fast lookup
var uniqueIDs: Set<UUID> = []
// ✅ GOOD: Dictionary for key-value pairs
var usersByID: [UUID: User] = [:]
// ✅ GOOD: OrderedSet (Swift Collections) when order + uniqueness matter
import OrderedCollections
var orderedUniqueItems: OrderedSet<Item> = []Functional Programming Patterns
// ✅ GOOD: Map, filter, reduce
let activeUserNames = users
.filter { $0.isActive }
.map { $0.name }
.sorted()
let totalScore = scores.reduce(0, +)
// ✅ GOOD: CompactMap for removing nils
let validURLs = strings.compactMap { URL(string: $0) }
// ✅ GOOD: FlatMap for flattening nested collections
let allTags = articles.flatMap { $0.tags }Memory Management
Capture Lists in Closures
// ✅ GOOD: Weak self to avoid retain cycles
class DocumentViewController: NSViewController {
private var document: Document
func setupObserver() {
NotificationCenter.default.addObserver(
forName: .documentDidChange,
object: nil,
queue: .main
) { [weak self] notification in
guard let self else { return }
self.updateUI()
}
}
}
// ✅ GOOD: Unowned when guaranteed to exist
class ChildView: NSView {
unowned let parentController: ParentViewController
func handleAction() {
parentController.performAction() // Safe: parent owns this view
}
}Automatic Reference Counting (ARC)
// ❌ BAD: Strong reference cycle
class Author {
var books: [Book] = []
}
class Book {
var author: Author // Strong reference creates cycle!
}
// ✅ GOOD: Break cycle with weak reference
class Author {
var books: [Book] = []
}
class Book {
weak var author: Author? // Weak reference breaks cycle
}Swift 6 Migration Checklist
- [ ] Enable strict concurrency checking
- [ ] Mark types as
Sendablewhere appropriate - [ ] Use actors for mutable shared state
- [ ] Replace completion handlers with async/await
- [ ] Use typed throws for better error handling
- [ ] Adopt new Swift 6 features (macros, etc.)
- [ ] Remove deprecated APIs
- [ ] Update to use
@Observableinstead ofObservableObject
Resources
Apple Intelligence Integration
Foundation Models, on-device AI, and MCP (Model Context Protocol) support in macOS 26.
Foundation Models API
import AppleIntelligence // Hypothetical framework
// ✅ Text generation
func generateText(prompt: String) async throws -> String {
let model = try await AIFoundationModel.load(.textGeneration)
let response = try await model.generate(prompt: prompt)
return response.text
}
// ✅ Text summarization
func summarize(_ text: String) async throws -> String {
let model = try await AIFoundationModel.load(.summarization)
return try await model.summarize(text)
}
// ✅ On-device processing (privacy-preserving)
func analyzeOnDevice(_ data: String) async throws -> Analysis {
let model = try await AIFoundationModel.load(.analysis)
model.processingLocation = .onDevice // Ensures privacy
return try await model.analyze(data)
}Model Context Protocol (MCP)
// ✅ MCP integration for AI context
struct MCPContext {
let tools: [MCPTool]
let resources: [MCPResource]
}
protocol MCPTool {
var name: String { get }
var description: String { get }
func execute(parameters: [String: Any]) async throws -> Any
}
// Example MCP tool
struct FileSearchTool: MCPTool {
let name = "file_search"
let description = "Search for files in the system"
func execute(parameters: [String: Any]) async throws -> Any {
guard let query = parameters["query"] as? String else {
throw MCPError.invalidParameters
}
// Perform file search
return searchFiles(query: query)
}
private func searchFiles(query: String) -> [URL] {
// Implementation
return []
}
}Privacy-Preserving AI
// ✅ On-device model inference
func processPrivately(_ input: String) async throws -> Result {
// All processing happens on-device
let model = try await LocalAIModel.load()
return try await model.process(input)
}
// ✅ Check processing location
func verifyPrivacy() async -> Bool {
let model = try? await AIFoundationModel.load(.textGeneration)
return model?.processingLocation == .onDevice
}Resources
Continuity Features
Cross-device integration between macOS, iOS, and iPadOS.
Universal Clipboard
// ✅ Copy to universal clipboard
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(text, forType: .string)
// Automatically syncs to other devices
// ✅ Monitor clipboard changes
NotificationCenter.default.addObserver(
forName: NSPasteboard.didChangeNotification,
object: nil,
queue: .main
) { _ in
handleClipboardChange()
}Handoff
// ✅ Enable Handoff
func setupHandoff() {
let activity = NSUserActivity(activityType: "com.app.editing")
activity.title = "Editing Document"
activity.userInfo = ["documentID": document.id.uuidString]
activity.isEligibleForHandoff = true
activity.becomeCurrent()
}
// ✅ Continue activity from another device
func application(
_ application: NSApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void
) -> Bool {
if userActivity.activityType == "com.app.editing",
let documentID = userActivity.userInfo?["documentID"] as? String {
openDocument(id: documentID)
return true
}
return false
}AirDrop Integration
import AppKit
// ✅ Share via AirDrop
let sharingService = NSSharingService(named: .sendViaAirDrop)
sharingService?.perform(withItems: [url])
// ✅ Receive AirDrop files
func application(_ application: NSApplication, open urls: [URL]) {
for url in urls {
handleReceivedFile(url)
}
}Resources
Related skills
How it compares
Pick macos-development when you need structured macOS-specific planning documents across architecture, UI, and data rather than generic cross-platform code review skills.
FAQ
Which UI framework?
SwiftUI with Xcode project conventions for macOS targets.
Does it cover distribution?
Yes; Mac App Store and direct distribution considerations are included.
What guidelines apply?
Apple Human Interface Guidelines for desktop window and menu patterns.
Is Macos Development safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.