从Apple 官方的SwiftUI 数据流转,可以知道SwiftUI是数据驱动UI,那么经典的MVC、MVVM还能适用吗?
一、背景与动机
随着 SwiftUI 在项目中的逐步落地,界面声明式化大大降低了 UI 编写成本,但也带来了新的问题:
状态分散在多个 @State / @ObservedObject / @EnvironmentObject
业务逻辑混杂在 View 中,难以测试
页面之间状态流转不透明,难以追踪
异步请求、副作用(网络、埋点、权限)缺乏统一管理方式当业务规模扩大后,这些问题会迅速放大,最终导致:
UI 可写,但业务不可控;页面能跑,但系统难维护
TCA(The Composable Architecture) 正是为了解决这些问题而诞生。
二、TCA 是什么?
TCA 是一套 单向数据流(Unidirectional Data Flow)架构,核心目标是:
让状态、行为和副作用全部显式化、可组合、可测试
在 TCA 中,一个功能模块由以下四个核心部分组成:
State —— 描述当前状态
Action —— 描述发生了什么
Reducer —— 决定状态如何变化
Effect —— 管理异步与副作用这四者构成了一个完整、可预测的闭环系统。
跟前端Redux很像
三、核心架构模型
1️⃣ State(状态)
State 是 页面或模块的唯一真相来源(Single Source of Truth)。
struct LoginState: Equatable {
var username: String = ""
var password: String = ""
var isLoading: Bool = false
var errorMessage: String?
}
设计原则:
只描述「是什么」,不描述「怎么来的」
必须是值类型(struct)
尽量保持扁平、可组合⸻
2️⃣ Action(行为)
Action 用于描述 用户行为、系统事件或异步结果。
enum LoginAction: Equatable {
case usernameChanged(String)
case passwordChanged(String)
case loginButtonTapped
case loginResponse(Result<User, LoginError>)
}
设计原则:
描述“发生了什么”,而不是“要做什么”
所有状态变化都必须由 Action 触发
异步结果必须回流为 Action⸻
3️⃣ Reducer(状态机)
Reducer 是一个 纯函数:
let loginReducer = Reducer<LoginState, LoginAction, LoginEnvironment> {
state, action, environment in
switch action {
case let .usernameChanged(text):
state.username = text
return .none
case .loginButtonTapped:
state.isLoading = true
return environment.authService
.login(state.username, state.password)
.map(LoginAction.loginResponse)
case let .loginResponse(.success(user)):
state.isLoading = false
return .none
case let .loginResponse(.failure(error)):
state.isLoading = false
state.errorMessage = error.localizedDescription
return .none
}
}
Reducer 的职责:
同步修改 State
决定是否产生 Effect
不允许直接执行副作用⸻
4️⃣ Effect(副作用)
Effect 用于处理:
网络请求
定时器
本地存储
埋点 / 权限 / 系统 API
environment.authService
.login(username, password)
.map(LoginAction.loginResponse)
原则:
Reducer 不能直接访问系统 API
所有副作用必须注入 Environment
副作用的结果必须回到 Action四、Store 与 View 关系
Store 是什么?
Store 是 State + Reducer + Environment 的运行容器:
let store = Store(
initialState: LoginState(),
reducer: loginReducer,
environment: LoginEnvironment(...)
)
SwiftUI View 只做三件事:
读取 State
发送 Action
根据 State 渲染 UI
struct LoginView: View {
let store: Store<LoginState, LoginAction>
var body: some View {
WithViewStore(store) { viewStore in
VStack {
TextField(
"Username",
text: viewStore.binding(
get: \.username,
send: LoginAction.usernameChanged
)
)
Button("Login") {
viewStore.send(.loginButtonTapped)
}
}
}
}
}
📌 View 永远不直接修改状态
📌 View 永远不直接调用业务逻辑五、模块拆分与组合(Composable)
TCA 的最大优势在于 可组合性。
子模块 State 嵌套
struct AppState: Equatable {
var loginState: LoginState?
var homeState: HomeState?
}
子模块 Action 嵌套
enum AppAction {
case login(LoginAction)
case home(HomeAction)
}
Reducer 组合
let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
loginReducer.optional().pullback(
state: \.loginState,
action: /AppAction.login,
environment: \.loginEnvironment
),
homeReducer.optional().pullback(
state: \.homeState,
action: /AppAction.home,
environment: \.homeEnvironment
)
)
这使得:
页面可以独立开发
模块可以自由组合 / 拆卸
架构天然支持大型项目六、TCA 与 SwiftUI 的边界约定
View 层职责(只能做的事)
UI 渲染
状态绑定
Action 转发禁止事项 ❌
View 中直接发起网络请求
View 中保存业务状态
View 中执行复杂逻辑判断七、测试能力(TCA 的核心优势)
TCA 的 Reducer 是纯函数,因此可以做到 100% 可预测测试。
let store = TestStore(
initialState: LoginState(),
reducer: loginReducer,
environment: .mock
)
store.send(.loginButtonTapped) {
$0.isLoading = true
}
store.receive(.loginResponse(.success(user))) {
$0.isLoading = false
}
无需启动 App,即可验证完整业务流程。
八、为什么选择 TCA?
维度MVVMTCA
状态来源分散单一
数据流双向单向
可测试性一般极强
组合能力弱强
大型项目难维护天然适合
九、总结
TCA 不是为了写代码更快,而是为了:
让状态变化可预测
让副作用可追踪
让业务逻辑可测试
让架构在复杂度上升时依然稳定当 SwiftUI 负责「怎么显示」,
TCA 负责「为什么这样显示」。