Back to Blog
Technical
November 20, 202412 min read

SwiftUI Architecture Patterns I Use in Production

After shipping multiple SwiftUI apps to the App Store, here are the architectural patterns that have served me well—and the ones I've abandoned.

SwiftUIArchitectureiOSBest Practices

SwiftUI Architecture Patterns


After building several production SwiftUI apps, I've developed strong opinions about what works and what doesn't. This post shares the patterns I reach for consistently.


The Basics: MVVM with a Twist


Classic MVVM works well with SwiftUI, but I've made some modifications:


View Models as @Observable Classes


With iOS 17's `@Observable` macro, view models became much cleaner:


@Observable
final class ProductListViewModel {
    var products: [Product] = []
    var isLoading = false
    var error: Error?

    private let repository: ProductRepository

    func loadProducts() async {
        isLoading = true
        defer { isLoading = false }

        do {
            products = try await repository.fetchProducts()
        } catch {
            self.error = error
        }
    }
}

Dependency Injection via Environment


Rather than passing dependencies through initializers, I use SwiftUI's environment:


@Environment(ProductRepository.self) private var repository

This keeps views clean and makes testing straightforward.


Navigation: The Coordinator Pattern


Navigation state in SwiftUI can become messy. I use a coordinator pattern with `NavigationStack`:


@Observable
final class AppCoordinator {
    var path = NavigationPath()

    func navigateToProduct(_ id: Product.ID) {
        path.append(Route.product(id))
    }

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

Data Flow: Unidirectional When It Matters


For complex features, unidirectional data flow (inspired by The Composable Architecture) prevents bugs:


  • Views dispatch actions
  • Actions are processed by reducers
  • State updates flow back to views

  • For simple CRUD, this is overkill. Know when to use it.


    Testing Strategy


  • **ViewModels**: Unit tested with mocked dependencies
  • **Views**: Snapshot tests for visual regression
  • **Integration**: A few happy-path tests through the coordinator

  • The key insight: test behavior, not implementation.

    Enjoyed this post?

    I write about iOS development, architecture patterns, and building products. Get in touch if you'd like to work together.