Лучшие практики для привязки элементов управления в UITableViewCell к ViewModel с использованием RxSwift - PullRequest
2 голосов
/ 28 октября 2019

Я нахожусь в процессе миграции существующего приложения с использованием MVC, который интенсивно использует шаблон делегирования для MVVM, используя RxSwift и RxCocoa для привязки данных.

В целом каждый контроллер представления владеет экземпляромвыделенный объект View Model. Давайте назовем 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
   }
}

Контроллер вида:

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. Я объявил ссылку на родительский элемент в CellViewModel как слабую, чтобы избежать цикла сохранения между Cell VM и Main VM. Однако это вызывает проблему при настройке привязки из-за необязательного значения (см. Строку .bind(to: viewModel.parent?.buttonClicked) в реализации TableViewCell выше.

1 Ответ

2 голосов
/ 29 октября 2019

Решение здесь состоит в том, чтобы переместить Предмет из 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
    }
}
...