There is one name that is impossible to miss in architecture discussions in the SwiftUI era: TCA (The Composable Architecture), created by Point-Free.
If MVVM (Model-View-ViewModel) and Clean Architecture from the previous articles asked “How should we divide responsibilities?”, TCA asks a different question: “Can we control every path by which state changes with a single set of rules?”
Today, we’ll summarize how TCA’s unidirectional data flow works—and what you gain and what you pay for.
Three ingredients: State, Action, Reducer
In TCA, a single screen—or feature—is defined by three things.
- State: A single struct containing all the state for this feature
- Action: An enum containing every event that can occur in this feature—from button taps and responses arriving to notifications being received
- Reducer: A pure function that defines how the State changes when an Action arrives in the current State
@Reducer
struct Counter {
@ObservableState
struct State: Equatable {
var count = 0
}
enum Action {
case incrementTapped
case decrementTapped
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .incrementTapped:
state.count += 1
return .none
case .decrementTapped:
state.count -= 1
return .none
}
}
}
}
The View cannot change state directly. All it can do is send an Action to the Store. When the Store runs the Reducer and changes the State, the View renders the new State.
사건 발생 → Action 전송 → Reducer가 State 변경 → View 갱신
That one-way cycle is everything. There are no hidden paths for state changes. The debugging maze of “Where on earth did this value change?” disappears by design.
Side effects follow the rules too
Asynchronous work such as network requests is not performed directly by the Reducer; it is returned as an Effect. When the Effect finishes, its result comes back as an Action.
case .refreshTapped:
state.isLoading = true
return .run { send in
let posts = try await postClient.fetch()
await send(.postsLoaded(posts))
}
case .postsLoaded(let posts):
state.isLoading = false
state.posts = posts
return .none
Dependencies such as postClient are injected through TCA’s dependency injection system, so tests can replace them with a fake client. This is where TCA’s strongest weapon appears: TestStore.
let store = TestStore(initialState: Feed.State()) { Feed() }
await store.send(.refreshTapped) { $0.isLoading = true }
await store.receive(.postsLoaded(mockPosts)) {
$0.isLoading = false
$0.posts = mockPosts
}
For every Action, it forces you to specify exactly how the State should change. If even one unexpected state change occurs, the test fails. MVVM tests struggle to provide this level of completeness verification.
What is the cost?
This precision comes with a bill.
The learning curve is steep. Before becoming productive, you must learn many concepts, including Reducer, Effect, Store, Scope, and the dependency system. This is even more true for teams without a functional programming background.
There is boilerplate. Even changing a single value requires creating an Action case and adding a branch to the Reducer. This ritual is excessive for simple screens.
It is a third-party dependency. TCA is a library built by Point-Free, not Apple. TCA has gone through several major migrations to keep up with SwiftUI’s yearly changes, and every screen in the app ends up standing on this library. It is a mature, actively maintained project, but deciding to entrust the app’s skeleton to an external dependency is not a trivial choice.
What kind of team is it for?
The conditions under which TCA pays off are fairly clear.
- Apps with highly entangled state, such as collaborative documents, finance, complex forms, and real-time synchronization
- Domains where complete testing of state changes matters
- Teams familiar with functional concepts or able to invest in learning
Conversely, if most screens in an app simply “receive and display” data, Model-View (MV)/MVVM plus a UseCase, as covered in Part 3, is often enough. TCA is not the wrong choice; it is a costly choice, and it shines when state complexity justifies that cost.
Summary
- TCA defines features with State, Action, and Reducer, and enforces a single unidirectional cycle for all state changes.
- Side effects also enter the rules through Effect → Action, while TestStore verifies the completeness of state changes.
- The cost is the learning curve, boilerplate, and the decision to entrust the app’s skeleton to a third party.
- It is an architecture with a clear trade-off: the higher the app’s state complexity, the greater the benefit.
Only the final question in the series remains. Among all these choices, from MVC to TCA, what should our team choose? In the next article, we’ll wrap up the series by organizing the criteria for making that choice.

![Cover image for [iOS Architecture #7] TCA and Unidirectional Data Flow](/assets/images/posts/caac9fa2-c44c-4646-87b0-01421048db10/1.jpg)