用RX優(yōu)雅處理TableViewCell中按鈕點擊

使用背景:

在開發(fā)中,我們經(jīng)常會碰到這種需求樣式,tableView上的cell上有按鈕,像是刪除,提交,查看等按鈕,點擊按鈕進行操作,像這種


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

tableView上的代理只能給到cell的點擊事件,所以按鈕點擊我們要自己處理,首先要拿到是第幾個cell上的按鈕點擊,然后拿到數(shù)據(jù)模型,根據(jù)數(shù)據(jù)模型來進行操作,如:圖上的刪除按鈕點擊后拿到cell的indexpath,然后拿到具體數(shù)據(jù),根據(jù)具體數(shù)據(jù)id進行刪除操作,然后更行表格。

解決方式:

這種樣式需求比較常見,解決起來也比較簡單,本人比較常用的有幾個方法

1.delegate. 在cell上有個我們自定義delegate,delegate上有個方法處理刪除按鈕點擊的操作,接受一個tableViewCell的參數(shù)。用target action監(jiān)聽刪除按鈕的點擊事件,在方法里執(zhí)行delegate里面的方法。tableView的cellForRow的代理方法里面給cell賦值屬性的時候,把delegate設置成self。注意:delegate用weak修飾,這里會引發(fā)內(nèi)存泄漏。
控制器實現(xiàn)delegate,拿到參數(shù)TableViewCell后,用自己的tableView方法執(zhí)行indexPathForCell的方法拿到indexPath,然后用自己的數(shù)據(jù)數(shù)組找出indexPath的下標拿到具體數(shù)據(jù)并且進行請求等操作,完成后reloadTableView

優(yōu)點:簡單,容易理解。
缺點:操作步驟多,容易疏漏出錯,而且每種這種需求都伴隨著protocol,很難受
2.closure. 具體操作和delegate方式差不多,只是把delegate換成了閉包,不需要額外定義protocol而是在cell上定義一種方法類型,然后tableView給cell賦值屬性的時候,把方法體賦值給cell,cell里按鈕點擊執(zhí)行這個方法。


image.png

同樣需要注意內(nèi)存泄漏的問題,在閉包里用weak若引用。因為ViewController擁有tableView,tableView擁有cell,只有ViewController銷毀cell才能銷毀,但是cell里有delegate或者閉包指向ViewController,引用循環(huán),內(nèi)存泄漏.

優(yōu)點:減少了額外定義的protocol,使用閉包代替,減少代碼量
缺點:操作步驟依然很多,缺點和delegate差不多,只是換了一種形式,而且可能存在閉包嵌套的可能,導致代碼難維護和理解

用RX優(yōu)雅解決這種問題:

用多了這種delegate或者closure,就會感覺到自己的代碼雜亂無章,而且最近一直在學習使用Rx,Rx試圖將通信模式統(tǒng)一起來,也就是說一般用了Rx,就最好不要用delegate,targetAction什么的了,全部用綁定的形式來做,代碼量少,穩(wěn)定性高,易維護,易理解。
而這種情況下,cell的按鈕點擊怎么用Rx來替代呢,基于這幾種困擾,想了一個不太好的方法,只是自己的一個思考方式,記錄下來了。覺得有問題可以隨時歡迎指出來。。

  • 把按鈕的點擊事件用rxCocoa中的controlEvent來代替target action. 怎么把這個ControlEvent給到ViewController呢,我們可以用創(chuàng)建Binder監(jiān)聽者。但是這個事件流是在cell里面的,怎么才能統(tǒng)一綁定起來呢,或者說監(jiān)聽特定cell上的按鈕點擊。我是用protocol方法解決的。
protocol cellButtonTriggerble:NSObjectProtocol{
    var triggerButton:UIButton{get}
    var canTrigger:Bool{get set}
}

tableViewCell實現(xiàn)這個協(xié)議。triggerButton就是比如刪除按鈕什么的,能夠觸發(fā)操作的按鈕,canTrigger是一個標示,作用下面介紹,cell里賦值為false

  • 創(chuàng)建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已經(jīng)綁定過了。然后把triggerButton的tap的controlEvent 通過map操作符先用weak弱飲用,然后拿到父類tableView,進行方法IndexPathForRow拿到indexPath發(fā)出去.

