一、问题本质
ViewModel/Presenter 反向依赖 UI = 模块边界破裂
原因:ViewModel/Presenter 不应该知道 UI 的具体视图、控件或渲染逻辑,它只负责 状态、业务逻辑、派生数据。
-
反向依赖会导致:
- 模块耦合:UI 改动 → 逻辑层也必须改
- 测试困难:无法在不加载 UI 的情况下测试业务逻辑
- 可替换性差:逻辑层无法被不同 UI 重用
二、常见反向依赖形式
- 直接引用 UI 元素 / UIKit 控件
class LoginViewModel {
var passwordField: UITextField? // ❌
func validate() {
passwordField?.backgroundColor = isValid ? .green : .red
}
}
- ViewModel 直接修改控件 → 反向依赖 UI
- 不可测试、不可复用
- 触发 UI 动画 / 弹窗
presentAlert(message: "错误") // ❌
- Presenter 调用 UIKit API → 业务逻辑层依赖 UI
- 常见于 VIPER 的 Presenter
三、SwiftUI 的情况
1️⃣ 更容易吗?
更容易控制反向依赖:
-
SwiftUI 声明式 + 单向数据流:
- View 由
@State / @ObservedObject / @StateObject / @EnvironmentObject观察 ViewModel - ViewModel 只管理状态和逻辑,UI 自动根据状态更新
- View 由
-
不需要手动操作控件 → 减少了反向依赖机会
2️⃣ 更难吗?
-
某些场景下更隐蔽:
- 如果 ViewModel 包含
UIViewRepresentable或 UIKit 绑定 - 如果 ViewModel 尝试通过 closure 或 delegate 调整 View(比如动画 / 弹窗)
→ 依然可能反向依赖 UI
- 如果 ViewModel 包含
-
SwiftUI 的“环境”容易滥用:
- 例如直接读取 Environment 值触发 UI 改变
- 如果逻辑层依赖环境来驱动状态 → 依赖链隐性化
四、工程实践:避免反向依赖
1️⃣ 单向数据流
ViewModel/Presenter → State → View
View → Action → ViewModel
- ViewModel 不直接操作 UI
- UI 根据状态渲染
- 典型 TCA / SwiftUI 做法
2️⃣ 事件/Action 传递给 UI 层
- UI 事件由 ViewModel 发出,但由 View 处理实际 UI 逻辑
enum LoginEvent {
case showError(String)
}
class LoginViewModel: ObservableObject {
@Published var state: LoginState
var event: PassthroughSubject<LoginEvent, Never>
}
struct LoginView: View {
@StateObject var vm: LoginViewModel
.onReceive(vm.event) { event in
switch event {
case .showError(let msg):
showAlert(msg) // UI 层处理
}
}
}
- ViewModel 不操作 UI
- UI 负责呈现 → 避免反向依赖
3️⃣ 抽象 UI 响应
-
如果 Presenter 需要通知 UI 做复杂操作:
- 定义 协议 / Action / Event
- 让 UI 处理具体实现
-
Presenter 只发信号,UI 响应
protocol LoginViewListener {
func loginSucceeded()
}
4️⃣ 避免 closure capture UI
- 不要在 ViewModel 里 capture View / ViewController
- 尽量用
Publisher / Binding / Event通知 UI
五、总结:SwiftUI 对比 UIKit
| 特性 | UIKit | SwiftUI |
|---|---|---|
| 控件操作 | 易反向依赖 | 不直接操作控件,更自然单向 |
| 状态管理 | 手动同步状态 | @State / @Published 自动同步 |
| 事件传递 | delegate / closure | Publisher / Binding / Action |
| 容易踩坑 | 高 | 中/低,但环境滥用仍可能 |
核心原则:
- ViewModel/Presenter 只管理状态和业务逻辑
- UI 层根据状态渲染或响应事件
- 任何 UI 操作都不出现在逻辑层 → 保持单向依赖