Вот проект с обновлениями ниже в GitHub: https://github.com/dtartaglia/RxCollectionViewTester
Первое, что мы делаем, - обрисовываем в общих чертах все наши входы и выходы. Выходные данные должны быть членами структуры модели представления, а входные данные должны быть членами входной структуры.
В этом случае у нас есть три входа из ячейки:
struct CellInput {
let plus: Observable<Void>
let minus: Observable<Void>
let delete: Observable<Void>
}
Один выход для самой ячейки (метка) и два выхода для родителя ячейки (предположительно, модель представления контроллера представления.)
struct CellViewModel {
let label: Observable<String>
let value: Observable<Int>
let delete: Observable<Void>
}
Также нам нужно настроить ячейку так, чтобы она принимала заводскую функцию, чтобы она могла создавать экземпляр модели представления. Ячейка также должна иметь возможность сброса себя:
class Cell : UICollectionViewCell {
var bag = DisposeBag()
var label: UILabel!
var plus: UIButton!
var minus: UIButton!
var delete: UIButton!
// code to configure UIProperties omitted.
override func prepareForReuse() {
super.prepareForReuse()
bag = DisposeBag() // this resets the cell's bindings
}
func configure(with factory: @escaping (CellInput) -> CellViewModel) {
// create the input object
let input = CellInput(
plus: plus.rx.tap.asObservable(),
minus: minus.rx.tap.asObservable(),
delete: delete.rx.tap.asObservable()
)
// create the view model from the factory
let viewModel = factory(input)
// bind the view model's label property to the label
viewModel.label
.bind(to: label.rx.text)
.disposed(by: bag)
}
}
Теперь нам нужно построить метод init модели представления. Здесь происходит вся настоящая работа.
extension CellViewModel {
init(_ input: CellInput, initialValue: Int) {
let add = input.plus.map { 1 } // plus adds one to the value
let subtract = input.minus.map { -1 } // minus subtracts one
value = Observable.merge(add, subtract)
.scan(initialValue, accumulator: +) // the logic is here
label = value
.startWith(initialValue)
.map { "number is \($0)" } // create the string from the value
delete = input.delete // delete is just a passthrough in this case
}
}
Вы заметите, что метод init модели представления нуждается в большем, чем предусмотрено заводской функцией. Дополнительная информация будет предоставлена контроллером представления при создании фабрики.
Контроллер вида будет иметь это в viewDidLoad
:
viewModel.counters
.bind(to: collectionView.rx.items(cellIdentifier: "Cell", cellType: Cell.self)) { index, element, cell in
cell.configure(with: { input in
let vm = CellViewModel(input, initialValue: element.value)
// Remember the value property tracks the current value of the counter
vm.value
.map { (id: element.id, value: $0) } // tell the main view model which counter's value this is
.bind(to: values)
.disposed(by: cell.bag)
vm.delete
.map { element.id } // tell the main view model which counter should be deleted
.bind(to: deletes)
.disposed(by: cell.bag)
return vm // hand the cell view model to the cell
})
}
.disposed(by: bag)
Для приведенного выше примера я предполагаю, что:
counters
относится к типу Observable<[(id: UUID, value: Int)]>
и относится к модели представления контроллера вида.
values
имеет тип PublishSubject<(id: UUID, value: Int)>
и вводится в модель представления контроллера вида.
deletes
имеет тип PublishSubject<UUID>
и вводится в модель представления контроллера вида.
Конструкция модели представления контроллера представления следует той же схеме, что и для ячейки:
Входы:
struct Input {
let value: Observable<(id: UUID, value: Int)>
let add: Observable<Void>
let delete: Observable<UUID>
}
Выходы:
struct ViewModel {
let counters: Observable<[(id: UUID, value: Int)]>
}
Логика:
extension ViewModel {
private enum Action {
case add
case value(id: UUID, value: Int)
case delete(id: UUID)
}
init(_ input: Input, initialValues: [(id: UUID, value: Int)]) {
let addAction = input.add.map { Action.add }
let valueAction = input.value.map(Action.value)
let deleteAction = input.delete.map(Action.delete)
counters = Observable.merge(addAction, valueAction, deleteAction)
.scan(into: initialValues) { model, new in
switch new {
case .add:
model.append((id: UUID(), value: 0))
case .value(let id, let value):
if let index = model.index(where: { $0.id == id }) {
model[index].value = value
}
case .delete(let id):
if let index = model.index(where: { $0.id == id }) {
model.remove(at: index)
}
}
}
}
}