在架构设计中,ViewModel 或 Presenter 的职责是**“处理业务逻辑并维护状态” ,而 UI 层的职责是“将状态绘制出来并捕捉用户动作”**。
“反向依赖”是指逻辑层(ViewModel)直接调用了 UI 层的具体实现(如 ViewController、View 实例),或者依赖了 UI 层的特有类型(如 UIColor、UIFont)。
1. 如何避免反向依赖:核心技术手段
A. 依赖倒置:协议(Protocol)是唯一的语言
ViewModel 不应该知道具体的 UI 类,它只应该通过抽象接口进行对话。
- 错误:
viewModel.view = MyViewController() - 正确:
viewModel.delegate = MyViewProtocol
B. 消除 UI 类型污染
如果你的 ViewModel 里出现了 import UIKit 或 import SwiftUI,这就是警报。
- 防御操作:使用 枚举 (Enum) 或 结构体 (Struct) 替代。
- 例子:不要让 ViewModel 返回
UIColor.red,让它返回.error状态,由 View 映射为红色。
C. 路由(Router)与协调器(Coordinator)
将“跳转”逻辑从 ViewModel 中抽离。ViewModel 只需要告诉协调器:“我已经完成了登录,请处理下一步”,而不关心是 push 一个 VC 还是 present 一个视图。
2. SwiftUI 中是更容易还是更难?
这是一个硬币的两面。结论是:设计上更容易,但“守规矩”更难。
为什么在 SwiftUI 中“更容易”?
- 数据驱动(Data-Driven)的本质: SwiftUI 本身就是单向的:
State -> View。在 UIKit 中你可能需要手动调用label.text = name(这是命令式的依赖),但在 SwiftUI 中,你只需更新name属性。这种声明式语法天生就切断了 ViewModel 对 View 的直接操控。 - 值类型(Value Types)的保护: SwiftUI 的 View 是结构体(Struct)。你无法在 ViewModel 中持久地持有 View 的引用,因为视图树是不断销毁和重建的。这在物理层面上阻止了反向持有。
为什么在 SwiftUI 中“更难”?
- 环境对象的诱惑:
@EnvironmentObject极其方便,但也极其危险。开发者很容易在 ViewModel 中滥用全局状态,导致逻辑层与特定视图树的生命周期紧紧绑定。 - Navigation 的耦合: 在 SwiftUI 初版中,导航是通过
NavigationLink绑定isActive状态实现的。这意味着 ViewModel 的状态必须直接控制 UI 的层级跳转。虽然现在有了NavigationStack和path绑定,但逻辑层和路由层的界限依然容易模糊。 - 闭包的循环引用: 在编写 Action 回调时,如果 ViewModel 捕获了 View 的某些闭包,而这些闭包又引用了 ViewModel,极易产生循环引用。
3. 防御式代码:SwiftUI 解耦模版
为了保证 ViewModel 绝对不反向依赖 UI,可以使用 Input/Output 模型:
Swift
// 1. 彻底隔离 UI 库
import Foundation // 禁止 import SwiftUI
class UserViewModel: ObservableObject {
// Output: 状态描述,View 只能读
@Published private(set) var status: Status = .idle
enum Status {
case idle, loading, success(String), error
}
// Input: 意图(Action),View 通过这个函数告诉 VM 发生了什么
func handleAction(_ action: Action) {
switch action {
case .loginTapped:
performLogin()
}
}
enum Action {
case loginTapped
}
}
总结
在 SwiftUI 中,**“数据即视图”**的思想让反向依赖在物理上变难了,但在逻辑上,由于 SwiftUI 强大的集成能力,保持 ViewModel 的“纯净”需要更强的架构自律。