AgentSkillsCN

swift-architecture

精通 iOS/macOS 应用架构——MVVM、整洁架构、协调器、DI、存储库

SKILL.md
--- frontmatter
name: swift-architecture
description: Master iOS/macOS app architecture - MVVM, Clean Architecture, Coordinator, DI, Repository
version: "2.0.0"
sasmp_version: "1.3.0"
bonded_agent: 05-swift-macos
bond_type: SECONDARY_BOND

Swift Architecture Skill

Design patterns and architectural approaches for scalable, testable Swift applications.

Prerequisites

  • Understanding of SOLID principles
  • Familiarity with dependency injection
  • Experience with protocol-oriented programming

Parameters

yaml
parameters:
  architecture_pattern:
    type: string
    enum: [mvvm, mvc, tca, viper, clean]
    default: mvvm
  navigation_pattern:
    type: string
    enum: [coordinator, router, navigation_stack]
    default: coordinator
  di_approach:
    type: string
    enum: [manual, container, property_wrapper]
    default: manual

Topics Covered

Architecture Patterns

PatternComplexityTestabilityBest For
MVCLowLowSimple apps
MVVMMediumHighMost apps
CleanHighVery HighLarge teams
TCAHighVery HighComplex state
VIPERVery HighVery HighEnterprise

Key Principles

PrincipleDescription
Separation of ConcernsEach layer has one job
Dependency InversionDepend on abstractions
Single Source of TruthOne place for state
Unidirectional Data FlowState → View → Action → State

Layer Responsibilities

LayerResponsibility
ViewUI rendering only
ViewModelPresentation logic
UseCaseBusiness logic
RepositoryData access
ServiceExternal integrations

Code Examples

MVVM with Coordinator

swift
// MARK: - Coordinator Protocol

protocol Coordinator: AnyObject {
    var navigationController: UINavigationController { get }
    var childCoordinators: [Coordinator] { get set }
    func start()
}

extension Coordinator {
    func addChild(_ coordinator: Coordinator) {
        childCoordinators.append(coordinator)
    }

    func removeChild(_ coordinator: Coordinator) {
        childCoordinators.removeAll { $0 === coordinator }
    }
}

// MARK: - App Coordinator

final class AppCoordinator: Coordinator {
    let navigationController: UINavigationController
    var childCoordinators: [Coordinator] = []
    private let dependencies: AppDependencies

    init(navigationController: UINavigationController, dependencies: AppDependencies) {
        self.navigationController = navigationController
        self.dependencies = dependencies
    }

    func start() {
        if dependencies.authService.isLoggedIn {
            showMain()
        } else {
            showLogin()
        }
    }

    private func showLogin() {
        let coordinator = LoginCoordinator(
            navigationController: navigationController,
            dependencies: dependencies
        )
        coordinator.delegate = self
        addChild(coordinator)
        coordinator.start()
    }

    private func showMain() {
        let coordinator = MainCoordinator(
            navigationController: navigationController,
            dependencies: dependencies
        )
        addChild(coordinator)
        coordinator.start()
    }
}

extension AppCoordinator: LoginCoordinatorDelegate {
    func loginDidComplete(_ coordinator: LoginCoordinator) {
        removeChild(coordinator)
        showMain()
    }
}

// MARK: - ViewModel

@MainActor
protocol ProductListViewModelProtocol: ObservableObject {
    var products: [Product] { get }
    var isLoading: Bool { get }
    var error: Error? { get }

    func loadProducts() async
    func selectProduct(_ product: Product)
}

@MainActor
final class ProductListViewModel: ProductListViewModelProtocol {
    @Published private(set) var products: [Product] = []
    @Published private(set) var isLoading = false
    @Published private(set) var error: Error?

    private let getProductsUseCase: GetProductsUseCaseProtocol
    private weak var coordinator: ProductCoordinator?

    init(getProductsUseCase: GetProductsUseCaseProtocol, coordinator: ProductCoordinator) {
        self.getProductsUseCase = getProductsUseCase
        self.coordinator = coordinator
    }

    func loadProducts() async {
        isLoading = true
        error = nil

        do {
            products = try await getProductsUseCase.execute()
        } catch {
            self.error = error
        }

        isLoading = false
    }

    func selectProduct(_ product: Product) {
        coordinator?.showProductDetail(product)
    }
}

Clean Architecture Layers

swift
// MARK: - Domain Layer (Use Cases)

protocol GetProductsUseCaseProtocol {
    func execute() async throws -> [Product]
}

final class GetProductsUseCase: GetProductsUseCaseProtocol {
    private let repository: ProductRepositoryProtocol

    init(repository: ProductRepositoryProtocol) {
        self.repository = repository
    }

    func execute() async throws -> [Product] {
        let products = try await repository.getProducts()
        // Business logic: filter, sort, validate
        return products.filter { $0.isAvailable }.sorted { $0.name < $1.name }
    }
}

// MARK: - Data Layer (Repository)

protocol ProductRepositoryProtocol {
    func getProducts() async throws -> [Product]
    func getProduct(id: String) async throws -> Product
    func saveProduct(_ product: Product) async throws
}

final class ProductRepository: ProductRepositoryProtocol {
    private let remoteDataSource: ProductRemoteDataSourceProtocol
    private let localDataSource: ProductLocalDataSourceProtocol

