如何使用 MVVM 和 RxSwift 编辑/删除 UICollectionView 单元格
Posted
技术标签:
【中文标题】如何使用 MVVM 和 RxSwift 编辑/删除 UICollectionView 单元格【英文标题】:How edit/delete UICollectionView cells using MVVM and RxSwift 【发布时间】:2019-01-04 22:18:34 【问题描述】:我试图了解如何使用对象列表和 UICollectionView 实现 MVVM。我不明白如何实现用户迭代 -> 模型流。
我设置了一个test application,模型只是一个带有 Int 的类,而视图是一个 UICollectionViewCell,它显示了一个具有相应 Int 值的文本,并有加号、减号和删除按钮来增加、减少和删除分别是一个元素。 每个条目看起来像: 我想知道使用 MVVM 和 RxSwift 更新/删除单元格的最佳方式。
我有一个随机生成的 Int 值列表
let items: [Model]
只有 Int 值的模型
class Model
var number: Int
init(_ n: Int = 0)
self.number = n
ViewModel 类,它只保存模型并具有 Observable
class ViewModel
var value: Observable<Model>
init(_ model: Model)
self.value = Observable.just(model)
还有细胞
class Cell : UICollectionViewCell
class var identifier: String return "\(self)"
var bag = DisposeBag()
let label: UILabel
let plus: UIButton
let minus: UIButton
let delete: UIButton
....
var viewModel: ViewModel? = nil
didSet
....
viewModel.value
.map( "number is \($0.number)" )
.asDriver(onErrorJustReturn: "")
.drive(self.label.rx.text)
.disposed(by: self.bag)
....
我不明白怎么做的是如何将按钮连接到相应的动作,然后更新模型和视图。
Cell 的 ViewModel 是否对此负责?它应该是接收点击事件,更新模型然后是视图的那个吗?
在删除情况下,单元格的删除按钮需要从数据列表中删除当前模型。如果不将所有内容混合在一起,如何做到这一点?
【问题讨论】:
【参考方案1】:这里是 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)
【讨论】:
感谢您的解释和提出更改建议的请求 :) !【参考方案2】:我是这样做的:
ViewModel.swift
import Foundation
import RxSwift
import RxCocoa
typealias Model = (String, Int)
class ViewModel
let disposeBag = DisposeBag()
let items = BehaviorRelay<[Model]>(value: [])
let add = PublishSubject<Model>()
let remove = PublishSubject<Model>()
let addRandom = PublishSubject<()>()
init()
addRandom
.map _ in (UUID().uuidString, Int.random(in: 0 ..< 10))
.bind(to: add)
.disposed(by: disposeBag)
add.map newItem in self.items.value + [newItem]
.bind(to: items)
.disposed(by: disposeBag)
remove.map removedItem in
self.items.value.filter (name, _) -> Bool in
name != removedItem.0
.bind(to: items)
.disposed(by: disposeBag)
Cell.swift
import Foundation
import Material
import RxSwift
import SnapKit
class Cell: Material.TableViewCell
var disposeBag: DisposeBag?
let nameLabel = UILabel(frame: .zero)
let valueLabel = UILabel(frame: .zero)
let removeButton = FlatButton(title: "REMOVE")
var model: Model? = nil
didSet
guard let (name, value) = model else
nameLabel.text = ""
valueLabel.text = ""
return
nameLabel.text = name
valueLabel.text = "\(value)"
override func prepare()
super.prepare()
let textWrapper = UIStackView()
textWrapper.axis = .vertical
textWrapper.distribution = .fill
textWrapper.alignment = .fill
textWrapper.spacing = 8
nameLabel.font = UIFont.boldSystemFont(ofSize: 24)
textWrapper.addArrangedSubview(nameLabel)
textWrapper.addArrangedSubview(valueLabel)
let wrapper = UIStackView()
wrapper.axis = .horizontal
wrapper.distribution = .fill
wrapper.alignment = .fill
wrapper.spacing = 8
addSubview(wrapper)
wrapper.snp.makeConstraints make in
make.edges.equalToSuperview().inset(8)
wrapper.addArrangedSubview(textWrapper)
wrapper.addArrangedSubview(removeButton)
ViewController.swift
import UIKit
import Material
import RxSwift
import SnapKit
class ViewController: Material.ViewController
let disposeBag = DisposeBag()
let vm = ViewModel()
let tableView = UITableView()
let addButton = FABButton(image: Icon.cm.add, tintColor: .white)
override func prepare()
super.prepare()
view.addSubview(tableView)
tableView.snp.makeConstraints make in
make.edges.equalToSuperview()
addButton.pulseColor = .white
addButton.backgroundColor = Color.red.base
view.layout(addButton)
.width(48)
.height(48)
.bottomRight(bottom: 16, right: 16)
addButton.rx.tap
.bind(to: vm.addRandom)
.disposed(by: disposeBag)
tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
vm.items
.bind(to: tableView.rx.items) (tableView, row, model) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
cell.model = model
cell.disposeBag = DisposeBag()
cell.removeButton.rx.tap
.map _ in model
.bind(to: self.vm.remove)
.disposed(by: cell.disposeBag!)
return cell
.disposed(by: disposeBag)
请注意,一个常见的错误是只在 Cell 中创建 DisposeBag 一次,这会在触发操作时造成混乱。
DisposeBag 必须在每次重用 Cell 时重新创建。
可以在here 找到完整的工作示例。
【讨论】:
以上是关于如何使用 MVVM 和 RxSwift 编辑/删除 UICollectionView 单元格的主要内容,如果未能解决你的问题,请参考以下文章
我应该如何访问有关用于使用RxSwift和MVVM填充表格视图的数组的数据