Skip to main content
3Nsofts logo3Nsofts
iOS Architecture

swiftui-architecture-patterns-2025

According to the 2024 State of Swift Developer Survey, SwiftUI is now the primary UI framework for over 58% of new iOS codebases — up from 34% in 2022. The architecture question is no longer whether to use SwiftUI. It's how to structure it for apps that also need to run on-device AI.

By Ehsan Azish · 3NSOFTS·March 2026·8 min read

The three patterns in production use

Most SwiftUI production codebases today use one of three architectural approaches. Each has a legitimate context where it wins — and a common context where teams choose it for the wrong reasons.

| Pattern | State model | Boilerplate | Testability | | --- | --- | --- | --- | | MVVM + @Observable | Observable class per screen | Low | High | | TCA | Reducer + State + Action | High | Very High | | Observation + Environment | Shared @Observable injected via environment | Low | Medium |

MVVM with Swift Observation — the 2026 default

Apple's Observation framework (introduced in iOS 17) made MVVM significantly leaner. The @Observable macro replaces the manual ObservableObject + @Published boilerplate and provides automatic fine-grained tracking — SwiftUI only re-renders the views that read a specific property when that property changes.

A production MVVM view model with @Observable looks like this:

@Observable
final class ImageClassifierViewModel {
 var result: ClassificationResult? = nil
 var isLoading: Bool = false
 var error: Error? = nil

 private let classifier: ImageClassifierActor

 init(classifier: ImageClassifierActor) 

 func classify(_ image: CGImage) {
 isLoading = true
 error = nil
 Task {
 do {
 result = try await classifier.predict(image)
 } catch 
 isLoading = false
 }
 }
}

The view model is simple to test — inject a mock ImageClassifierActor, call classify(), and assert on the published state. No view rendering required. The Actor layer handles thread safety for the ML model itself — described in the next section.

The Actor pattern for Core ML integration

Core ML's MLModel is not thread-safe. Calling prediction() concurrently from multiple threads can cause crashes or silent incorrect results. Swift Actors solve this at the language level: the actor serializes access to its internal state, making the model thread-safe by construction.

actor ImageClassifierActor {
 private var model: ImageClassifier? = nil

 func predict(_ image: CGImage) async throws -> ClassificationResult {
 if model == nil {
 model = try await ImageClassifier.load()
 }
 guard let model else 
 let input = try ImageClassifierInput(image: image)
 let output = try model.prediction(input: input)
 return ClassificationResult(output)
 }
}

Two details matter here. First, the model is loaded lazily inside the actor — not at app launch. Model loading can take 50–500ms and should never block the main thread or delay app startup. Second, the actor satisfies Swift's Sendable requirements automatically, so the compiler will catch any accidental unsafecrossing of the actor boundary.

Apple's WWDC 2023 session "Discover Observation in SwiftUI" covers how @Observable integrates with Swift Concurrency — worth watching if you are migrating existing ObservableObject-based view models.

When TCA is the right choice

The Composable Architecture (TCA) forces every state mutation through an explicit Action type processed by a Reducer. All state is a value type. Side effects are isolated to Effect. This guarantees unidirectional data flow with no exceptions — which pays off in three specific contexts:

  • Complex multi-step flows. Checkout, onboarding, and multi-page wizards with back-navigation and partial-completion states are significantly easier to reason about in TCA than in MVVM, where state can drift from multiple sources.
  • Large engineering teams. TCA's strict structure prevents accidental state mutations that become harder to track as the team grows. Every change goes through the same Reducer path.
  • Exhaustive testing requirements. TCA's TestStore allows you to assert on every state transition in sequence — a level of test coverage that's impossible with observable classes.

The trade-off is real. TCA adds approximately 40–60% more code for equivalent functionality compared to MVVM + Observation. For a 3-person startup building a focused productivity app, that overhead ships slower and teaches the team a framework-specific mental model instead of standard Swift patterns.

Architecture decision matrix

| Context | Recommended pattern | | --- | --- | | Startup MVP, 1–3 engineers, simple flows | MVVM + @Observable | | App with on-device AI or Core ML inference | MVVM + @Observable + Actor | | Complex multi-step flows with undo/redo | TCA | | Large team (>5 engineers) needing strict conventions | TCA | | Shared global state across many screens | Observation + Environment | | iOS 16 compatibility required | MVVM + ObservableObject (no @Observable) |

Common architectural mistakes in SwiftUI apps

  • Business logic in views. SwiftUI views are an excellent rendering layer. They are a poor business logic container. Any logic that needs to be tested without rendering a view belongs in a view model or domain layer.
  • Singleton state managers. Global singletons for app-wide state feel convenient until you need to test components in isolation. Prefer dependency injection — pass view models and services explicitly, or inject via SwiftUI's environment.
  • Not designing for async state. Async operations — network calls, Core ML inference, SwiftData fetches — all produce loading, success, and error states. Designing views to handle all three states from the start results in significantly fewer production crashes than retrofitting error handling later.
  • Over-structuring early. Adding TCA or a custom module architecture to a project with one developer and one screen wastes time that should go toward shipping. Architecture is a response to real complexity — not a hedge against future complexity that may never arrive.

FAQ

Should I use MVVM or TCA for my SwiftUI app in 2026? For most iOS startups, Swift Observation with MVVM is the right default. Use TCA when your product has complex multi-step flows, extensive undo/redo, or a team that needs unidirectional data flow enforced by the compiler. TCA adds meaningful overhead in boilerplate and learning curve — if you don't need its guarantees, MVVM with @Observable ships faster and is easier to hand off.

How should Core ML inference be integrated into a SwiftUI architecture? Wrap your MLModel in a Swift Actor for thread-safe access. The Actor is injected into your view model — never called directly from a View. Your view model holds @Observable state for inference results and calls the Actor's async predict() method in a Task block. This keeps inference off the main thread and makes the ML layer independently testable.

Does @Observable replace Combine in SwiftUI apps? For most new code, yes. @Observable provides automatic fine-grained view invalidation without Combine's subscription boilerplate. Combine is still useful for complex event stream transformations, debouncing, and merging multiple async sources — but for simple view-model state binding, @Observable is less code and fewer bugs.

What happens to my SwiftUI architecture when I add AI features? AI features stress-test architecture assumptions. Models take 10–200ms to load, inference can take 5–100ms per call, and results may arrive during background tasks. The architecture must handle async model loading with loading states, inference cancellation when the user navigates away, and result caching. A well-structured Actor + ViewModel pattern handles all of this. Coupling ML directly to views doesn't.

Related reading

SwiftUI + Core ML Architecture Patterns Actor-based inference, async prediction, and model lifecycle management Swift Concurrency for AI Workloads Actors, AsyncStream, and Task priority for inference pipelines

Need an architecture review for your iOS app?

The iOS Architecture Audit covers system design, SwiftUI state management, Core ML integration readiness, and a prioritized roadmap — delivered in 5 business days.

Apply for an Audit

Related

Authoritative References