使用 RxSwift 将 UITableViewCell 中的控件绑定到 ViewModel 的最佳实践
Best practice for binding controls in UITableViewCell to ViewModel using RxSwift
我正在使用大量使用委托模式的 MVC 将现有应用程序迁移到使用 RxSwift 和 RxCocoa 进行数据绑定的 MVVM。
一般来说,每个视图控制器都拥有一个专用视图模型对象的实例。我们将视图模型称为 MainViewModel
以供讨论。当我需要一个驱动 UITableView 的视图模型时,我通常创建一个 CellViewModel
作为 struct
,然后创建一个可观察的序列,该序列被转换为一个驱动程序,我可以用它来驱动 table ] 查看。
现在,假设 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: [=10=], 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
中声明对 parent 的引用是弱的,以避免 Cell VM 和 Main VM 之间的保留循环。但是,由于可选值,这会在设置绑定时导致问题(请参阅上面 TableViewCell 实现中的行 .bind(to: viewModel.parent?.buttonClicked)
。
此处的解决方案是将 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: [=10=]) }
}
.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
}
}
你可以使用这个 RxReusable。
这是 UITableViewCell、UICollectionView 的 Rx 扩展…
我正在使用大量使用委托模式的 MVC 将现有应用程序迁移到使用 RxSwift 和 RxCocoa 进行数据绑定的 MVVM。
一般来说,每个视图控制器都拥有一个专用视图模型对象的实例。我们将视图模型称为 MainViewModel
以供讨论。当我需要一个驱动 UITableView 的视图模型时,我通常创建一个 CellViewModel
作为 struct
,然后创建一个可观察的序列,该序列被转换为一个驱动程序,我可以用它来驱动 table ] 查看。
现在,假设 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: [=10=], 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
中声明对 parent 的引用是弱的,以避免 Cell VM 和 Main VM 之间的保留循环。但是,由于可选值,这会在设置绑定时导致问题(请参阅上面 TableViewCell 实现中的行.bind(to: viewModel.parent?.buttonClicked)
。
此处的解决方案是将 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: [=10=]) }
}
.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
}
}
你可以使用这个 RxReusable。 这是 UITableViewCell、UICollectionView 的 Rx 扩展…