RxCocoa, 從 OOP 到 FRP: 以 tableView 舉例說明
OOP, 程序寫起來,很爽。程序修修補補,想到哪里,改到哪里。問題是,自己的代碼很久不看,代碼思路忘記了。又要修改的時候,比較煩,代碼從頭看起來。全局搜索,搞起來。(看別人的代碼,也是同樣的感受)
FRP, 寫起來,也很爽。代碼都在一塊,邏輯相對清晰,不用到處找。代碼結構需要設計,設計好了,就有美感。有一點數據結構與算法知識的味道。設計不好,自己的編程極限就有些體現出來了。
程序是數據結構+算法。淺顯一些,數據結構,就是手上有什么數據,結構化的數據,程序可以作用到的。算法,就是就是拿到數據,怎么辦。
開個 tableView, 以 OOP 面向對象的方式:
讀 RxCocoa 源碼中的 TableView 部分: 函數柯里化,函數泛型,尾閉包
這樣調用
let items = Observable.just(
(0..<20).map { "\($0)" }
)
items
.bind_deng_table(to: tableView.rx.items_deng(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
cell.textLabel?.text = "\(element) @ row \(row)"
}
.disposed(by: disposeBag)
有一個尾閉包,展開一下,就是
items.bind_deng_table(to: tableView.rx.items_deng(cellIdentifier: "Cell", cellType: UITableViewCell.self)
, curriedArgument: {
(row, element, cell) in
cell.textLabel?.text = "\(element) @ row \(row)"
}).disposed(by: disposeBag)
知識點: 尾閉包,就是語法糖。讓最后一個參數,如果是函數,就以閉包的形式。
展開后,就清晰了一些,items 是事件源,要交給 tableView 去處理。
調用了 bind_deng_table
函數,需要傳進去兩個參數,兩個參數都是匿名函數。
看一下 bind_deng_table
這個函數:
public func bind_deng_table<R1, R2>(to binderDa: (Self) -> (R1) -> R2, curriedArgument: R1) -> R2 {
return binderDa(self)(curriedArgument)
}
這里有函數的柯里化,和函數的泛型。
第一個參數 binder: (Self) -> (R1) -> R2
,對于柯里化,就是從后往前讀,這個匿名函數,輸出 R2, 返回 R2, 輸入 (Self) -> (R1), 輸入一個匿名函數 (Self) -> (R1)
a, 匿名函數 a, 輸入 (Self) -> 輸出 (R1)
對于柯里化,我寫錯了
RxDataSource 使用套路與解釋: RxSwift 方法調用時機的轉移
RxSwift 非常強大,用得好,很爽
套路: tableView 刷新以后,就是有了數據源,怎樣來一個回調。
場景舉例,就是兩表關聯。 RxDataSource,怎樣列表刷新出來,就自動選擇第一個。然后子列表根據上一個列表的選擇,確認要刷新的數據。
問題是 RxDataSource 專注于列表視圖的數據處理,自動選擇第一個 row 是 tableViewDelegate 干的事情。
左邊一個列表 tableView, 右邊一個列表 tableView , 兩個列表之間的數據存在關聯,左邊列表是一級選項,右邊列表是對應左邊的二級選項。
一般可以這么做:
private var lastIndex : NSInteger = 0
// ...
// 看這一段代碼,就夠了
let leftDataSource = RxTableViewSectionedReloadDataSource<CategoryLeftSection>( configureCell: { [weak self] ds, tv, ip, item in
guard let strongSelf = self else { return UITableViewCell()}
let cell : CategoryLeftCell = tv.dequeueReusableCell(withIdentifier: "Cell1", for: ip) as! CategoryLeftCell
cell.model = item
// 看這一句代碼,就夠了
if ip.row == strongSelf.lastIndex {
// ...
tv.selectRow(at: ip, animated: false, scrollPosition: .top)
tv.delegate?.tableView!(tv, didSelectRowAt: ip)
}
return cell
})
vmOutput!.sections.asDriver().drive(self.leftMenuTableView.rx.items(dataSource: leftDataSource)).disposed(by: rx.disposeBag)
// 選擇左邊列表,給右邊提供的數據
let rightPieceListData = self.leftMenuTableView.rx.itemSelected.distinctUntilChanged().flatMapLatest {
[weak self](indexPath) -> Observable<[SubItems]> in
guard let strongSelf = self else { return Observable.just([]) }
strongSelf.currentIndex = indexPath.row
if indexPath.row == strongSelf.viewModel.vmDatas.value.count - 1 {
// ...
strongSelf.leftMenuTableView.selectRow(at: strongSelf.currentSelectIndexPath, animated: false, scrollPosition: .top)
strongSelf.leftMenuTableView.delegate?.tableView!(strongSelf.leftMenuTableView, didSelectRowAt: strongSelf.currentSelectIndexPath!)
return Observable.just((strongSelf.currentListData)!)
}
if let subItems = strongSelf.viewModel.vmDatas.value[indexPath.row].subnav {
// ...
var reult:[SubItems] = subItems
// ...
strongSelf.currentListData = reult
strongSelf.currentSelectIndexPath = indexPath
return Observable.just(reult)
}
return Observable.just([])
}.share(replay: 1)
// 右邊列表的數據源,具體 cell 的配置方法
let rightListDataSource = RxTableViewSectionedReloadDataSource<CategoryRightSection>( configureCell: { [weak self]ds, tv, ip, item in
guard let strongSelf = self else { return UITableViewCell() }
if strongSelf.lastIndex != strongSelf.currentIndex {
tv.scrollToRow(at: ip, at: .top, animated: false)
strongSelf.lastIndex = strongSelf.currentIndex
}
if ip.row == 0 {
let cell :CategoryListBannerCell = CategoryListBannerCell()
cell.model = item
return cell
} else {
let cell : CategoryListSectionCell = tv.dequeueReusableCell(withIdentifier: "Cell2", for: ip) as! CategoryListSectionCell
cell.model = item
return cell
}
})
// 設置右邊列表的代理
rightListTableView.rx.setDelegate(self).disposed(by: rx.disposeBag)
// 右邊列表需要取得的數據,傳遞給右邊列表的配置方法
rightPieceListData.map{ [CategoryRightSection(items:$0)] }.bind(to: self.rightListTableView.rx.items(dataSource: rightListDataSource))
.disposed(by: rx.disposeBag)
上面這個實現很 Low, 選擇的時機是這個 if ip.row == strongSelf.lastIndex {
, 當更新數據到指定 cell 時候,操作。
程序可以跑,但是不優雅。
如果把上面的邏輯翻譯成面向對象,就是:
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : CategoryLeftCell = tv.dequeueReusableCell(withIdentifier: "Cell1", for: ip) as! CategoryLeftCell
cell.model = item
// 看這一句代碼,就夠了
if ip.row == strongSelf.lastIndex {
// ...
tv.selectRow(at: ip, animated: false, scrollPosition: .top)
tv.delegate?.tableView!(tv, didSelectRowAt: ip)
// ...
}
return cell
}
這樣展開,清晰了一些。我們是不會這么用的,如果不用 Rx, 直接 OOP.
我們是這么用
刷新完了之后,選中一下
tableView.reloadData()
tv.selectRow(at: ip, animated: false, scrollPosition: .top)
// 其他操作
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : CategoryLeftCell = tv.dequeueReusableCell(withIdentifier: "Cell1", for: ip) as! CategoryLeftCell
cell.model = item
return cell
}
看一看源碼
leftDataSource
通過泛型指明每個 tableView section 的數據結構,他唯一的參數是一個閉包, configureCell .
let rightListDataSource = RxTableViewSectionedReloadDataSource<CategoryLeftSection>( configureCell: { [weak self] ds, tv, ip, item in
// ...
}
configureCell 實際上就是系統提供的代理方法 open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
RxDataSource 的源代碼挺清晰的,首先采用前提條件 precondition
, row 確保不會越界。然后調用 configureCell
匿名函數。這樣的設計,借用了 Swift 中函數是一級公民,函數可以像值一樣傳遞。
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
precondition(indexPath.item < _sectionModels[indexPath.section].items.count)
return configureCell(self, tableView, indexPath, self[indexPath])
}
更好的調用時機:
繼承 TableViewSectionedDataSource
, 創建自己想要的子類,任意根據需求改方法。
做一個繼承
final class MyDataSource<S: SectionModelType>: RxTableViewSectionedReloadDataSource<S> {
private let relay = PublishRelay<Void>()
var rxRealoded: Signal<Void> {
return relay.asSignal()
}
override func tableView(_ tableView: UITableView, observedEvent: Event<[S]>) {
super.tableView(tableView, observedEvent: observedEvent)
// Do diff
// Notify update
relay.accept(())
}
}
因為這個場景下, super.tableView(tableView, observedEvent: observedEvent)
, 調用之后,需要接受一下事件,以后把它發送出去。所以要用一個 PublishSubject
.
PublishRelay
是對 PublishSubject
的封裝,區別是他的功能沒 PublishSubject
那么強,PublishRelay
少兩個狀態,完成 completed 和出錯 error, 適合更加專門的場景。
Signal
信號嘛,共享事件流 SharedSequence。他是對 Observable
的封裝。具有在主線程調用等特性,更適用于搞 UI .
簡單優化下,代碼語義更加明確
// left menu 數據源
let leftDataSource = MyDataSource<CategoryLeftSection>( configureCell: { ds, tv, ip, item in
let cell : CategoryLeftCell = tv.dequeueReusableCell(withIdentifier: "Cell1", for: ip) as! CategoryLeftCell
cell.model = item
return cell
})
// 刷新完了,做一下選擇
leftDataSource.rxRealoded.emit(onNext: { [weak self] in
guard let self = self else { return }
let indexPath = IndexPath(row: 0, section: 0)
self.leftMenuTableView.selectRow(at: indexPath, animated: false, scrollPosition: UITableView.ScrollPosition.none)
self.leftMenuTableView.delegate?.tableView?(self.leftMenuTableView, didSelectRowAt: indexPath)
}).disposed(by: rx.disposeBag)
// ...
// 其余不變
所謂代碼的語義
FRP 就是把要干嘛,直接寫清楚,不會像 OOP 那樣,過度的見縫插針,調用到了,就改一下狀態。
聲明式編程是只在一處修改。就算把那一處修改,聲明式編程也只在一處調用。OOP 就找的比較辛苦。
git repo , 我放在 coding.net 上面了,pod 都裝好了,下載了直接跑