All skills
rshankras avatar

/architecture-patterns

@1361023

Deep dive into software architecture for macOS. Covers SOLID principles, design patterns, and modular code organization. Use when designing app architecture or refactoring.

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/architecture-patterns

This session only. Nothing lands on disk.

design-patterns.md

≈2.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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

// 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)

@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
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

Source: SKILL.md on GitHub

No alerts2mo3 checks · Risk SAFE
  • Gen Agent Trust Hub2mo

    The skill is a comprehensive guide for macOS software architecture using Swift. It is generally safe but possesses a standard attack surface for indirect prompt injection as it analyzes external code using file-reading tools without explicit boundary instructions.

  • Socket2mo

    No alerts

  • Snyk2mo

    Risk: LOW · No issues

Signed by skilld at 1361023. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 3 months ago
What it can do
Reads files
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 3 allowed tools
ReadGlobGrep

README badge

README badge for rshankras/claude-code-apple-skills/architecture-patterns