使用背景:
在开发中,我们经常会碰到这种需求样式,tableView上的cell上有按钮,像是删除,提交,查看等按钮,点击按钮进行操作,像这种
tableView上的代理只能给到cell的点击事件,所以按钮点击我们要自己处理,首先要拿到是第几个cell上的按钮点击,然后拿到数据模型,根据数据模型来进行操作,如:图上的删除按钮点击后拿到cell的indexpath,然后拿到具体数据,根据具体数据id进行删除操作,然后更行表格。
解决方式:
这种样式需求比较常见,解决起来也比较简单,本人比较常用的有几个方法
1.delegate. 在cell上有个我们自定义delegate,delegate上有个方法处理删除按钮点击的操作,接受一个tableViewCell的参数。用target action监听删除按钮的点击事件,在方法里执行delegate里面的方法。tableView的cellForRow的代理方法里面给cell赋值属性的时候,把delegate设置成self。注意:delegate用weak修饰,这里会引发内存泄漏。
控制器实现delegate,拿到参数TableViewCell后,用自己的tableView方法执行indexPathForCell的方法拿到indexPath,然后用自己的数据数组找出indexPath的下标拿到具体数据并且进行请求等操作,完成后reloadTableView
优点:简单,容易理解。
缺点:操作步骤多,容易疏漏出错,而且每种这种需求都伴随着protocol,很难受
2.closure. 具体操作和delegate方式差不多,只是把delegate换成了闭包,不需要额外定义protocol而是在cell上定义一种方法类型,然后tableView给cell赋值属性的时候,把方法体赋值给cell,cell里按钮点击执行这个方法。
同样需要注意内存泄漏的问题,在闭包里用weak若引用。因为ViewController拥有tableView,tableView拥有cell,只有ViewController销毁cell才能销毁,但是cell里有delegate或者闭包指向ViewController,引用循环,内存泄漏.
优点:减少了额外定义的protocol,使用闭包代替,减少代码量
缺点:操作步骤依然很多,缺点和delegate差不多,只是换了一种形式,而且可能存在闭包嵌套的可能,导致代码难维护和理解
用RX优雅解决这种问题:
用多了这种delegate或者closure,就会感觉到自己的代码杂乱无章,而且最近一直在学习使用Rx,Rx试图将通信模式统一起来,也就是说一般用了Rx,就最好不要用delegate,targetAction什么的了,全部用绑定的形式来做,代码量少,稳定性高,易维护,易理解。
而这种情况下,cell的按钮点击怎么用Rx来替代呢,基于这几种困扰,想了一个不太好的方法,只是自己的一个思考方式,记录下来了。觉得有问题可以随时欢迎指出来。。
- 把按钮的点击事件用rxCocoa中的controlEvent来代替target action. 怎么把这个ControlEvent给到ViewController呢,我们可以用创建Binder监听者。但是这个事件流是在cell里面的,怎么才能统一绑定起来呢,或者说监听特定cell上的按钮点击。我是用protocol方法解决的。
protocol cellButtonTriggerble:NSObjectProtocol{
var triggerButton:UIButton{get}
var canTrigger:Bool{get set}
}
tableViewCell实现这个协议。triggerButton就是比如删除按钮什么的,能够触发操作的按钮,canTrigger是一个标示,作用下面介绍,cell里赋值为false
- 创建binder:
extension Reactive where Base: UITableViewCell & cellButtonTriggerble{
var trigger: Observable<IndexPath> {
base.canTrigger = true
return base.triggerButton.rx.tap.map{[weak cell = self.base]_ in
if let tb = cell?.superview as? UITableView{
return tb.indexPath(for: cell!)!
}else{
fatalError()
}
}
}
}
这个binder首先类型是Observable<IndexPath>,一个类型为IndexPath的observable流。
第一步绑定的时候把标示设置为true,表示这个cell已经绑定过了。然后把triggerButton的tap的controlEvent 通过map操作符先用weak弱饮用,然后拿到父类tableView,进行方法IndexPathForRow拿到indexPath发出去.
*绑定:
整理一下思路,把cell指定类型上的cell的trigger按钮的点击事件转换成indexPath信号发出来。然后我们可以拿到这个事件流,现在有一个问题,怎么拿到?而且这是一个cell对象的事件流,tableView上有好几个cell对象,而且还有复用机制,cell复用一个对象等。
首先,在willDisplayCell上可以不侵入cell的赋值属性代码上拿到这个事件流。那么复用的cell怎么办,如果重复监听会导致按钮点击一下,好几个indexPath信号发出来。所以上面的唯一标识符就有作用了。
定义一个数据流来接受所有cell点击事件流。
let deleteTaps:BehaviorRelay<Observable<IndexPath>> = BehaviorRelay(value: Observable.empty())
是个事件流接受所有cell的点击事件流
在willDisplayCell上把cell的事件流通过Merge操作符号添加进去.
tableView.rx.willDisplayCell.subscribe(onNext: { [weak self](info) in
if let cell = info.cell as? FinishedOrdersTableViewCell, let self = self, !cell.canTrigger{
self.deleteTaps.accept(Observable.merge(self.deleteTaps.value,cell.rx.trigger))
}
}, onError: nil, onCompleted: nil, onDisposed: nil).disposed(by: disposeBag)
cell显示的时候,如果cell对象还没有进行绑定,是个新创建的cell那么把他的点击事件流merge进去。
好了,现在我们拿到了所有cell对象的点击事件流了,下一步,把他转换成需要的数据对象传给viewModel进行监听。说明一下,我这里用的刷新控件是第三方的MJReFresh,同时也创建了几个binder很方便的进行操作.
这是RxDatasource中的datasource
let datasource = RxTableViewSectionedAnimatedDataSource<SectionOfCancelOrder>(configureCell: { data,tb,index,item in
guard let cell = tb.dequeueReusableCell(withIdentifier: FinishedOrdersTableViewCell.identifier) as? FinishedOrdersTableViewCell else{fatalError()}
cell.routeType = Router(rawValue: item.truckMode) ?? .direct
if item.isShortDistance == 0{
cell.timeLabel.text = Helper.pbcGenericFormatter.string(from: Date(timeIntervalSince1970: item.consignorTimestamp.sInterval)) + "-" + Helper.pbcMinutsFormatter.string(from: Date(timeIntervalSince1970: (item.consignorTimestamp + 21600000).sInterval)) + "出发"
}else{
cell.timeLabel.text = Helper.pbcGenericFormatter.string(from: Date(timeIntervalSince1970: item.consignorTimestamp.sInterval)) + "出发"
}
cell.sourceLabel.text = item.consignorAddress
cell.destinationLabel.text = item.receiverAddress
cell.routeDistanceLabel.text = "\(item.mileage)km"
cell.priceLabel.text = "¥" + item.driverIncomde.price
cell.markLabel.text = item.seriesName + " " + (TruckType(rawValue: item.platformtruckType) ?? .oblique).description + " " + item.consignorRemark
cell.rating = 3
cell.selectionStyle = .none
return cell
})
我们把viewModel创建出来,并且赋值给他点击事件流
viewModel = FinishedViewModel(refresh: tableView.mj_header.rx.refreshing, getMore: tableView.mj_footer.rx.refreshing, orderDelete: deleteTaps.flatMap{$0}.throttle(2, latest: false, scheduler: MainScheduler.instance).map{datasource[$0].id})
解释一下最后一个参数orderDelete.
deleteTaps是我们merge出来的事件流,里面是Observable<IndexPath>类型,我们进行一次压平操作flatMap,拿到indexPath,然后我这里用了一个Throttle来防止点击过快,最后通过datasource和map操作赋,把indexPath元素转换成了一个数据对象,并且因为我这里需要整个数据对象,只取了他的id进行请求删除就可以了。
- 在viewModel的初始化方法里面,进行相关绑定
init(refresh:ControlEvent<Void>,getMore:ControlEvent<Void>,orderDelete:Observable<Int>) {
noDataImageHidden = orders.map{ $0[0].items.count != 0 }
refresh.subscribe(onNext: { [weak self](_) in
self?.requestList(state: .refresh)
}, onError: nil, onCompleted: nil, onDisposed: nil).disposed(by: disposeBag)
getMore.subscribe(onNext: { [weak self](_) in
self?.requestList(state: .getMore)
}, onError: nil, onCompleted: nil, onDisposed: nil).disposed(by: disposeBag)
orderDelete.subscribe(onNext: { [weak self](id) in
self?.requestDelete(oid: id)
}, onError: nil, onCompleted: nil, onDisposed: nil).disposed(by: disposeBag)
}
监听每一次删除按钮点击事件,并且拿到对应的数据id请求删除
请求删除成功后
func requestDelete(oid:Int){
normalProvider.rx.request(MultiTarget(MyOrderTarget.deleteOrder(oid: oid))).mapEmpty().subscribe(onSuccess: { (_) in
self.beginFresh.onNext(())
}, onError: nil).disposed(by: disposeBag)
}
let beginFresh:BehaviorSubject<Void> = BehaviorSubject(value: ())
beginFresh是一个behaviorSubject,默认有一个元素,进行页面的初始刷新,然后我们没删除成功一个cell后,让beginFresh发出一个元素,来进行刷新操作
viewModel.beginFresh.bind(to: tableView.mj_header.rx.beginRefreshing).disposed(by: disposeBag)
beginFresh绑定到了tableView刷新控件上面,每收到一个元素,进行一次自动下拉操作,而下拉操作在viewModel初始化的时候已经绑定了刷新列表的请求,列表刷新成功又会通过rxDatasource的diff来动画的删除view也就是cell。整个绑定模块层层递进,单向数据源。
是的没有错,是不是发现还不如delegate,嗯,用个🐔的响应式,写尼玛呢,算了自己想出来的想法,跪着也要搞完。。
这就是本人想出来的解决这种情形的一个rx的解决方法。通过比较发现执行操作更加少,不用额外定义什么delegate或者closure,只需要创建一个cell事件流容器然后在cellWillDisplay上merge就可以监听这个容器拿到任何我们想拿到的东西。
有不对的地方欢迎指正。。
补充:目前这种方式这能有一个triggerButton,有的时候一个cell上可能会有两个多个触发操作的按钮而cellButtonTriggerble协议只有一个UIButton。由于目前本人没有遇到这种需求所以代码上没加上去。解决方式是:cellButtonTriggerble协议上可以有一个button数组,然后binder上分别监听发出来indexpath,因为他们的点击发出的indexPath都是一样的,只是要触发的操作不同,可以设置button的Tag来区分,然后根据不同的tag来merge到不同的按钮点击的容器,分别进行监听请求等操作。