局部 Cell 刷新


本文是对在实践中应用 RxSwift 3 处理 Cell的补充,同时修正一些之前犯得错误(Cell ),Demo 地址:Thought

ReactiveCocoa 提供了 rac_prepareForReuseSignal 属性,一般用来解决 Cell 重用问题(重复订阅导致点击效果多次相应)。但之前我一直是持有不赞同的观点,这一直让我感觉这样下面这段代码会增加理解难度。

[[[cell.okButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
takeUntil:cell.rac_prepareForReuseSignal]
subscribeNext:^(UIButton *x) {
MXThing *thing = self.things[indexPath.row];
// use this thing
}];

[[[cell.cancelButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
takeUntil:cell.rac_prepareForReuseSignal]
subscribeNext:^(UIButton *x) {
// do other things
}];

选择支付方式

然而前几天遇到这下面图片的需求。

选择支付方式
选择支付方式

选择支付方式,需要注意的是支付方式选项是动态的,比如可能没有支付宝支付。

类似完整的页面如下。

确认订单选择支付方式
确认订单选择支付方式

我去参考了一下上图的代码,看了十分钟后,,开始写代码。

每一个 Cell 需要两个信息。

  • 支付方式信息 payment
  • 是否选择该支付方式 isSelected

原本我的设计是一个 set 方法,设置支付方式信息,另一个是是否选择该支付方式。Cell 中的代码如下:

func setInfo(payment: Payment, isSelected: Bool) {
// ...
}

看起来没什么问题,点击 Cell 的时候更新数据,在当前场景下我更新 Cell 的方式只有调用以下几种方法:

public func reloadData()
public func reloadSections(sections: NSIndexSet, withRowAnimation animation: UITableViewRowAnimation)
public func reloadRowsAtIndexPaths(indexPaths: [NSIndexPath], withRowAnimation animation: UITableViewRowAnimation)

看起来没什么问题,点击一次,刷新一下 UITableView ,然而这里存在一个很差的用户体验,点击效果很差。点击动画还未结束,Cell 就被重新加载了。不可以,这效果很糟糕,用了 Rx 会导致效果变差,如果真的是这样,这就非常尴尬了。

再讨论解决方案前,先考虑一个问题,有没有必要调用 reload 方法,该场景下没有必要。完成上述需求只需要做两件事情:

  • 更新 Cell 中“√”颜色
  • 更改数据源结果(确保 Cell 消失后再出现仍然是正确的“√”状态)

Cell 中的绑定

之前自己一直固执的认为 Cell 是不需要绑定的,因为它们经常被重用,经常出现又消失。而本例提到的场景并没有很强的重用、出现消失体现,更多体现在交互逻辑变化。

本例中存在两个绑定场景:

  • 完整数据 -> UITableView
  • 选择状态 -> [UITableViewCell]

第一个是显然的,来看第二个,Cell 需要两个信息 payment 和 isSelected ,payment 设置一下就好了,需要变的时候只需要 reload 。 isSelected 则不同,我们希望不 reload 就做到视图和数据的更新。

那么为什么不能在一个数据模型中加个 Observable 呢,由这个 Observable 通知 Cell 的更新,这里我们选择 Variable 完成这个需求。

那么如何实现这部分代码,开始之前回顾一下真实的例子:

确认订单选择支付方式
确认订单选择支付方式

选择支付方式是一个 Section ,将选择的信息放在 Section 是比较好的选择。

struct SelectPayment {
let select: Variable<Payment>
}

item 对应的 Model 如下。

enum Payment {
/// 支付宝
case alipay
/// 微信
case wechat
/// 银联
case unionpay
}

完整的 SectionModel 如下。

typealias PaymentSectionModel = AnimatableSectionModel<SelectPayment, Payment>

关键代码如下。

selectedPayment
.map { $0 == payment }
.bindTo(cell.rx_isSelectedPayment)
.addDisposableTo(cell.reusableDisposeBag)

唯一要注意的是,不要添加到 self.disposeBag ,即 addDisposeableTo(self.disposeBag),这样的代码做不到更新绑定规则。 reusableDisposeBag 的原理很简单。

override func prepareForReuse() {
super.prepareForReuse()
reusableDisposeBag = DisposeBag()
}

重新设置 disposeBag ,取消之前所有的订阅。

购物车商品加减

处理商品加减在 Model 中更需要传入一个 Observable 了,而且这里还存在非常清晰的双向绑定,Item <-> Cell

商品加减
商品加减

即每一个商品的购买数量和 Cell 存在绑定关系。完整的逻辑图如下。

商品与视图的关系
商品与视图的关系

重点关注这个绑定关系,Cell 中的加减行为只会影响到购买数量

购买数量与 Cell
购买数量与 Cell

那么修改该变量则没必有必要影响其他数据的变化,即

至于代码的实现,仍然是在一个 ItemModel 中传入一个 Observable 。

struct ProductInfo {
let id: Int64
let name: String
let unitPrice: Int
let count: Variable<Int>
}

Cell 的绑定关系只需一行代码。

(cell.rx_count <-> product.count).addDisposableTo(cell.reusableDisposeBag)

<-> 操作符的实现见:Operators.swift

彩蛋

我在本文的示例代码中添加了一些你可能没有注意到的 Swift 细节。

.subscribeNext(tableView.deselectRowAtIndexPath)
changeCount(-=)

总结

在需要绑定场景使用 RxSwift ,这是考虑要不要使用 RxSwift 的最佳考虑方案,需要优雅的处理异步不一定选择 RxSwift ,还可以考虑 PromiseKit 。

Rx 实际上是一个思想,一种面向(异步)数据流编程的思想,注重流的变化。

相关阅读

双向绑定

Issue

Repo