Architecture · 12 min read

TCA + SwiftUI in Production: A Senior iOS Developer's Guide (2026)

A practical, production-tested guide to The Composable Architecture (TCA) with SwiftUI: when to use it, how to structure features, testing, performance, and migration from MVVM.

By Vitalii Derhunov · Published July 17, 2026

Why TCA, and why now

After seven years shipping iOS apps — from BLE-driven hardware companions to AI-powered travel apps — one architectural pattern has consistently held up under real product pressure: The Composable Architecture (TCA). It enforces the boring discipline that ad-hoc MVVM projects lose within six months: one source of truth, explicit effects, and features that compose instead of leaking into each other.

The core loop

Every TCA feature is the same four pieces: State, Action, Reducer, and Effect. The reducer is a pure function; the store owns time. This is what makes it testable — you never mock a view model, you assert on state transitions.

@Reducer
struct Counter {
  @ObservableState
  struct State: Equatable { var count = 0 }

  enum Action { case increment, decrement }

  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .increment: state.count += 1; return .none
      case .decrement: state.count -= 1; return .none
      }
    }
  }
}

When TCA earns its keep

  • Multi-screen flows with shared state (onboarding, checkout, wizards).
  • Long-lived async work: BLE, WebSockets, HealthKit streams, GraphQL subs.
  • Teams of 3+ iOS engineers — module boundaries stop being a suggestion.
  • Anything you need to snapshot-test or replay for QA.

For a single-screen utility app, plain SwiftUI with @Observable is fine. Don't cargo-cult TCA into a weekend project.

Structuring a feature

I keep each feature in its own Swift Package with three targets: FeatureCore (reducer + models), FeatureUI (SwiftUI views), and FeatureTests. The app target only wires stores together. This is the single change that has saved the most compile time on every project I've inherited.

Testing effects

TestStore is TCA's superpower. You send actions, assert exact state deltas, and control the clock. No XCTest expectations, no flaky timing.

@MainActor
func testIncrement() async {
  let store = TestStore(initialState: Counter.State()) { Counter() }
  await store.send(.increment) { $0.count = 1 }
  await store.send(.increment) { $0.count = 2 }
}

Performance in practice

With @ObservableState (TCA 1.7+), SwiftUI only re-renders views that read the exact fields that changed. In one client migration from vanilla MVVM, we cut list-scroll body invocations by ~80% without touching the view code — the store did the pruning.

Migrating from MVVM

  1. Wrap one leaf screen in a Reducer. Leave everything else alone.
  2. Move network calls into Effect.run with a dependency client.
  3. Replace the view model with @Bindable var store.
  4. Write a TestStore test before deleting the old code.
  5. Only then move up the tree.

Common mistakes I see

  • God reducers — split by feature, compose with Scope.
  • Business logic in views — if it's not in the reducer, it's not testable.
  • Ignoring dependencies — use @Dependency, not singletons.
  • Fighting navigation@Presents + PresentationAction is the answer.

Wrapping up

TCA isn't a silver bullet — it's a contract. It trades a bit of upfront ceremony for a codebase that survives feature growth, team turnover, and the inevitable "just add one more thing" from product. On every non-trivial iOS project I've led, that trade has paid for itself within the first quarter.