上一篇介绍了 Massive View Controller 产生的原因。由于视图控制器同时承担 View 和 Controller 的职责,无处安放的代码最后全都堆在那里。
最广泛使用的解决方案是MVVM(Model-View-ViewModel)。但打开声称采用 MVVM 的代码,常会发现这种情况:有一个只有名字叫 ViewModel 的类,而视图控制器仍然逐个取出值并应用到界面上。
今天我们来梳理 ViewModel 的真正职责,以及为什么没有绑定的 MVVM 只完成了一半。
ViewModel 是生成界面状态的工厂
MVVM 的三个部分如下划分。
- Model:数据和业务逻辑(与 MVC 相同)
- View:显示界面。在 iOS 中,UIView 和 UIViewController 都属于这一部分
- ViewModel:将 Model 的数据处理成适合显示在界面上的形式,并持有界面状态的对象
有两点需要注意。
第一,在 MVVM 中,UIViewController 属于 View 一侧。上一篇中视图控制器定位模糊的问题,在这里被明确归入 View。视图控制器只负责绘制界面,所有判断都交给 ViewModel。
第二,ViewModel 不应该了解 UIKit。也就是说,import UIKit不应存在 UIKit 依赖Date。接收 Date 并将其转换为“3 分钟前”这样的字符串,以及用 Bool 保存是否正在加载,都是 ViewModel 的职责。至于把这个字符串放进哪个 UILabel,则是 View 的职责。
这种分离让 ViewModel 可以脱离界面进行测试。无需 UIViewController,就能通过纯逻辑验证“没有帖子时会显示提示文案”。
没有绑定,为什么只完成了一半?
如果到这里就停下,代码会变成这样。
// 没有绑定的“只有 MVVM 外表” MVVM"
final class ProfileViewController: UIViewController {
let viewModel = ProfileViewModel()
func refresh() {
viewModel.load()
nameLabel.text = viewModel.displayName // 直接取出值
statusLabel.text = viewModel.statusText // 逐个应用
emptyView.isHidden = !viewModel.isEmpty
}
}
每当 ViewModel 的状态发生变化,视图控制器都必须记得调用refresh()。只要漏掉一次调用,界面和状态就会不一致。你只是移动了状态,“同步状态与界面的责任”仍然留在视图控制器中。
这就是 MVVM 从诞生于 Microsoft WPF 起,就以数据绑定为前提进行设计的原因。只有自动保证“ViewModel 变化时 View 会跟着变化”,才算真正完整。
在 UIKit 中使用 Combine 进行绑定
UIKit 没有内置绑定,因此配合 Combine 是接近标准的做法。
final class ProfileViewModel {
@Published private(set) var displayName = ""
@Published private(set) var isLoading = false
func load() { ... } // 完成时 @Published 更新值
}
final class ProfileViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$displayName
.assign(to: \.text!, on: nameLabel)
.store(in: &cancellables)
viewModel.$isLoading
.sink { [weak self] in self?.spinner.isAnimating = $0 }
.store(in: &cancellables)
}
}
设置一次订阅后,无论 ViewModel 何时、如何变化,界面都会跟着更新。“什么时候该调用 refresh?”这个问题本身就消失了。
在 SwiftUI 中,这种绑定已经融入语言层面。View 只要读取添加了@Observable的对象属性,值发生变化时,该 View 就会自动重新绘制。甚至不需要订阅代码。
Massive ViewModel 的陷阱
采用 MVVM 几个月后,你会遇到新的问题。这次是ViewModel 变得臃肿。如果网络请求、缓存和业务规则全都塞进 ViewModel,那只是把怪物换了个位置。
ViewModel 终究只负责表示层逻辑(处理界面状态)。数据获取应交给 Repository 或 Service,业务规则则应下沉到 Model 层。MVVM 是用于分离 View 与其余部分的模式,并不是要把其余所有内容都放进 ViewModel。
总结
- ViewModel 将 Model 数据处理成界面状态,并且不应该了解 UIKit。这样才能脱离界面进行测试。
- 只移动状态而没有绑定时,同步责任仍然留在视图控制器中。MVVM 必须包含绑定才算完整。
- 在 UIKit 中由 Combine 负责绑定,在 SwiftUI 中则由 @Observable 负责。
- 如果把所有逻辑都放进 ViewModel,就会变成 Massive ViewModel。让它只负责表示层逻辑。
不过在 SwiftUI 中,View 本身会以声明式方式反映状态,因此“根本不需要 ViewModel”的观点也越来越有影响力。这个争论将在下一篇中正面讨论。

![[iOS 架构 #2] MVVM 与绑定要点总结 封面图](/assets/images/posts/f493544a-c1fe-4846-88cc-edd9a3879c4c/1.jpg)