20170504 RunTime





















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)

對于柯里化,我寫錯了

iOS---防止UIButton重復點擊的三種實現方式

iOS防止Button重復點擊

iOS網絡請求緩沖優化HttpCachesLoader

iOS應用架構談 網絡層設計方案

IOS應用架構思考一(網絡層)

RxDataSource 使用套路與解釋: RxSwift 方法調用時機的轉移

RxSwift 非常強大,用得好,很爽

套路: tableView 刷新以后,就是有了數據源,怎樣來一個回調。

場景舉例,就是兩表關聯。 RxDataSource,怎樣列表刷新出來,就自動選擇第一個。然后子列表根據上一個列表的選擇,確認要刷新的數據。

問題是 RxDataSource 專注于列表視圖的數據處理,自動選擇第一個 row 是 tableViewDelegate 干的事情。

output.png

左邊一個列表 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 都裝好了,下載了直接跑

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容