原文:https://realm.io/news/tryswift-Marin-Todorov-I-create-iOS-apps-is-RxSwift-for-me/
在本次try! Swift NYC talk活動中骆膝,Marin Todorov介紹了RxSwift在一個iOS開發(fā)人員日常工作中的使用案例(RxSwift是一個異步,基于事件處理的框架)吱晒。如果你希望以引入一個庫的成本來解決你大半部分的痛苦的話,那么這篇文章對你最合適不過柠逞。
簡單介紹
Reactive Extensions,簡稱Rx粥谬,大量采用了observable序列和link style操作運算进胯,是用來解決事件和異步處理的庫伐庭,使用這個庫粉渠,開發(fā)者處理異步數(shù)據(jù)流不要太爽。
Marin表示說似忧,我讀了Rx理論很多次渣叛,但是我也沒法搞清楚iOS開發(fā)日常中如何去使用它丈秩,我踩過一些坑盯捌,今天就想介紹一下我用Rx解決過的一些實際問題。
Rx響應式的應用
首先來看看Rx響應式的方面蘑秽。使用Rx的響應式饺著,數(shù)據(jù)改變后會直接push到你 而不需要你再次主動pull數(shù)據(jù)箫攀。
Array < String >
這個array,作為一個strings的集合類幼衰,有時候處理起來有點小麻煩靴跛,你需要去遍歷它其中每個子元素,根據(jù)子元素的情況去處理數(shù)據(jù)渡嚣,即便采用更Swifty的方法梢睛,比如forEach這樣的,也還是挺麻煩识椰,因為本質(zhì)都還一樣绝葡,你需要提供一個閉包,一些block的代碼腹鹉,得一個元素接一個元素的處理藏畅。
問題在于,你處理這個array的時候功咒,是在一個固定的時間點愉阎,而這個時間點的數(shù)據(jù)也是固定的。而有新數(shù)據(jù)加到這個array中的時候力奋,你是不知道的榜旦。
拿table view controller來舉例。它綁定一個含有三個元素的array景殷,用戶點擊增加的按鈕章办,array里數(shù)據(jù)變成四個了,但是界面上還是三個滨彻。你或許可以采用通知或者委托的方式來同步數(shù)據(jù)model和UI這樣藕届。但是如果添加數(shù)據(jù)恰好是個異步耗時的過程,那就有點麻煩了亭饵。
Observable < Array < String > >
Rx把數(shù)據(jù)封裝成一個observable類休偶,這樣一個類,可以讓你本來的數(shù)據(jù)有時間的跨度辜羊,簡單的說踏兜,是給數(shù)據(jù)加上一個"時間的維度"
在這個例子中,observable序列定義了每個元素需要做的處理方法八秃,例如:print每個元素碱妆。在初始化的時候,每個元素會執(zhí)行一遍這個方法昔驱。當有新元素加入的時候疹尾,會再執(zhí)行一遍這個方法,以前你需要不僅要定義管做什么事情,還要管什么時間做纳本,現(xiàn)在你只需要這些元素各自的處理方法窍蓝,然后一但數(shù)據(jù)有新的變化,會自動執(zhí)行處理方法繁成。
我覺得這就是Rx最牛逼的地方吓笙,把需要異步處理變成線性的,這樣就把事情簡化了巾腕。你都不需要考慮你現(xiàn)在有啥數(shù)據(jù)面睛,過去有啥數(shù)據(jù),將來可能都有啥情況尊搬,而只需要定義數(shù)據(jù)驅(qū)動的處理方法塊就可以了侮穿。在上面的例子中,你如果想在tableview里顯示一個list毁嗦,就只需要線性的碼好從數(shù)據(jù)到UI的渲染代碼亲茅。把list中元素加加減減,數(shù)據(jù)變化驅(qū)動的錯綜復雜事件和交互都交給RxSwift打點就好狗准。
Observables特別好用克锣,再舉個例子,你在text filed輸入的字符串需要在某個label中顯示腔长,你只要實現(xiàn)數(shù)據(jù)如何顯示到label的代碼就行了袭祟,其他用戶輸入事件的激活等其他環(huán)節(jié)就交給RxSwift。
有了RxSwift捞附,事情變得觸手可及巾乳,簡單,且線性
再考慮一個復雜點的情況鸟召,scrollview中的翻頁加載數(shù)據(jù)問題胆绊,如果啟用了RxSwift,你只需要實現(xiàn)的翻頁代碼就比較簡單了:只需要實現(xiàn)從服務器加載20條數(shù)據(jù)并加入到list當中的這段代碼即可欧募。每次scroll到底部都會執(zhí)行這段相同的代碼并顯示到界面上压状。這樣把精力簡單的集中在這段代碼塊就可以了。
Rx中的函數(shù)式
來看下這些通常帶有預先設計的跟继,偉大的observables類
我們之前的三個例子中种冬,每個里面都涉及到一個observable類。
例如text field中的text就轉(zhuǎn)化為Observable<String>舔糖。只要把text封裝到成一個observable娱两,所有關于text的變化都通過這個observable string像信號源一樣來發(fā)射出來。而table view controller的那個例子中金吗,數(shù)據(jù)源變成了一個Observable<Array<...>>十兢,一個包含string的array趣竣。在scrolling的例子中,并沒有數(shù)據(jù)纪挎,那就封裝成一個Observable<Void>,因為我們感興趣的只是用戶觸及scrollview底部的事件本身跟匆。
在這些情況中异袄,你主要操作的都是同一個類,不管里面包含什么數(shù)據(jù)源玛臂,array也好烤蜕,string也好,或者其他的什么也好迹冤,他們都是observables讽营,你只需要關注你對observables的操作,比如行為定義這些泡徙。這種對數(shù)據(jù)源的observable封裝類橱鹏,你可以在不同項目中通過復制粘貼來復用,或者抽象成一個基礎框架堪藐,這樣在具體的項目中莉兰,就只需要集中處理具體的數(shù)據(jù)類型了。
下面我們來看一個Rx入門的小例子:
有一個app礁竞,需要實現(xiàn)一個textfield糖荒,用戶輸入文字后能查詢到匹配的代碼倉庫。我們依然采用有偉大的有前途的observables.界面上是一個textfield模捂,它輸出一個Observable<String>捶朵。通過它,用戶每次輸入改變都能獲得一個新的String狂男;
observable的有個“filter”函數(shù)能幫我們忽略掉不符合條件的數(shù)據(jù)综看,比如,兩個字母就不要搜了岖食,搜出來的結(jié)果也沒什么關聯(lián)的寓搬。而且特別贊的一點是,filter這個函數(shù)返回值也仍然是個observable實例县耽。這意味著在結(jié)果上仍然可以執(zhí)行對應的函數(shù)句喷,分分鐘搞出鏈式語法。接下來我調(diào)用debounce函數(shù)兔毙。
debounce是用來干嘛的呢唾琼?它用來過濾太密集的一些事件,只取最后一個.比如用戶輸入的時候不需要每個字母敲進去都發(fā)送一個事件澎剥,只要稍等一下锡溯,取最后一個事件就行了。
下一個要介紹的是map函數(shù),這個函數(shù)---函如其名祭饭,把輸入數(shù)據(jù)映射成輸出數(shù)據(jù)的樣子芜茵。比如這個例子中,輸入一個string倡蝙,對應就得生成一個URL九串,那么從string到NSURLRequest就有一次map。經(jīng)過上述的函數(shù)鏈條寺鸥,每一次輸入都會引發(fā)一次輸出猪钮。
FlatMap函數(shù)會允許我們創(chuàng)建一個網(wǎng)絡請求,并等待服務器結(jié)果返回胆建。返回是一個NSData類型烤低,進行一次map函數(shù)處理,利用NSJSONSerialization轉(zhuǎn)化成Array<AnyObject>笆载。
如圖14所述是這個app整個的工作流扑馁。從textfield的鍵盤輸入,到數(shù)據(jù)校驗凉驻,到網(wǎng)絡請求檐蚜,再到數(shù)據(jù)轉(zhuǎn)化,最后得到一個代碼倉庫的列表沿侈,可能以Realm的方式存儲下來闯第。這個流程非常優(yōu)秀,因為它是線性的缀拭,很容易就知道一個步驟接下來的下一個步驟咳短,也不需要去管數(shù)據(jù)的protocol或者delegate那些看起來很分散的東西。所有的流程是序列化的蛛淋,一個接一個的發(fā)生咙好。
下面這個代碼從storyboard引出textfield的outlet,然后引入Rx框架褐荷,實現(xiàn)一下這段代碼
query.rx_text
.filter {string in
return string.characters.count > 3
}
.debounce(0.51, scheduler: MainScheduler.instance)
.map {string in
let apiURL = NSURL(string: "https://api.github.com/q?=" + string)!
return NSURLRequest(URL: apiURL)
}
.flatMapLatest { request in
return NSURLSession.sharedSession().rx_data(request)
}
.map { data —> Array<AnyObject> in
let json = try NSJSONSerialization.JSONObjectWithData(data, options: [])
return json as! Array<AnyObject>
}
.map {object in
return Repo(object: object)
}
.bindTo(tableView.rx_itemsWithCellIdentifier("Cell"))
我們調(diào)用filter函數(shù)勾效,和filter函數(shù)中實現(xiàn)的函數(shù)體,然后調(diào)用debounce叛甫,設置有效事件的時間間隔层宫,然后調(diào)用map,把string映射到URL然后是URLRequest其监,然后調(diào)用了FlatMap函數(shù)萌腿,把其中每個元素都調(diào)起一個URLSession來發(fā)起網(wǎng)絡請求,數(shù)據(jù)從服務器返回來后抖苦,再發(fā)起一次map的調(diào)用通過NSJASONSerialization來處理NSData毁菱,最終轉(zhuǎn)換成repo對象米死。在鏈條的最末端調(diào)用的bindTo可以把輸出的repo數(shù)據(jù)列表綁定到tableview中,直接調(diào)用了tableView的CellIdentifier函數(shù)就可以完成綁定贮庞。
看起來這些代碼又簡單又短并不是啥代碼峦筒,但是可以引發(fā)一些好的思考。你花了15分鐘體驗了一下Rx思維窗慎。這些簡單的代碼告訴你用Rx你可以干的事情物喷。這些代碼段都是線性完成的,一看就知道一塊代碼接下去的下一段代碼是要干啥捉邢。團隊的新成員很容易就讀懂了代碼邏輯脯丝,這種線性方式也能讓你很容易就看懂6個月前的代碼商膊。
最好的地方是我不是通過一個對象和另外一個對象的關系來調(diào)用代碼伏伐,而是鏈式的調(diào)用。鏈條中每一段代碼塊都有輸入輸出晕拆,也依賴上游的輸出藐翎,影響下游的輸入。上下游之間是緊密相連的实幕。一旦編譯成功吝镣,整個鏈條就不接受任何改變從而保證代碼過程順利的運行。
函數(shù)響應式App框架
這個話題跟我的iOS程序有多大關系呢昆庇。
我們來看看一個更復雜點的需要彈出新ViewController的情況末贾。在app中有一個NavtigationController,其中有個包含repos列表的tableview整吆,一個添加新repo的模態(tài)方式的viewcontroller拱撵。用戶通過鍵盤添加信息,點擊“Done”按鈕表蝙,然后數(shù)據(jù)需要發(fā)生變化拴测。平常,我們的解決辦法是實現(xiàn)一個delegate,并在協(xié)議里定義viewcontroller之間的交互和調(diào)用方法,這顯然是有點麻煩的「撸現(xiàn)在來試試新的解決方案集索。
我們有個全局的類,通過它我們可以跟任何類進行交互汇跨。這個類就是我們的observable务荆。比如在Add Repo的viewcontroller中可以包含一個observable屬性。每次用戶點擊done之后就發(fā)射出新加入的數(shù)據(jù)穷遂,那么事情會變得非常簡單蛹含。
下面這段源代碼從點擊右上角的“+”按鈕開始。在Rx的框架中塞颁,tap會返回來一個observable浦箱,每次用戶點擊+按鈕都會產(chǎn)生一次動作吸耿。
addBarItem.rx_tap
.debounce(0.5, scheduler: MainScheduler.instance)
.flatMapFirst {[weak self] _ —> Observable<Repo> in
let addVC = AddRepoViewController()
self?.presentViewController(addVC, animated: true, completion: nil)
return addVC.newRepo.asObservable()
}
.doOn {_ in
self.dismissViewControllerAnimated(true, completion: nil)
}
.subscribeNext {repo in
repos.value.append(repo)
}
repos.asObservable()
.bindTo(tableView.rx_itemsWithCellIdentifier("Cell"))
這里我又用到了debounce函數(shù),用來避免按鈕被多次點擊酷窥。如果用戶手欠點“+”點的特別快咽安,那么可能打開多個viewcontroller,rx框架里用debounce函數(shù)就避免了這種情況蓬推,只會打開一次妆棒。
來看看FlatMap在里面干了什么,之前的例子當中沸伏,我們用它來做了一些耗時的處理工作糕珊。這也是可以應用到這里的,present一個viewcontroller然后等待直到它被關閉毅糟,通過暴露出一個observable屬性红选,來返回一個新的repo。然后再關閉這個viewctroller姆另。在鏈條的最后對返回的數(shù)據(jù)進行處理喇肋。在這個例子中,新生成的repo返回后需要觸發(fā)前一個列表窗口的數(shù)據(jù)更新迹辐。所以這里蝶防,我們把repos列表綁定到了tableview中。
這樣的MVVM的模式相當快速明吩,只要生成一些viewcontrollers间学,暴露一些models來驅(qū)動他們的數(shù)據(jù),這就是你主要引入Rx代碼的地方印荔,通過數(shù)據(jù)驅(qū)動低葫,在這個鏈條的最后可以吧數(shù)據(jù)渲染到UI上。這個過程串起來相當清晰的躏鱼。
業(yè)務邏輯和界面的代碼分離的很清楚氮采。所有的邏輯代碼都在view model中。所以可以在這一層寫case做測試染苛。也不需要涉及到viewcontroller的初始化這些東東鹊漠。這樣的模式里,模塊的邊界變得顯而易見茶行。
RxSwift
RxSwift是一個長著同步臉的異步框架
它有函數(shù)式編程的一面躯概,用來處理異步事件,比如各種轉(zhuǎn)換還有別的東東畔师。也引入了很好的架構(gòu)模式娶靡,其實跟iOS的開發(fā)關聯(lián)可以非常緊密的。
進一步的資料可參考
ReactiveX.io
Rx有多種平臺多種語言的實現(xiàn)看锉,在這個網(wǎng)站上你可以找到一些官方的API說明姿锭,包含Swift塔鳍,Java,JS和Skala版本哦
RXSwift.org,rx-marin.com
這里有我寫的一些關于Rx的東東呻此,可以看看轮纫,加深理解
最后,非常感謝鼓勵我的Ash Furrow焚鲜,幫助我理解基礎要點的Jens Ravens掌唾,早期幫我改代碼的Florent Pillet,跟我一起玩Rx的朋友忿磅,Junior Bontognali糯彬,以及發(fā)起RxSwift項目的牛人Krunoslav Zaher