    init(remoteDataSource: ProductRemoteDataSourceProtocol,
         localDataSource: ProductLocalDataSourceProtocol) {
        self.remoteDataSource = remoteDataSource
        self.localDataSource = localDataSource
    }

    func getProducts() async throws -> [Product] {
        // Try cache first
        if let cached = try? await localDataSource.getProducts(), !cached.isEmpty {
            // Refresh in background
            Task {
                if let remote = try? await remoteDataSource.fetchProducts() {
                    try? await localDataSource.saveProducts(remote)
                }
            }
            return cached
        }

        // Fetch from remote
        let products = try await remoteDataSource.fetchProducts()
        try? await localDataSource.saveProducts(products)
        return products
    }

    func getProduct(id: String) async throws -> Product {
        try await remoteDataSource.fetchProduct(id: id)
    }

    func saveProduct(_ product: Product) async throws {
        try await remoteDataSource.createProduct(product)
        try await localDataSource.saveProduct(product)
    }
}

Dependency Injection Container

swift
// MARK: - Dependencies Protocol

protocol HasAuthService {
    var authService: AuthServiceProtocol { get }
}

protocol HasProductRepository {
    var productRepository: ProductRepositoryProtocol { get }
}

typealias AppDependencies = HasAuthService & HasProductRepository

// MARK: - DI Container

final class DependencyContainer: AppDependencies {
    // Singletons
    lazy var authService: AuthServiceProtocol = AuthService()

    // Factories
    lazy var productRepository: ProductRepositoryProtocol = {
        ProductRepository(
            remoteDataSource: ProductRemoteDataSource(apiClient: apiClient),
            localDataSource: ProductLocalDataSource(database: database)
        )
    }()

    private lazy var apiClient: APIClientProtocol = APIClient()
    private lazy var database: DatabaseProtocol = Database()

    // Factory methods for ViewModels
    func makeProductListViewModel(coordinator: ProductCoordinator) -> ProductListViewModel {
        ProductListViewModel(
            getProductsUseCase: GetProductsUseCase(repository: productRepository),
            coordinator: coordinator
        )
    }
}

// MARK: - Property Wrapper Approach

@propertyWrapper
struct Injected<T> {
    private let keyPath: KeyPath<DependencyContainer, T>

    var wrappedValue: T {
        DependencyContainer.shared[keyPath: keyPath]
    }

    init(_ keyPath: KeyPath<DependencyContainer, T>) {
        self.keyPath = keyPath
    }
}

// Usage
final class SomeService {
    @Injected(\.authService) private var authService
}

SwiftUI MVVM

swift
// MARK: - View

struct ProductListView: View {
    @StateObject private var viewModel: ProductListViewModel

    init(viewModel: @autoclosure @escaping () -> ProductListViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel())
    }

    var body: some View {
        Group {
            if viewModel.isLoading {
                ProgressView()
            } else if let error = viewModel.error {
                ErrorView(error: error) {
                    Task { await viewModel.loadProducts() }
                }
            } else {
                productList
            }
        }
        .navigationTitle("Products")
        .task {
            await viewModel.loadProducts()
        }
    }

    private var productList: some View {
        List(viewModel.products) { product in
            ProductRow(product: product)
                .onTapGesture {
                    viewModel.selectProduct(product)
                }
        }
    }
}

// MARK: - SwiftUI Coordinator (Router)

@MainActor
final class Router: ObservableObject {
    @Published var path = NavigationPath()

    func push<T: Hashable>(_ value: T) {
        path.append(value)
    }

    func pop() {
        path.removeLast()
    }

    func popToRoot() {
        path.removeLast(path.count)
    }
}

struct ContentView: View {
    @StateObject private var router = Router()
    @StateObject private var dependencies = DependencyContainer()

    var body: some View {
        NavigationStack(path: $router.path) {
            ProductListView(viewModel: dependencies.makeProductListViewModel(router: router))
                .navigationDestination(for: Product.self) { product in
                    ProductDetailView(product: product)
                }
        }
        .environmentObject(router)
    }
}

Troubleshooting

Common Issues

IssueCauseSolution
Massive ViewModelToo many responsibilitiesSplit into smaller VMs or use UseCases
Tight couplingDirect dependenciesUse protocols and DI
Hard to testStatic/singleton dependenciesInject dependencies
Memory leaksStrong coordinator referencesUse weak delegates
State sync issuesMultiple sources of truthSingle source + binding

Debug Tips

swift
// Check retain cycles
deinit {
    print("\(Self.self) deinit")
}

// Trace view updates
var body: some View {
    let _ = Self._printChanges()
    // ...
}

// Validate architecture
// Run: swift package diagnose-api-breaking-changes

Validation Rules

yaml
validation:
  - rule: layer_separation
    severity: error
    check: Views should not import data layer
  - rule: protocol_abstractions
    severity: warning
    check: Dependencies should be protocols
  - rule: unidirectional_flow
    severity: info
    check: State changes flow in one direction

Usage

code
Skill("swift-architecture")

Related Skills

  • swift-fundamentals - Protocol-oriented design
  • swift-swiftui - SwiftUI patterns
  • swift-testing - Testing architecture