在 SwiftUI 时代的架构讨论中,有一个名字无法绕开:Point-Free 打造的 TCA(The Composable Architecture)。
如果前几篇介绍的 MVVM(Model-View-ViewModel)和 Clean Architecture 讨论的是“如何划分职责”,那么 TCA 提出的问题就不同了:“能否用一套规则控制状态变化的所有路径?”
今天我们来总结 TCA 的单向数据流如何工作,以及我们能获得什么、又要付出什么。
三种材料:State、Action、Reducer
在 TCA 中,一个界面(或一个功能)由三部分定义。
- State:保存该功能全部状态的一个结构体
- Action:包含该功能可能发生的所有事件的 enum,包括按钮点击、响应到达和收到通知等
- Reducer:定义“当前 State 收到此 Action 后,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
}
}
}
}
View 不能直接修改状态。它能做的只有向 Store 发送 Action。Store 运行 Reducer 并修改 State 后,View 会渲染更新后的 State。
사건 발생 → Action 전송 → Reducer가 State 변경 → View 갱신
这条单向循环就是全部。状态不存在其他隐蔽的变化路径。“这个值到底在哪里变了?”这样的调试迷宫会从结构上消失。
副作用也纳入规则
网络请求等异步操作不会由 Reducer 直接执行,而是以 Effect 的形式返回。Effect 完成后,结果会再次作为 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
postClient 等依赖会通过 TCA 的依赖注入系统注入,因此测试时可以替换为伪客户端。TCA 最强大的武器就在这里登场了: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
}
对于每个 Action,它强制你完整明确地指定 State 应该如何变化。只要出现一次未预期的状态变化,测试就会失败。MVVM 测试很难达到这种完整性验证水平。
代价是什么?
这种精确性也伴随着成本。
**学习曲线很陡。**在获得生产力之前,需要先学习 Reducer、Effect、Store、Scope 和依赖系统等许多概念。对于没有函数式编程背景的团队来说更是如此。
**存在样板代码。**即使只修改一个值,也需要创建 Action case,并在 Reducer 中添加分支。对于简单界面来说,这种仪式过于繁琐。
**它是第三方依赖。**TCA 是 Point-Free 而非 Apple 开发的库。为了适配每年变化的 SwiftUI,TCA 也经历了多次重大迁移,应用中的所有界面最终都会建立在这个库之上。它是一个活跃维护的成熟项目,但决定把应用的骨架交给外部依赖,本身并不是一个轻松的决定。
它适合什么样的团队?
TCA 能发挥价值的条件相当明确。
- 状态复杂交织的应用,例如协作文档、金融、复杂表单和实时同步
- 完整测试状态变化非常重要的领域
- 熟悉函数式概念,或能够投入学习成本的团队
反过来,如果应用的大多数界面只是“接收并展示”,那么像第 3 篇介绍的 MV(Model-View)/MVVM 加上 UseCase,通常就足够了。TCA 不是错误的选择,而是昂贵的选择;当状态复杂度足以证明这项成本合理时,它才能发挥价值。
总结
- TCA 使用 State、Action 和 Reducer 定义功能,并强制所有状态变化路径遵循一个单向循环。
- 副作用也会通过 Effect → Action 纳入规则,TestStore 则验证状态变化的完整性。
- 代价是学习曲线、样板代码,以及把应用骨架交给第三方的决定。
- 这是一种权衡明确的架构:应用的状态越复杂,收益就越大。
系列只剩最后一个问题。从 MVC 到 TCA,在这么多选项中,**我们的团队应该选择什么?**下一篇将整理选择标准,为这个系列收尾。

![[iOS 架构 #7] TCA 与单向数据流 封面图](/assets/images/posts/caac9fa2-c44c-4646-87b0-01421048db10/1.jpg)