使用 RxSwift 将 UITableViewCell 中的控件绑定到 ViewModel 的最佳实践

Posted

技术标签:

【中文标题】使用 RxSwift 将 UITableViewCell 中的控件绑定到 ViewModel 的最佳实践【英文标题】:Best practice for binding controls in UITableViewCell to ViewModel using RxSwift 【发布时间】:2019-10-28 19:52:04 【问题描述】:

我正在使用 MVC 迁移现有应用程序,该应用程序大量使用委托模式到使用 RxSwift 和 RxCocoa 进行数据绑定的 MVVM。

一般来说,每个 View Controller 都拥有一个专用 View Model 对象的实例。让我们将视图模型称为MainViewModel 进行讨论。当我需要一个驱动 UITableView 的 View Model 时,我通常会创建一个 CellViewModel 作为 struct,然后创建一个可观察的序列,该序列转换为我可以用来驱动 table view 的驱动程序。

现在,假设 UITableViewCell 包含一个我想绑定到 MainViewModel 的按钮,这样我就可以在我的交互层中发生一些事情(例如触发网络请求)。我不确定在这种情况下使用的最佳模式是什么。

这是我开始使用的简化示例(请参阅代码示例下面的 2 个特定问题):

主视图模型:

class MainViewModel 

   private let buttonClickSubject = PublishSubject<String>()   //Used to detect when a cell button was clicked.

   var buttonClicked: AnyObserver<String> 
      return buttonClickSubject.asObserver()
   

   let dataDriver: Driver<[CellViewModel]>

   let disposeBag = DisposeBag()

   init(interactor: Interactor) 
      //Prepare the data that will drive the table view:
      dataDriver = interactor.data
                      .map  data in
                         return data.map  MyCellViewModel(model: $0, parent: self) 
                      
                      .asDriver(onErrorJustReturn: [])

      //Forward button clicks to the interactor:
      buttonClickSubject
         .bind(to: interactor.doSomethingForId)
         .disposed(by: disposeBag)
   

单元视图模型:

struct CellViewModel 
   let id: String
   // Various fields to populate cell

   weak var parent: MainViewModel?

   init(model: Model, parent: MainViewModel) 
      self.id = model.id
      //map the model object to CellViewModel

      self.parent = parent
   

视图控制器:

class MyViewController: UIViewController 
    let viewModel: MainViewModel
    //Many things omitted for brevity

    func bindViewModel() 
        viewModel.dataDriver.drive(tableView.rx.items)  tableView, index, element in
            let cell = tableView.dequeueReusableCell(...) as! TableViewCell
            cell.bindViewModel(viewModel: element)
            return cell
        
        .disposed(by: disposeBag)
    

单元格:

class TableViewCell: UITableViewCell 
    func bindViewModel(viewModel: MyCellViewModel) 
        button.rx.tap
            .map  viewModel.id        //emit the cell's viewModel id when the button is clicked for identification purposes.
            .bind(to: viewModel.parent?.buttonClicked)   //problem binding because of optional.
            .disposed(by: cellDisposeBag)
    

问题:

    有没有更好的方法来使用这些技术实现我想要实现的目标? 我将CellViewModel 中对父级的引用声明为弱,以避免单元虚拟机和主虚拟机之间的保留周期。但是,由于可选值,这会在设置绑定时导致问题(请参阅上面 TableViewCell 实现中的 .bind(to: viewModel.parent?.buttonClicked) 行。

【问题讨论】:

【参考方案1】:

这里的解决方案是将Subject 移出ViewModel 并移入ViewController。如果您发现自己在视图模型中使用了 Subject 或 dispose bag,那么您可能做错了什么。有例外,但它们非常罕见。您当然不应该将其作为一种习惯。

class MyViewController: UIViewController 
    var tableView: UITableView!
    var viewModel: MainViewModel!
    private let disposeBag = DisposeBag()

    func bindViewModel() 
        let buttonClicked = PublishSubject<String>()
        let input = MainViewModel.Input(buttonClicked: buttonClicked)
        let output = viewModel.connect(input)
        output.dataDriver.drive(tableView.rx.items)  tableView, index, element in
            var cell: TableViewCell! // create and assign
            cell.bindViewModel(viewModel: element, buttonClicked: buttonClicked.asObserver())
            return cell
        
        .disposed(by: disposeBag)
    


class TableViewCell: UITableViewCell 
    var button: UIButton!
    private var disposeBag = DisposeBag()
    override func prepareForReuse() 
        super.prepareForReuse()
        disposeBag = DisposeBag()
    

    func bindViewModel<O>(viewModel: CellViewModel, buttonClicked: O) where O: ObserverType, O.Element == String 
        button.rx.tap
            .map  viewModel.id     //emit the cell's viewModel id when the button is clicked for identification purposes.
            .bind(to: buttonClicked) //problem binding because of optional.
            .disposed(by: disposeBag)
    


class MainViewModel 

    struct Input 
        let buttonClicked: Observable<String>
    

    struct Output 
        let dataDriver: Driver<[CellViewModel]>
    

    private let interactor: Interactor

    init(interactor: Interactor) 
        self.interactor = interactor
    

    func connect(_ input: Input) -> Output 
        //Prepare the data that will drive the table view:
        let dataDriver = interactor.data
            .map  data in
                return data.map  CellViewModel(model: $0) 
            
            .asDriver(onErrorJustReturn: [])

        //Forward button clicks to the interactor:
        _ = input.buttonClicked
            .bind(to: interactor.doSomethingForId)
        // don't need to put in dispose bag because the button will emit a `completed` event when done.

        return Output(dataDriver: dataDriver)
    


struct CellViewModel 
    let id: String
    // Various fields to populate cell

    init(model: Model) 
        self.id = model.id
    

【讨论】:

感谢 Daniel,这里有很多很棒的东西可以帮上大忙。一个简单的问题:您能解释一下您对按钮为什么会发出completed 事件的评论吗?我是 RxSwift 的新手,不明白为什么会发生这种情况。 所有 UIControls 在被释放时都会发出一个completed 事件。 重复使用电池时为什么要重新创建处理袋?为什么不直接使用串行一次性用品呢? @DanielT。顺便说一句,您在此处引用:“如果您发现自己在视图模型中使用主题或处置包,您可能做错了什么。”。为什么?以这种方式使用它们有什么缺点吗?还是只是偏好问题? 因为您的视图模型应该代表它所附加的视图的逻辑。如果您需要一个处置袋,那么这意味着您正在处理的是行为,而不是逻辑。视图模型的目的是将逻辑与行为分开,因此在视图模型中有 disposeBag 意味着您违背了该目的。【参考方案2】:

你可以使用这个RxReusable。 这是 UITableViewCell、UICollectionView 的 Rx 扩展...

【讨论】:

以上是关于使用 RxSwift 将 UITableViewCell 中的控件绑定到 ViewModel 的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

使用 RxSwift 将 Alamofire 请求绑定到表视图

使用 RxSwift 将 UITableViewCell 中的控件绑定到 ViewModel 的最佳实践

如何使用 RXSwift 将 Object 的属性绑定到 UIlabel?

使用 rxSwift 中的 tableView 单元将数据从视图模型传递到视图控制器

RxSwift - 将序列拆分为更小的序列以连接

如何将多个驱动程序与 RxSwift 正确组合?