RxSwift by Examples 分成 4 部分。以下是 PART 1 的學(xué)習(xí)筆記和翻譯整理趴拧。原文在這里。
RxSwfit 是用 Swift 來實(shí)現(xiàn)的一個(gè)響應(yīng)式拓展。ReactiveX 是觀察者模式弊攘、迭代者模式和函數(shù)式編程的優(yōu)點(diǎn)組合。
你需要根本上改變你的視野姑曙,從靜態(tài)地分配一個(gè)值給變量襟交,到觀察將可能在未來會(huì)改變的某物。
為什么要使用它伤靠?答案是:簡(jiǎn)單捣域。用信號(hào) signals 取代難以測(cè)試的通知 notifications。用 block 取代 delegate宴合,委托要將代碼寫在多個(gè)地方焕梅,block 還可以移除大量的 switch/if。Rxswift 平滑地處理了 KVO卦洽、IBAction, 輸入 filter贞言、MVVM 等等。
術(shù)語
你的手機(jī)是一個(gè)可被觀察者 observable阀蒂,它發(fā)出信號(hào)该窗,比如 Facebook 的通知消息。你訂閱了它蚤霞,所以你可以從 home 屏幕收到每個(gè)推送通知酗失。于是你可以決定如何處理這些信號(hào) signal。你是一個(gè)觀察者昧绣。
例子
我們將寫一個(gè) City Searcher规肴,在搜索框中輸入城市,動(dòng)態(tài)地顯示列表夜畴。當(dāng)你在 search bar 中輸入的時(shí)候拖刃,我們動(dòng)態(tài)地獲取以輸入字母開頭的城市并顯示在 table view 中。
似乎很簡(jiǎn)單是嗎贪绘?當(dāng)你試著創(chuàng)建動(dòng)態(tài)搜索時(shí)兑牡,你不得不考慮異常的情況。比如說我輸入非惩么兀快并且經(jīng)常改變想法发绢?我們會(huì)有很多 api 請(qǐng)求需要過濾硬耍。在真實(shí)的 app 中,你需要取消之前的請(qǐng)求边酒,在發(fā)出另一個(gè)請(qǐng)求之前等待一段時(shí)間经柴,檢查輸入是否和之前的一樣,等等墩朦。咋看似乎很簡(jiǎn)單的任務(wù)創(chuàng)造了大量的邏輯坯认。
當(dāng)然沒有 Rx 你也可以做到,但是我們看看如何用一點(diǎn)點(diǎn)代碼實(shí)現(xiàn)這些邏輯氓涣。
Podfile
創(chuàng)建項(xiàng)目并用 pod 安裝 RxSwift + RxCocoa
platform :ios, '8.0'
use_frameworks!
target 'RxSwiftExample' do
pod "RxSwift"
pod "RxCocoa"
end
創(chuàng)建UI
UISearchBar + UITableView
變量
為了減少代碼邏輯我們先不用 API 請(qǐng)求牛哺。我們需要兩個(gè)數(shù)組,一個(gè)存所有城市劳吠,另一個(gè)存需要顯示的城市引润。
var shownCities = [String]() // Data source for UITableView
let allCities = ["New York", "London", "Oslo", "Warsaw", "Berlin", "Praga"] // Our mocked API data source
然后我們需要設(shè)置 UITableViewDataSource 并連接到變量 shownCities。
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return shownCities.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cityPrototypeCell", for: indexPath)
cell.textLabel?.text = shownCities[indexPath.row]
return cell
}
觀察
開始更有趣的部分痒玩。我們將觀察 UISearchaBar 中的 text淳附。RxCocoa 已經(jīng)為我們創(chuàng)建好了。Rx 團(tuán)隊(duì)支持許多 Cocoa 框架蠢古,包括 UISearchBar 和許多控制器奴曙。在我們的例子中,我們使用 UISearchBar草讶,我們可以使用它的屬性 rx.text洽糟,每次 search bar 中的 text 改變的時(shí)候,它就會(huì)發(fā)出信號(hào)堕战。怎樣觀察這個(gè)東西呢坤溃?首先引入 RxCocoa 和 RxSwift
import RxCocoa
import RxSwift
進(jìn)入觀察部分:在 viewDidLoad() 中,我們?yōu)?UISearchBar 添加對(duì) rx.text 屬性的觀察践啄。
searchBar
.rx.text // Observable property thanks to RxCocoa
.orEmpty // Make it non-optional
.subscribe(onNext: { [unowned self] query in // Here we will be notified of every new value
self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // We now do our "API Request" to find cities.
self.tableView.reloadData() // And reload table view data.
})
.addDisposableTo(disposeBag)
動(dòng)態(tài)搜索已經(jīng)做好了浇雹。subscribe 很好理解 —— 我們訂閱了可觀察的 property沉御,它產(chǎn)生信號(hào)屿讽。就像你告訴你的手機(jī):每次收到新消息請(qǐng)顯示給我。它將顯示所有最新的值吠裆,這是我們唯一需要的伐谈,不過 subscribe 還包裝了很多事件比如 onError, onCompleted 等等试疙。
更有意思的是最后一行诵棵。當(dāng)你訂閱了 observables,你通常想要在這個(gè)對(duì)象被銷毀 deallocated 的時(shí)候取消訂閱祝旷。在 Rx 中用 DisposeBag 裝載所有你想要在 deinit 進(jìn)程中取消訂閱的東西履澳。通常你需要?jiǎng)?chuàng)建這個(gè) bag 并把廢棄物丟給它嘶窄。
var shownCities = [String]() // Data source for UITableView
let allCities = ["New York", "London", "Oslo", "Warsaw", "Berlin", "Praga"] // Our mocked API data source
let disposeBag = DisposeBag() // Bag of disposables to release them when view is being dea
優(yōu)化
編譯后我們就有一個(gè)正常工作的 app 了。但是我們所擔(dān)心的那些事情呢距贷?泛濫的 api 請(qǐng)求柄冲?空輸入?延遲忠蝗?對(duì)现横,我們需要保護(hù)自己,保護(hù)我們的 api 后端阁最。我們需要加延遲戒祠,在輸入 x 秒之后發(fā)出請(qǐng)求,并且僅在輸入改變的情況下速种。通常我們需要?jiǎng)?chuàng)建一個(gè) NSTimer 來 fire 和 delay 后 invalidate 它姜盈,如果有新的輸入。
當(dāng)我們輸入 O配阵,結(jié)果顯示贩据,然后改變主意輸入 oc,然后迅速又改回 o闸餐,在延遲之前饱亮,api 請(qǐng)求發(fā)出。這時(shí)候我們發(fā)出了兩個(gè)同樣的請(qǐng)求舍沙。通常我們需要延遲 0.5 秒近上。沒有 Rx 的話,我們需要添加 flag 給搜索請(qǐng)求拂铡,并且對(duì)比它是否與新的請(qǐng)求一樣壹无。隨著邏輯的不斷增加,這有太多代碼要寫了感帅。在 RxSwift 中做這個(gè)事情只需要兩行斗锭。
debounce()
制造延遲效果,distinctUntilChanged()
在值相同的時(shí)候保護(hù)我們失球。
searchBar
.rx.text // Observable property thanks to RxCocoa
.orEmpty // Make it non-optional
.debounce(0.5, scheduler: MainScheduler.instance) // Wait 0.5 for changes.
.distinctUntilChanged() // If they didn't occur, check if the new value is the same as old.
.subscribe(onNext: { [unowned self] query in // Here we subscribe to every new value
self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // We now do our "API Request" to find cities.
self.tableView.reloadData() // And reload table view data.
})
.addDisposableTo(disposeBag)
但是我們還忘了一件事岖是。我們輸入一些值,刷新了 table view实苞,然后又刪除這些輸入的時(shí)候呢豺撑?我們發(fā)出了空參數(shù)的請(qǐng)求。Swift 已經(jīng)內(nèi)建了 filter()
函數(shù)黔牵。為什么我要在一個(gè)值中使用 filter 呢聪轿?filter()
不是作用于集合嗎?嗯猾浦,不要把可觀察者 Observable 看作一個(gè)值/對(duì)象陆错,它是一系列值的流灯抛,這樣你將很容易理解函數(shù)式 block 的使用。為了過濾值我們對(duì)待它如同對(duì)待 string 數(shù)組一樣音瓷。
.filter { !$0.isEmpty } // Filter for non-empty query.
完整的代碼只需要 9 行牧愁,實(shí)現(xiàn)了并不簡(jiǎn)單的邏輯。
searchBar
.rx.text // Observable property thanks to RxCocoa
.orEmpty // Make it non-optional
.debounce(0.5, scheduler: MainScheduler.instance) // Wait 0.5 for changes.
.distinctUntilChanged() // If they didn't occur, check if the new value is the same as old.
.filter { !$0.isEmpty } // If the new value is really new, filter for non-empty query.
.subscribe(onNext: { [unowned self] query in // Here we subscribe to every new value, that is not empty (thanks to filter above).
self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // We now do our "API Request" to find cities.
self.tableView.reloadData() // And reload table view data.
})
.addDisposableTo(disposeBag)
結(jié)束外莲。