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

使用背景:

在开发中,我们经常会碰到这种需求样式,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。整个绑定模块层层递进,单向数据源。

是的没有错,是不是发现还不如delegate,嗯,用个🐔的响应式,写尼玛呢,算了自己想出来的想法,跪着也要搞完。。

这就是本人想出来的解决这种情形的一个rx的解决方法。通过比较发现执行操作更加少,不用额外定义什么delegate或者closure,只需要创建一个cell事件流容器然后在cellWillDisplay上merge就可以监听这个容器拿到任何我们想拿到的东西。

QQ20181129-161539-HD.gif

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容