ios - 使用 RxSwift 将 UITableViewCell 中的控件绑定(bind)到 ViewModel 的最佳实践

标签 ios mvvm rx-swift

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

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

现在,假设 UITableViewCell 包含一个我想绑定(bind)到 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)
   }
}

单元格 View 型号:
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
   }
}

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

问题:
  • 有没有更好的方法来做我想要使用这些技术实现的目标?
  • 我在 CellViewModel 中声明了对父级的引用尽可能弱,以避免 Cell VM 和 Main VM 之间的保留周期。但是,这会在设置绑定(bind)时由于可选值而导致问题(参见上面 TableViewCell 实现中的 .bind(to: viewModel.parent?.buttonClicked) 行。
  • 最佳答案

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

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

    关于ios - 使用 RxSwift 将 UITableViewCell 中的控件绑定(bind)到 ViewModel 的最佳实践,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58597531/

    相关文章:

    ios - 确保 WKWebView View 已呈现内容?

    iphone - 如何使 View 看起来像报摊书架

    c# - 如何在不丢失 WPF 中的绑定(bind)的情况下更改 TextBox.Text?

    c# - 为任务/意图提供单声道跨平台支持

    swift - 如何过滤 combineLatest 仅在一项更改时触发?

    iphone - 无法找到带有异常断点的 exc_bad_access 行并设置 NSZombies 类

    ios - 在集成 Quickblox SDK 时限制发送状态的数量

    c# - 如何在文本框上显示气球提示?

    ios - 如何使用 RxSwift 的 Groupby 运算符将 GroupedObservable<String, Message> 转换为 SectionModel<String, Message>?

    ios - 找到匹配项后,如何停止RxSwift ble扫描仪?