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

问题描述 投票:2回答:1

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

通常,每个View Controller拥有一个专用View Model对象的实例。出于讨论目的,我们将视图模型称为MainViewModel。当我需要一个用于驱动UITableView的视图模型时,通常会创建一个CellViewModel作为struct,然后创建一个可观察的序列,该序列会转换为可用于驱动表格视图的驱动程序。

现在,假设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
   }
}

View Controller:

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)
    }
}

问题:

  1. 是否有更好的方法来使用这些技术来实现我想要的目标?
  2. 为了避免单元VM和主VM之间的保留周期,我宣布CellViewModel中对父级的引用很弱。但是,由于可选值,这会在设置绑定时引起问题(请参见上面的TableViewCell实现中的第.bind(to: viewModel.parent?.buttonClicked)行。
ios mvvm rx-swift
1个回答
2
投票

这里的解决方案是将Subject从ViewModel移到ViewController。如果发现自己使用主题或在视图模型中放置包,则可能是您做错了。有例外,但很少见。您当然不应该以此为习惯。

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
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.