2026-04-27

从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 负责「为什么这样显示」。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容