6-28.【架构设计】如何避免ViewModel/Presenter反向依赖UI层?在SwiftUI中这个问题更难还是更容易?

3 阅读2分钟

一、问题本质

ViewModel/Presenter 反向依赖 UI = 模块边界破裂
原因:ViewModel/Presenter 不应该知道 UI 的具体视图、控件或渲染逻辑,它只负责 状态、业务逻辑、派生数据

  • 反向依赖会导致:

    • 模块耦合:UI 改动 → 逻辑层也必须改
    • 测试困难:无法在不加载 UI 的情况下测试业务逻辑
    • 可替换性差:逻辑层无法被不同 UI 重用

二、常见反向依赖形式

  1. 直接引用 UI 元素 / UIKit 控件
class LoginViewModel {
    var passwordField: UITextField? // ❌
    func validate() {
        passwordField?.backgroundColor = isValid ? .green : .red
    }
}
  • ViewModel 直接修改控件 → 反向依赖 UI
  • 不可测试、不可复用
  1. 触发 UI 动画 / 弹窗
presentAlert(message: "错误") // ❌
  • Presenter 调用 UIKit API → 业务逻辑层依赖 UI
  • 常见于 VIPER 的 Presenter

三、SwiftUI 的情况

1️⃣ 更容易吗?

更容易控制反向依赖

  • SwiftUI 声明式 + 单向数据流

    • View 由 @State / @ObservedObject / @StateObject / @EnvironmentObject 观察 ViewModel
    • ViewModel 只管理状态和逻辑,UI 自动根据状态更新
  • 不需要手动操作控件 → 减少了反向依赖机会

2️⃣ 更难吗?

  • 某些场景下更隐蔽

    • 如果 ViewModel 包含 UIViewRepresentable 或 UIKit 绑定
    • 如果 ViewModel 尝试通过 closure 或 delegate 调整 View(比如动画 / 弹窗)
      → 依然可能反向依赖 UI
  • 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

特性UIKitSwiftUI
控件操作易反向依赖不直接操作控件,更自然单向
状态管理手动同步状态@State / @Published 自动同步
事件传递delegate / closurePublisher / Binding / Action
容易踩坑中/低,但环境滥用仍可能

核心原则

  • ViewModel/Presenter 只管理状态和业务逻辑
  • UI 层根据状态渲染或响应事件
  • 任何 UI 操作都不出现在逻辑层 → 保持单向依赖