使用Combine如何在订阅回调中获取绑定的ViewController视图实例
实现方案说明
完全不需要把视图实例塞进ViewModel的*@Published*属性里混存,视图和模型的绑定属于ViewController层的职责,ViewModel只需要负责发布纯业务模型即可,通过Combine内置操作符就能实现目标效果,完全可以规避用元组存视图的不合理设计。
方案1:在订阅链路注入本地视图实例
先把ViewModel的发布属性调整为只存纯业务模型,不要混入视图引用:
// ViewModel 内部定义,纯业务层属性,无任何视图依赖 @Published private(set) var labelModel: Label.Model?
在ViewController里订阅时,通过Combine的map操作符,把本地持有的视图实例和上游发布的模型值在订阅链路内绑定,不需要修改ViewModel的任何逻辑:
// 取到本地持有的labelView实例后直接订阅 viewModel.$labelModel // 链路内绑定视图实例和模型值,不侵入ViewModel .map { [weak self] model in return (self?.labelView, model) } .sink { (labelView, model) in guard let labelView = labelView, let model = model else { return } // 回调内直接拆分取视图、模型值调用更新逻辑 labelView.update(with: model) } .store(in: &subscriptions)
这种方式完全保持分层边界,ViewModel不持有任何视图引用,不会出现内存泄漏、无法单元测试的问题,也不需要改动原有ViewModel的结构。
方案2:封装自定义Combine操作符,复刻目标订阅体验
如果想要更接近你最初设想的自定义订阅写法,可以给Combine的Publisher写一个通用扩展,实现通用的视图绑定逻辑,省去每次手动写map绑定的重复代码:
extension Publisher { /// 通用绑定方法:将上游发布值传递给指定的目标对象,自动弱引用避免循环 func bind<Target: AnyObject>( to target: Target, using closure: @escaping (Target, Output) -> Void ) -> AnyCancellable { return self .receive(on: DispatchQueue.main) // 自动切主线程更新UI,减少重复配置 .sink { [weak target] output in guard let target = target else { return } closure(target, output) } } }
调用时的写法和你最初设计的自定义订阅几乎完全一致,不需要手动处理元组:
viewModel.$labelModel .bind(to: labelView) { labelView, model in guard let model = model else { return } labelView.update(with: model) } .store(in: &subscriptions)
这个实现里对绑定的目标视图是弱引用,不会因为闭包持有视图导致无法释放,同时自动切主线程处理UI更新,避免后台线程刷新视图的异常。
为什么不建议用元组在ViewModel里存视图+模型
- 打破分层边界:ViewModel属于业务层,持有具体的UIView实例会让业务代码和UI层强耦合,后续做单元测试、跨端复用时根本无法独立运行
- 存在内存风险:@Published属性如果强引用视图,当ViewModel的生命周期长于ViewController时,会导致视图无法释放,出现内存泄漏
- 扩展性不足:如果一个模型需要同时绑定多个视图,元组的方式会无限膨胀,根本无法维护
内容的提问来源于stack exchange,提问作者stevenpcurtis
相关产品推荐
相关产品推荐

