UI 架构模式解决的是“状态、事件、副作用和导航如何在界面边界协作”,不是替代业务分层、数据库事务或网络治理。MVC、MVVM、Coordinator、Redux/UDF、TCA 和 VIPER 都能写出可靠系统,也都能因状态多份、依赖隐藏和生命周期失控而失败。选择模式前,先明确 State Owner、Effect Owner、Navigation Owner 和 Feature Scope,再比较模式带来的约束是否值得团队承担。
一、问题背景:同一功能可以有多种正确结构
考虑一个文章 Feature:
- 列表首次加载、下拉刷新和分页;
- 点击文章进入详情;
- 收藏使用乐观更新,失败后对账;
- Deep Link 可直接打开详情;
- 未登录时先展示登录,再继续原导航意图;
- 页面退出后取消只属于页面的请求;
- 离线状态展示本地数据与同步状态。
不同 UI 架构都必须回答相同问题:
- 列表、选中项、加载和错误状态由谁拥有?
- 用户点击如何变成业务 Command?
- 网络、数据库观察、Timer 和 Analytics 在哪里执行?
- Effect 完成后如何避免旧结果覆盖新状态?
- 导航是命令还是可恢复状态?
- 页面、Scene、账号和 App 级对象分别活多久?
- 单元测试替换哪些边界,如何控制时间和异步顺序?
如果这些问题没有答案,换成更流行的模式只会改变类型名称。
本文以 Swift 6、iOS 17+ 为示例基线,同时覆盖 UIKit 与 SwiftUI。The Composable Architecture(TCA)是持续演进的第三方架构库;本文只讲其稳定概念,不绑定某个版本的具体宏或 API。采用时应以项目锁定版本的官方文档和 Migration Guide 为准。
核心结论
- MVC 的问题不是 Controller 必然庞大,而是职责未拆分;小型页面使用 MVC 往往最直接。
- MVVM 将 View State 与展示逻辑移到 ViewModel,但不会自动提供单向数据流、导航治理、依赖注入或副作用取消。
- Coordinator 把跨页面流程和 UIKit 转场所有权上移,适合与 MVC/MVVM/UDF 组合;它不应成为第二个业务 Store。
- Redux/UDF 用
State -> View -> Action -> Reducer -> State建立单向更新,Reducer 应保持纯净,Effect 独立执行并回送 Action。 - TCA 将 State、Action、Reducer、Effect、Dependency、Store、组合和测试形成统一工具体系;收益来自一致约束,代价是框架学习、建模和版本迁移。
- VIPER 通过 View、Interactor、Presenter、Entity、Router 强拆职责,适合边界稳定、团队协作严格的复杂 Feature,不适合无差别套用到简单页面。
- 状态只能有一个权威 Owner,其他层持有 Snapshot、Binding 或 Derived State。两份可写状态最终会互相覆盖。
- Side Effect 必须有身份、生命周期、取消、错误和结果回流规则,不能偷偷写在 Reducer、Computed Property 或 View Render 中。
- Navigation State 应保存语义 Route 和稳定 ID,不保存 View Controller、ViewModel 或完整响应;UIKit 命令栈还需与 Route State 对账。
- 测试替身应放在 I/O、时间、UUID、权限和导航边界,不必为每个纯类型制造 Protocol。
- 模式选择取决于 Feature 状态复杂度、并发流程、团队规模和长期维护成本,而不是文件数量或简历关键词。
二、模式不是分层:先确定共同边界
MVC、MVVM、UDF 等主要改变 UI Pattern Boundary 内部结构。外部依然需要 Use Case、Repository、网络与存储边界。把 URLSession 或 Core Data 直接搬进 ViewModel/Reducer,不会因为类型名变了就符合分层。
2.1 选择前先量化复杂度
- 独立可写状态有多少,是否存在 Impossible State?
- 同时运行的 Effect 有多少,是否需要取消/合并/防抖?
- 页面是否支持 Deep Link、状态恢复和多步骤流程?
- 多人是否频繁修改同一 Feature?
- 是否需要精确重放 Action 和断言状态演进?
- 团队是否能稳定维护 Reducer、Coordinator 或模块契约?
简单静态详情页与离线协同编辑器,不应承担相同架构成本。
三、MVC:框架对象明确,职责需要主动拆分
经典 MVC:
- Model:领域数据和业务能力,不等于网络 DTO;
- View:显示与输入,不决定业务;
- Controller:协调生命周期、事件、Model 和 View。
UIKit 天然以 UIViewController 为生命周期和容器边界,因此 MVC 符合平台结构。Massive View Controller 是实现失控的结果,不是 MVC 的定义。
3.1 Controller 应保留什么
- View Lifecycle 与 View Hierarchy;
- 输入事件适配;
- 请求 Use Case 并提交 UI State;
- Child Controller 和 Transition 协调;
- 页面级 Task/Subscription 的 Owner。
应移出:
- JSON/数据库映射;
- 跨页面业务流程;
- 复杂领域规则;
- 重试、Token Refresh、离线同步;
- 可复用格式化和纯状态转换。
3.2 适用边界
适合状态少、交互线性、UIKit 生命周期主导的页面。若 Controller 同时维护复杂表单、多个异步 Effect 和可恢复导航,继续拆成 ViewModel/Store 或引入 Coordinator 会更清楚。
四、MVVM:View State 与展示逻辑的 Owner
MVVM 通常让 ViewModel:
- 接收 View Event;
- 调用 Use Case/Repository;
- 将 Domain Value 映射为 View State;
- 持有页面 Task 与订阅;
- 输出可观察状态。
View 只绑定状态并发送意图。SwiftUI 的声明式更新很容易与 MVVM 配合,但 Observable/@Observable 本身不是 MVVM,也不决定状态所有权。
import Observation
@MainActor
@Observable
final class ArticleDetailViewModel {
enum State: Equatable {
case loading
case content(ArticleDetailState)
case failed(message: String)
}
private let load: LoadArticleUseCase
private let toggleFavorite: ToggleFavoriteUseCase
private var task: Task<Void, Never>?
private(set) var state: State = .loading
init(
load: LoadArticleUseCase,
toggleFavorite: ToggleFavoriteUseCase
) {
self.load = load
self.toggleFavorite = toggleFavorite
}
func appeared(articleID: UUID) {
task?.cancel()
task = Task {
do {
let article = try await load.execute(id: articleID)
try Task.checkCancellation()
state = .content(ArticleDetailState(article))
} catch is CancellationError {
return
} catch {
state = .failed(message: present(error))
}
}
}
deinit { task?.cancel() }
}4.1 MVVM 不自动解决的问题
- ViewModel 可以照样变成 Massive ViewModel;
- 双向 Binding 可形成隐式写入和反馈循环;
- 多个 ViewModel 可能复制同一业务状态;
- Navigation 若散落在 View Closure 中仍不可恢复;
- Effect 若没有 ID/取消策略仍会发生旧结果覆盖;
- Repository/Use Case 依赖仍需 Composition Root 注入。
当 Event 和 State Transition 数量增加,可在 ViewModel 内引入显式 Reducer,而不必立刻采用完整 Redux 框架。
五、Coordinator:流程和导航的所有者
Coordinator 常与 MVC/MVVM 组合,用于:
- 创建页面并注入依赖;
- Push/Present/Dismiss;
- 多步骤流程,如登录后继续结算;
- Deep Link 解析后的导航决策;
- Child Coordinator 生命周期;
- Scene-scope Navigation State 与恢复。
@MainActor
protocol ArticleListRouting: AnyObject {
func showArticle(id: UUID)
func showLogin(continuation: RouteIntent)
}
@MainActor
final class ArticleCoordinator: ArticleListRouting {
private let navigationController: UINavigationController
private let factory: ArticleFeatureFactory
private var children: [UUID: AnyObject] = [:]
func showArticle(id: UUID) {
let controller = factory.makeDetail(articleID: id)
navigationController.pushViewController(controller, animated: true)
}
}5.1 Coordinator 不应拥有第二份业务状态
Coordinator 可保存 Route/Flow State,但 Article 内容、收藏和同步状态仍由 Repository/Feature Store 拥有。若 Coordinator、ViewModel 和 View Controller 都能修改同一个字段,会形成多个事实源。
5.2 生命周期陷阱
- Parent 强持有 Child,Child 完成后必须移除;
- Closure/Delegate 避免 Child 与 Parent 循环强引用;
- Interactive Pop 可能取消,不能只在
viewWillDisappear判断完成; - 多 Scene 不应扫描全局“当前 Navigation Controller”;
- UIKit Stack 与 Route State 要在手势返回、恢复和外部 Deep Link 后对账。
六、Redux 与 Unidirectional Data Flow
Unidirectional Data Flow(UDF)通常包含:
State -> View -> Action -> Reducer -> New State -> View
-> Effect -> ActionState 是唯一快照,Action 描述发生了什么,Reducer 是同步纯函数,Effect 执行外部工作并把结果重新包装为 Action。
struct ArticleFeatureState: Sendable, Equatable {
var articles: [Article] = []
var isLoading = false
var error: ArticleFeatureError?
var requestRevision = 0
}
enum ArticleFeatureAction: Sendable, Equatable {
case reload
case response(revision: Int, Result<[Article], ArticleFeatureError>)
case cancel
}
enum ArticleFeatureEffect: Sendable, Equatable {
case load(revision: Int)
case cancelLoad
}
func reduce(
state: inout ArticleFeatureState,
action: ArticleFeatureAction
) -> [ArticleFeatureEffect] {
switch action {
case .reload:
state.requestRevision &+= 1
state.isLoading = true
state.error = nil
return [.cancelLoad, .load(revision: state.requestRevision)]
case let .response(revision, result):
guard revision == state.requestRevision else { return [] }
state.isLoading = false
switch result {
case let .success(articles): state.articles = articles
case let .failure(error): state.error = error
}
return []
case .cancel:
state.requestRevision &+= 1
state.isLoading = false
return [.cancelLoad]
}
}Reducer 不执行网络、不读取当前时间、不生成随机 UUID,也不捕获 View。它只根据旧 State 和 Action 产生新 State 与 Effect Description,因此可确定性测试。
6.1 UDF 的收益
- 状态写入路径集中;
- Action 日志便于复现;
- 旧异步结果可用 Revision/Effect ID 拒绝;
- 父子 Feature 组合与状态恢复更明确;
- Reducer 测试不依赖 UI/网络。
6.2 UDF 的成本
- Action 和 State 可能膨胀;
- 简单 Binding 需要转成 Action;
- 大 State 的观察粒度和拷贝语义需要设计;
- Effect Runtime、取消 ID 和依赖环境需要统一;
- Debug Log 可能泄露敏感 State/Action;
- 不恰当的 Global Store 会扩大更新与团队耦合。
UDF 不等于所有 App 只有一个 Store。通常按 Feature/Scene 切分 Local Store,再由 Parent 组合必要 State 和 Action。
七、The Composable Architecture:完整约束体系
TCA 以 UDF 为基础,提供一套组合式工具来表达:
- Feature
State与Action; - Reducer 与 Child Feature Composition;
- Effect、Cancellation 和 Dependency;
- Store 与 View Observation;
- Navigation/Presentation State;
- Test Store 驱动 Action、State 与 Effect 断言。
本文只使用这些概念。具体 @Reducer、Observation、Dependency 和 Navigation API 会随版本变化,代码应以锁定的 Package Version 为准。
7.1 适用场景
- Feature 多、异步流程复杂且需要一致的 Effect Cancellation;
- 团队愿意统一 State/Action/Reducer 建模;
- 高度重视确定性测试和 Feature Composition;
- Navigation、Sheet、Alert 也希望纳入状态驱动;
- 可承担框架升级与培训。
7.2 不适用或谨慎场景
- 极小页面、生命周期简单;
- 团队尚未理解 UDF,却只复制模板;
- 已有稳定架构,迁移收益不明确;
- 业务状态被强行全部放进单个巨大 State;
- 第三方框架依赖政策或版本升级成本不可接受。
采用 TCA 不会替代 Repository、数据库 Transaction、请求幂等和 Domain Rule。Reducer 中也不应直接操作 NSManagedObject 或把非 Sendable SDK 对象塞进 State。
八、VIPER:强边界适合强协作需求
VIPER 常见职责:
- View:渲染与输入;
- Interactor:业务用例;
- Presenter:把 View Event 与 Interactor Result 转换;
- Entity:业务数据;
- Router:导航和模块组装。
8.1 收益
- 角色和接口明确,便于多人并行;
- View 与业务/导航可独立测试;
- 大型 Feature 的职责审计直接;
- Router 提供清晰模块入口。
8.2 代价
- 类型和 Wiring 多,简单页面样板显著;
- Presenter/Interactor 容易出现一行转发;
- Entity 的归属可能与真正 Domain Module 冲突;
- Protocol 过细会让测试和重构脆弱;
- Router 若同时组装依赖和持有全局状态会失控。
VIPER 更适合高复杂度、边界稳定、团队已有统一生成/审查规范的模块。对只展示一个静态列表的 Feature,全套 VIPER 往往成本高于收益。可以吸收其职责思想,而不必逐字母建类型。
九、状态所有权:所有模式的共同底座
9.1 按生命周期确定 Owner
| 状态 | 推荐 Owner | 不应由谁长期拥有 |
|---|---|---|
| 登录账号/权限 | Account Session | Cell、临时页面 |
| 文章事实与同步状态 | Repository/Domain Store | ViewModel 的复制数组作为权威源 |
| 当前导航路径 | Scene Coordinator/Feature Store | 全局 Singleton、任意 Child View |
| 页面筛选和加载 | Feature Model/Store | Repository 全局状态 |
| 输入框焦点/动画进度 | View/Presentation | Domain Entity |
| 格式化文本 | Derived View State | 重复持久化为第二事实源 |
9.2 借用不等于拥有
SwiftUI @Environment、Binding 或 UIKit Delegate 只提供访问通道,不决定创建和销毁。Owner 必须强持有状态并定义 Scope;Child 借用后不能反向决定全局生命周期。
9.3 避免双向同步两份 State
如果 ViewModel 和 Coordinator 各有 selectedArticleID,再用 Notification 相互同步,就已经有竞态。选择一个 Owner,另一个接收 Snapshot/Binding 或发送 Action。Derived State 应从源状态计算,不再可写保存。
十、Side Effect:把外部世界隔离在可取消边界
Side Effect 包括:
- 网络、数据库、文件、Keychain;
- Clock、Timer、Random、UUID;
- Analytics、日志、通知;
- Navigation、Clipboard、Haptic;
- 系统权限和第三方 SDK Callback。
每个 Effect 应有:
Trigger Action
Owner / Scope
Effect ID
Input Snapshot
Cancellation Policy
Timeout / Retry Policy
Result Action
Stale Result Guard
Cleanup10.1 Reducer 纯净,Effect Runner 执行
UDF 中 Reducer 返回 Effect Description,Runtime 执行后发送 .response。MVVM 中 ViewModel 可持有 Task,但仍应把网络细节委托给 Use Case。MVC 中 Controller 同样应持有页面 Task,并在生命周期结束取消。
10.2 Effect 不应藏在哪里
- SwiftUI
body或 UIKitlayoutSubviews; - Computed Property / Formatter;
- Reducer 同步函数;
- Domain Entity Setter;
didSet中隐式网络请求;- 无 Owner 的 Fire-and-forget
Task {}。
Analytics 也是 Effect。Action/State 全量日志可能包含 Token、搜索词和个人数据,应使用字段允许列表和采样,而不是默认记录整个 State。
十一、Navigation State:命令式栈与声明式路径
Navigation 有两种常见表达:
- Command-driven:Coordinator 执行 Push/Present;
- State-driven:Route/Path/Sheet State 描述当前导航树,UI 与状态同步。
enum ArticleRoute: Hashable, Codable, Sendable {
case detail(articleID: UUID)
case edit(articleID: UUID)
case login(continuation: RouteIntent)
}Route 保存重建页面所需的稳定 ID,不保存 UIViewController、ViewModel、Repository 或完整 Article。恢复时还要重新验证账号、权限和实体是否存在。
11.1 谁拥有 Navigation
- App Root:登录/登出导致的 Root Flow;
- Scene:每个 Window 的独立路径;
- Feature:Feature 内部页面和 Modal;
- View:仅局部、短生命周期 Presentation State。
UIKit Coordinator 可维护语义 Route State,但必须在 Interactive Pop、系统 Dismiss 和状态恢复后与实际 Stack 对账。SwiftUI NavigationStack 虽然 State-driven,也要防多处同时写 Path 和 Sheet。
11.2 Deep Link 是 Intent,不是直接 Push
Deep Link Parser 输出 Typed Intent;Navigation Owner 检查 Session、Feature Flag、权限和数据,再决定 Route。若需登录,保存可验证的 Continuation Intent,而不是保存旧 View Controller Closure。
十二、测试替身:替换不确定边界
高价值替身包括:
- Repository/Use Case Stub;
- Test Clock;
- Deterministic UUID/Random;
- Permission Client;
- Navigation Spy;
- Analytics Recorder;
- Controllable Effect Gate。
protocol ArticleLoading: Sendable {
func load() async throws -> [Article]
}
actor ArticleRepositoryStub: ArticleLoading {
private let result: Result<[Article], ArticleError>
private(set) var loadCount = 0
init(result: Result<[Article], ArticleError>) {
self.result = result
}
func load() async throws -> [Article] {
loadCount += 1
return try result.get()
}
}Test Double 应符合真实 Contract,包括取消、顺序、错误和 Actor 隔离。一个永远同步成功的 Fake 无法验证旧请求覆盖、Loading 中取消和并发 Refresh。
12.1 各模式的测试重点
| 模式 | 重点 |
|---|---|
| MVC | Lifecycle Event、Controller-View Adapter、Task 清理 |
| MVVM | Event -> View State、取消、旧结果、错误映射 |
| Coordinator | Route Decision、Child 生命周期、交互返回对账 |
| UDF/Redux | Action -> State + Effect、Effect Result、取消 ID |
| TCA | Reducer Composition、Dependency、Effect 与 Navigation State |
| VIPER | Presenter/Interactor Contract、Router Invocation、Wiring |
Snapshot/UI Test 验证渲染,不替代 Reducer/Use Case 的业务测试。End-to-end Test 验证组装,不应承担所有错误组合。
十三、模式横向比较
| 维度 | MVC | MVVM | Coordinator | Redux/UDF | TCA | VIPER |
|---|---|---|---|---|---|---|
| 核心关注 | Controller 协调 | View State/展示逻辑 | 流程与导航 | 单向状态演进 | 完整组合式 UDF 工具 | 强角色分离 |
| 状态集中度 | 取决于实现 | ViewModel 级 | 不应拥有业务状态 | Store/Feature State | Store/Feature State | Presenter/View State |
| Effect 约束 | 自行设计 | 自行设计 | 主要导航 Effect | 明确 Effect Runtime | 框架统一 | Interactor 执行 |
| 导航 | Controller/外接 Router | 常需 Coordinator | 核心职责 | Action/Route State | 状态化工具 | Router |
| 测试粒度 | 中等 | ViewModel 较好 | 路由决策明确 | Reducer 高确定性 | 工具体系完整 | 角色接口明确 |
| 样板/学习 | 低 | 低到中 | 中 | 中到高 | 高 | 高 |
| 适合 | 简单 UIKit 页面 | 中等 Feature | 多页面流程 | 复杂状态与 Effect | 统一 UDF 团队 | 强边界大模块 |
Coordinator 不是 MVC/MVVM/UDF 的替代项,而是可横向组合的 Navigation Pattern。TCA 是对 UDF 的具体、完整工具实现;采用它意味着接受库的约束和升级成本。
十四、团队复杂度成本
14.1 认知成本
- 新成员需要理解多少角色和数据流?
- 从 Button Tap 到 Network Effect 需要跳转多少文件?
- 错误和取消路径是否一眼可追踪?
- 架构词汇是否在团队中含义一致?
14.2 协作成本
- 多人修改同一个 Giant Reducer/ViewModel/Presenter 是否冲突?
- Feature 是否有 Owner 和 Public Contract?
- 代码生成与模板是否掩盖实际职责?
- 架构升级是否需要全仓同步修改?
14.3 运行与构建成本
- Global Store 是否造成无关观察更新?
- State 是否包含大对象导致比较/复制/日志成本?
- Protocol/Generic/宏组合是否影响增量构建?
- Effect Runtime 是否产生悬空任务或重复订阅?
不要凭感觉断言某模式“性能更好”。应测量 SwiftUI Body Evaluation、UIKit Update、Reducer 耗时、State Observation、内存和构建时间。多数 UI 卡顿的根因仍可能是图片、布局、数据库 Fetch 或主线程 I/O。
十五、方案选择方法
15.1 小型 UIKit 页面
选择 MVC + Use Case,必要时外接 Coordinator。Controller 保持生命周期和 UI 协调,业务规则移出。不要为一个静态页面创建完整 Store 或 VIPER 五件套。
15.2 中等 SwiftUI Feature
MVVM 或局部 UDF 都合适。状态少、交互线性时使用 MainActor ViewModel;Action 和并发 Effect 增长后引入 Reducer、Effect ID 和 Route State。
15.3 大型状态密集 Feature
Redux/UDF 或 TCA 能提供一致状态流、Effect Cancellation 和确定性测试。按 Feature 切分 State/Reducer,不创建整个 App 的巨型 Store;Domain/Data 仍保留独立边界。
15.4 多步骤跨模块流程
使用 Coordinator/Router 作为上层流程 Owner。内部 Feature 可各自使用 MVC、MVVM 或 UDF,通过 Typed Route/Result 通信。
15.5 强流程团队与稳定模块
VIPER 可提供严格角色和并行协作,但应审查每个角色是否有真实职责。简单模块可采用轻量变体,不以模板完整度作为质量指标。
十六、渐进式演进
不要一次重写整个 App:
- 找到重复可写状态,指定唯一 Owner;
- 把网络/数据库从 View/Controller 移到 Use Case/Repository;
- 为页面 Task 加 Owner、Cancellation 和 Revision;
- 把跨页面 Push/Present 上移 Coordinator 或 Route State;
- 将复杂 ViewModel Transition 提取成纯 Reducer;
- 为 Clock、UUID、Repository、Navigation 注入 Test Double;
- 在新复杂 Feature 试点 UDF/TCA/VIPER,记录缺陷率、测试速度和开发成本;
- 只有证据支持时扩大采用范围。
模式可以共存,但同一个 Feature 内的 State Mutation 规则应统一。最危险的组合不是“UIKit + SwiftUI”,而是一半双向 Binding、一半 Reducer、一半 Notification 都能改同一状态。
十七、常见误区与修复
17.1 ViewModel 等于所有非 View 代码
问题: 网络、存储、业务、导航和日志全部进入 ViewModel,形成 Massive ViewModel。
修复: ViewModel 只管 View State 与事件,业务交给 Use Case,数据交给 Repository,导航交给 Route Owner。
17.2 Coordinator 保存业务对象副本
问题: Coordinator 与 Repository/Store 形成双事实源,返回页面后状态互相覆盖。
修复: Coordinator 保存 Route 和稳定 ID,业务对象由领域 Store 拥有。
17.3 Reducer 内直接启动 Task
问题: Reducer 不再纯净,测试、取消和结果顺序不可控。
修复: Reducer 返回 Effect Description,由 Runtime 执行并发送 Result Action。
17.4 全 App 只有一个巨大 Store
问题: 状态、观察和团队修改耦合,任意 Feature 都能访问过宽数据。
修复: 按 App/Scene/Feature Scope 组合 Local Store,Public State/Action 最小化。
17.5 用框架替代业务建模
问题: TCA/VIPER/MVVM 只规定协作方式,不理解幂等、权限、冲突和事务。
修复: Domain Rule、Use Case、Repository 和服务端协议继续独立设计。
十八、发布前检查清单
18.1 状态与事件
- 每份可写状态是否只有一个权威 Owner;
- View State 与 Domain State 是否分离;
- Derived State 是否避免重复持久化;
- Event/Action 是否表达事实或用户意图;
- Impossible State 是否通过 Enum/State Machine 消除。
18.2 Effect 与生命周期
- 每个 Effect 是否有 Owner、ID、取消和 Result;
- 页面结束是否清理 Task/Subscription;
- 旧结果是否通过 Revision/Identity 拒绝;
- Timeout/Unknown Outcome 是否进入对账,而非伪装失败;
- Analytics/Action Log 是否脱敏。
18.3 导航与测试
- Navigation Owner 是否符合 App/Scene/Feature Scope;
- Route 是否只包含稳定 ID,可恢复并重新验证权限;
- UIKit 手势返回是否与 Route State 对账;
- Test Clock、Repository、Navigation 和 Effect Gate 是否可注入;
- 模式样板和学习成本是否与 Feature 复杂度匹配。
十九、总结
MVC、MVVM、Coordinator、Redux/UDF、TCA 和 VIPER 没有脱离场景的优劣排序。MVC 适合直接的平台协调,MVVM 管理 View State,Coordinator 管流程与导航,UDF 用 Reducer 约束状态演进,TCA 提供完整的组合与测试体系,VIPER 适合需要强角色边界的复杂模块。
真正跨模式稳定的原则是:状态有唯一 Owner,Effect 有生命周期和结果回流,导航有语义状态和 Scope,依赖从 Composition Root 注入,业务规则不进入 UI 框架。选择最少但足够的约束,并通过竞态测试、团队交付和运行指标验证,而不是让模式名称替代工程判断。
问答复盘
Q1:MVC 是否必然导致 Massive View Controller?
答: 不必然。问题来自 Controller 承担业务、数据和导航等过多职责。小型页面配合 Use Case/Repository 与可选 Coordinator,MVC 可以很清晰。
Q2:使用 @Observable 是否意味着项目采用了 MVVM?
答: 不意味着。它只提供 Observation;是否为 MVVM 取决于 ViewModel 是否拥有 View State、接收事件并隔离展示逻辑与依赖。
Q3:Coordinator 与 MVVM 是互斥方案吗?
答: 不是。MVVM 处理页面状态和展示,Coordinator 处理跨页面流程与导航,两者经常组合。
Q4:UDF 中为什么 Reducer 不应直接发网络请求?
答: Reducer 应是可重复的纯状态转换。网络属于 Effect,需要独立执行、取消并把结果包装成 Action,测试才能确定性验证。
Q5:采用 TCA 后是否不再需要 Repository 和 Use Case?
答: 仍然需要相应边界。TCA 组织 UI State、Action、Reducer、Effect 和 Dependency,不替代数据一致性、业务规则与服务端协议。
Q6:VIPER 最适合什么场景?
答: 适合职责复杂、边界稳定、多人协作且团队接受较高 Wiring/Protocol 成本的 Feature。简单页面全套使用容易产生空转发。
Q7:Navigation State 中为什么只保存稳定 ID?
答: View Controller、ViewModel 和响应对象不可可靠编码或恢复。稳定 ID 可在恢复时重新查询数据,并重新验证账号、权限和实体存在性。
Q8:页面退出时取消 Task,是否应删除已经进入 Outbox 的操作?
答: 通常不应。页面 Task 属于 Presentation 生命周期,Durable Operation 属于业务状态;撤销需要显式 Command 和补偿协议。
Q9:测试替身是否越多越好?
答: 不是。优先替换 I/O、时间、随机、权限和导航等不确定边界。纯 Value 和稳定算法直接使用,避免 Protocol/Mock 过度膨胀。
Q10:如何判断复杂架构模式带来了真实收益?
答: 比较缺陷复现、竞态覆盖、测试速度、变更影响范围、多人冲突、构建时间和新成员学习成本,而不是统计类型或测试数量。
延伸知识
- 状态建模:Domain/View State、Impossible State、Reducer、异步结果过期与乐观回滚;
- 分层与依赖:Repository、Use Case、Dependency Rule、Composition Root 与模块边界;
- SwiftUI 状态流:Observation、Binding、Environment、Identity 与 NavigationStack;
- 并发工程:MainActor、Cancellation、Revision、Actor Reentrancy 与可控竞态测试;
- 模块化:Feature/Core/Interface Module、跨模块导航、Build Time 和 Ownership。