美文网首页Rxswift
用RX优雅处理TableViewCell中按钮点击

用RX优雅处理TableViewCell中按钮点击

作者: 睿智的盛子康 | 来源:发表于2018-11-29 15:18 被阅读86次

使用背景:

在开发中,我们经常会碰到这种需求样式,tableView上的cell上有按钮,像是删除,提交,查看等按钮,点击按钮进行操作,像这种


8F8005AB-FAA5-49D9-80D3-E8727FFD8C2A.png

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里按钮点击执行这个方法。


image.png

同样需要注意内存泄漏的问题,在闭包里用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。整个绑定模块层层递进,单向数据源。
这就是本人想出来的解决这种情形的一个rx的解决方法。通过比较发现执行操作更加少,不用额外定义什么delegate或者closure,只需要创建一个cell事件流容器然后在cellWillDisplay上merge就可以监听这个容器拿到任何我们想拿到的东西。

QQ20181129-161539-HD.gif

又不对的地方欢迎指正。。

补充:目前这种方式这能有一个triggerButton,有的时候一个cell上可能会有两个多个触发操作的按钮而cellButtonTriggerble协议只有一个UIButton。由于目前本人没有遇到这种需求所以代码上没加上去。解决方式是:cellButtonTriggerble协议上可以有一个button数组,然后binder上分别监听发出来indexpath,因为他们的点击发出的indexPath都是一样的,只是要触发的操作不同,可以设置button的Tag来区分,然后根据不同的tag来merge到不同的按钮点击的容器,分别进行监听请求等操作。

相关文章

网友评论

    本文标题:用RX优雅处理TableViewCell中按钮点击

    本文链接:https://www.haomeiwen.com/subject/jitfcqtx.html