*綁定:
整理一下思路,把cell指定類型上的cell的trigger按鈕的點擊事件轉(zhuǎn)換成indexPath信號發(fā)出來。然后我們可以拿到這個事件流,現(xiàn)在有一個問題,怎么拿到?而且這是一個cell對象的事件流,tableView上有好幾個cell對象,而且還有復用機制,cell復用一個對象等。
首先,在willDisplayCell上可以不侵入cell的賦值屬性代碼上拿到這個事件流。那么復用的cell怎么辦,如果重復監(jiān)聽會導致按鈕點擊一下,好幾個indexPath信號發(fā)出來。所以上面的唯一標識符就有作用了。
定義一個數(shù)據(jù)流來接受所有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對象還沒有進行綁定,是個新創(chuàng)建的cell那么把他的點擊事件流merge進去。
好了,現(xiàn)在我們拿到了所有cell對象的點擊事件流了,下一步,把他轉(zhuǎn)換成需要的數(shù)據(jù)對象傳給viewModel進行監(jiān)聽。說明一下,我這里用的刷新控件是第三方的MJReFresh,同時也創(chuàng)建了幾個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)) + "出發(fā)"
            }else{
                cell.timeLabel.text = Helper.pbcGenericFormatter.string(from: Date(timeIntervalSince1970: item.consignorTimestamp.sInterval)) + "出發(fā)"
            }
            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創(chuàng)建出來,并且賦值給他點擊事件流

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})

解釋一下最后一個參數(shù)orderDelete.
deleteTaps是我們merge出來的事件流,里面是Observable<IndexPath>類型,我們進行一次壓平操作flatMap,拿到indexPath,然后我這里用了一個Throttle來防止點擊過快,最后通過datasource和map操作賦,把indexPath元素轉(zhuǎn)換成了一個數(shù)據(jù)對象,并且因為我這里需要整個數(shù)據(jù)對象,只取了他的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)
    }

監(jiān)聽每一次刪除按鈕點擊事件,并且拿到對應的數(shù)據(jù)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發(fā)出一個元素,來進行刷新操作

viewModel.beginFresh.bind(to: tableView.mj_header.rx.beginRefreshing).disposed(by: disposeBag)

beginFresh綁定到了tableView刷新控件上面,每收到一個元素,進行一次自動下拉操作,而下拉操作在viewModel初始化的時候已經(jīng)綁定了刷新列表的請求,列表刷新成功又會通過rxDatasource的diff來動畫的刪除view也就是cell。整個綁定模塊層層遞進,單向數(shù)據(jù)源。

是的沒有錯,是不是發(fā)現(xiàn)還不如delegate,嗯,用個??的響應式,寫尼瑪呢,算了自己想出來的想法,跪著也要搞完。。

這就是本人想出來的解決這種情形的一個rx的解決方法。通過比較發(fā)現(xiàn)執(zhí)行操作更加少,不用額外定義什么delegate或者closure,只需要創(chuàng)建一個cell事件流容器然后在cellWillDisplay上merge就可以監(jiān)聽這個容器拿到任何我們想拿到的東西。

QQ20181129-161539-HD.gif

有不對的地方歡迎指正。。

補充:目前這種方式這能有一個triggerButton,有的時候一個cell上可能會有兩個多個觸發(fā)操作的按鈕而cellButtonTriggerble協(xié)議只有一個UIButton。由于目前本人沒有遇到這種需求所以代碼上沒加上去。解決方式是:cellButtonTriggerble協(xié)議上可以有一個button數(shù)組,然后binder上分別監(jiān)聽發(fā)出來indexpath,因為他們的點擊發(fā)出的indexPath都是一樣的,只是要觸發(fā)的操作不同,可以設置button的Tag來區(qū)分,然后根據(jù)不同的tag來merge到不同的按鈕點擊的容器,分別進行監(jiān)聽請求等操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容