架构
先划边界,再选 MVVM
MVC 或 MVVM 只是界面那一层怎么分工。工程变乱,通常是界面、状态、副作用缠在一起。
争论 MVC 还是 MVVM,经常停在“逻辑要不要从 ViewController 挪走”。挪走之后如果 ViewModel 里既请求网络、又 push 下一个页面、又直接改 UIKit,那只是换了个更难看的控制器。
三块
- 界面:把状态画出来,把用户操作变成事件。UIViewController、SwiftUI View 都算这里。
- 状态:这份界面现在是加载中、内容、空、失败。转移规则放在一个类型里,输入是事件,输出是新状态。
- 副作用:网络、磁盘、定位、埋点。状态层描述“要发请求”,真正发请求的类型在边缘,可以替换。
依赖方向只允许向内。界面向状态要数据,状态不 import UIKit。副作用实现细节不出现在界面文件里。这样换界面技术(多一个 SwiftUI 页面)时,状态和请求不用重写。
ViewModel 什么时候是有用的
它有用,是当一个界面的状态转移不适合写在 view 生命周期里:多次点击的取消、输入校验、分页拼接。它没用,是当它只是把每个 IBAction 原样包一层函数。
enum FeedEvent {
case appear
case loaded([Item])
case failed
case retry
}
struct FeedState {
var items: [Item] = []
var phase: Phase = .idle
enum Phase { case idle, loading, failed, ready }
mutating func send(_ event: FeedEvent) {
switch (phase, event) {
case (.idle, .appear), (.failed, .retry):
phase = .loading
case (.loading, .loaded(let items)):
self.items = items
phase = .ready
case (.loading, .failed):
phase = .failed
default:
break
}
}
}
这段没有 URLSession,也没有 navigationController。请求是否发出,由外面看到 phase 变成 loading 来决定。非法转移(已经在 loading 时再 appear)被默认分支丢掉。这种代码可以用单测覆盖,不需要把控制器跑起来。
命名随你。叫 Store、Model、Reducer 都可以。守住的是边界,不是文件夹名字。