原文:https://realm.io/news/tryswift-Marin-Todorov-I-create-iOS-apps-is-RxSwift-for-me/
在本次try! Swift NYC talk活動中,Marin Todorov介紹了RxSwift在一個iOS開發人員日常工作中的使用案例(RxSwift是一個異步,基于事件處理的框架)。如果你希望以引入一個庫的成本來解決你大半部分的痛苦的話,那么這篇文章對你最合適不過。
簡單介紹
Reactive Extensions,簡稱Rx,大量采用了observable序列和link style操作運算,是用來解決事件和異步處理的庫,使用這個庫,開發者處理異步數據流不要太爽。
Marin表示說,我讀了Rx理論很多次,但是我也沒法搞清楚iOS開發日常中如何去使用它,我踩過一些坑,今天就想介紹一下我用Rx解決過的一些實際問題。
Rx響應式的應用
首先來看看Rx響應式的方面。使用Rx的響應式,數據改變后會直接push到你 而不需要你再次主動pull數據。
Array < String >
這個array,作為一個strings的集合類,有時候處理起來有點小麻煩,你需要去遍歷它其中每個子元素,根據子元素的情況去處理數據,即便采用更Swifty的方法,比如forEach這樣的,也還是挺麻煩,因為本質都還一樣,你需要提供一個閉包,一些block的代碼,得一個元素接一個元素的處理。
問題在于,你處理這個array的時候,是在一個固定的時間點,而這個時間點的數據也是固定的。而有新數據加到這個array中的時候,你是不知道的。
拿table view controller來舉例。它綁定一個含有三個元素的array,用戶點擊增加的按鈕,array里數據變成四個了,但是界面上還是三個。你或許可以采用通知或者委托的方式來同步數據model和UI這樣。但是如果添加數據恰好是個異步耗時的過程,那就有點麻煩了。
Observable < Array < String > >
Rx把數據封裝成一個observable類,這樣一個類,可以讓你本來的數據有時間的跨度,簡單的說,是給數據加上一個"時間的維度"
在這個例子中,observable序列定義了每個元素需要做的處理方法,例如:print每個元素。在初始化的時候,每個元素會執行一遍這個方法。當有新元素加入的時候,會再執行一遍這個方法,以前你需要不僅要定義管做什么事情,還要管什么時間做,現在你只需要這些元素各自的處理方法,然后一但數據有新的變化,會自動執行處理方法。
我覺得這就是Rx最牛逼的地方,把需要異步處理變成線性的,這樣就把事情簡化了。你都不需要考慮你現在有啥數據,過去有啥數據,將來可能都有啥情況,而只需要定義數據驅動的處理方法塊就可以了。在上面的例子中,你如果想在tableview里顯示一個list,就只需要線性的碼好從數據到UI的渲染代碼。把list中元素加加減減,數據變化驅動的錯綜復雜事件和交互都交給RxSwift打點就好。
Observables特別好用,再舉個例子,你在text filed輸入的字符串需要在某個label中顯示,你只要實現數據如何顯示到label的代碼就行了,其他用戶輸入事件的激活等其他環節就交給RxSwift。
有了RxSwift,事情變得觸手可及,簡單,且線性
再考慮一個復雜點的情況,scrollview中的翻頁加載數據問題,如果啟用了RxSwift,你只需要實現的翻頁代碼就比較簡單了:只需要實現從服務器加載20條數據并加入到list當中的這段代碼即可。每次scroll到底部都會執行這段相同的代碼并顯示到界面上。這樣把精力簡單的集中在這段代碼塊就可以了。
Rx中的函數式
來看下這些通常帶有預先設計的,偉大的observables類
我們之前的三個例子中,每個里面都涉及到一個observable類。
例如text field中的text就轉化為Observable<String>。只要把text封裝到成一個observable,所有關于text的變化都通過這個observable string像信號源一樣來發射出來。而table view controller的那個例子中,數據源變成了一個Observable<Array<...>>,一個包含string的array。在scrolling的例子中,并沒有數據,那就封裝成一個Observable<Void>,因為我們感興趣的只是用戶觸及scrollview底部的事件本身。
在這些情況中,你主要操作的都是同一個類,不管里面包含什么數據源,array也好,string也好,或者其他的什么也好,他們都是observables,你只需要關注你對observables的操作,比如行為定義這些。這種對數據源的observable封裝類,你可以在不同項目中通過復制粘貼來復用,或者抽象成一個基礎框架,這樣在具體的項目中,就只需要集中處理具體的數據類型了。
下面我們來看一個Rx入門的小例子:
有一個app,需要實現一個textfield,用戶輸入文字后能查詢到匹配的代碼倉庫。我們依然采用有偉大的有前途的observables.界面上是一個textfield,它輸出一個Observable<String>。通過它,用戶每次輸入改變都能獲得一個新的String;
observable的有個“filter”函數能幫我們忽略掉不符合條件的數據,比如,兩個字母就不要搜了,搜出來的結果也沒什么關聯的。而且特別贊的一點是,filter這個函數返回值也仍然是個observable實例。這意味著在結果上仍然可以執行對應的函數,分分鐘搞出鏈式語法。接下來我調用debounce函數。
debounce是用來干嘛的呢?它用來過濾太密集的一些事件,只取最后一個.比如用戶輸入的時候不需要每個字母敲進去都發送一個事件,只要稍等一下,取最后一個事件就行了。
下一個要介紹的是map函數,這個函數---函如其名,把輸入數據映射成輸出數據的樣子。比如這個例子中,輸入一個string,對應就得生成一個URL,那么從string到NSURLRequest就有一次map。經過上述的函數鏈條,每一次輸入都會引發一次輸出。
FlatMap函數會允許我們創建一個網絡請求,并等待服務器結果返回。返回是一個NSData類型,進行一次map函數處理,利用NSJSONSerialization轉化成Array<AnyObject>。
如圖14所述是這個app整個的工作流。從textfield的鍵盤輸入,到數據校驗,到網絡請求,再到數據轉化,最后得到一個代碼倉庫的列表,可能以Realm的方式存儲下來。這個流程非常優秀,因為它是線性的,很容易就知道一個步驟接下來的下一個步驟,也不需要去管數據的protocol或者delegate那些看起來很分散的東西。所有的流程是序列化的,一個接一個的發生。
下面這個代碼從storyboard引出textfield的outlet,然后引入Rx框架,實現一下這段代碼
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"))
我們調用filter函數,和filter函數中實現的函數體,然后調用debounce,設置有效事件的時間間隔,然后調用map,把string映射到URL然后是URLRequest,然后調用了FlatMap函數,把其中每個元素都調起一個URLSession來發起網絡請求,數據從服務器返回來后,再發起一次map的調用通過NSJASONSerialization來處理NSData,最終轉換成repo對象。在鏈條的最末端調用的bindTo可以把輸出的repo數據列表綁定到tableview中,直接調用了tableView的CellIdentifier函數就可以完成綁定。
看起來這些代碼又簡單又短并不是啥代碼,但是可以引發一些好的思考。你花了15分鐘體驗了一下Rx思維。這些簡單的代碼告訴你用Rx你可以干的事情。這些代碼段都是線性完成的,一看就知道一塊代碼接下去的下一段代碼是要干啥。團隊的新成員很容易就讀懂了代碼邏輯,這種線性方式也能讓你很容易就看懂6個月前的代碼。
最好的地方是我不是通過一個對象和另外一個對象的關系來調用代碼,而是鏈式的調用。鏈條中每一段代碼塊都有輸入輸出,也依賴上游的輸出,影響下游的輸入。上下游之間是緊密相連的。一旦編譯成功,整個鏈條就不接受任何改變從而保證代碼過程順利的運行。
函數響應式App框架
這個話題跟我的iOS程序有多大關系呢。
我們來看看一個更復雜點的需要彈出新ViewController的情況。在app中有一個NavtigationController,其中有個包含repos列表的tableview,一個添加新repo的模態方式的viewcontroller。用戶通過鍵盤添加信息,點擊“Done”按鈕,然后數據需要發生變化。平常,我們的解決辦法是實現一個delegate,并在協議里定義viewcontroller之間的交互和調用方法,這顯然是有點麻煩的。現在來試試新的解決方案。
我們有個全局的類,通過它我們可以跟任何類進行交互。這個類就是我們的observable。比如在Add Repo的viewcontroller中可以包含一個observable屬性。每次用戶點擊done之后就發射出新加入的數據,那么事情會變得非常簡單。
下面這段源代碼從點擊右上角的“+”按鈕開始。在Rx的框架中,tap會返回來一個observable,每次用戶點擊+按鈕都會產生一次動作。
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函數,用來避免按鈕被多次點擊。如果用戶手欠點“+”點的特別快,那么可能打開多個viewcontroller,rx框架里用debounce函數就避免了這種情況,只會打開一次。
來看看FlatMap在里面干了什么,之前的例子當中,我們用它來做了一些耗時的處理工作。這也是可以應用到這里的,present一個viewcontroller然后等待直到它被關閉,通過暴露出一個observable屬性,來返回一個新的repo。然后再關閉這個viewctroller。在鏈條的最后對返回的數據進行處理。在這個例子中,新生成的repo返回后需要觸發前一個列表窗口的數據更新。所以這里,我們把repos列表綁定到了tableview中。
這樣的MVVM的模式相當快速,只要生成一些viewcontrollers,暴露一些models來驅動他們的數據,這就是你主要引入Rx代碼的地方,通過數據驅動,在這個鏈條的最后可以吧數據渲染到UI上。這個過程串起來相當清晰的。
業務邏輯和界面的代碼分離的很清楚。所有的邏輯代碼都在view model中。所以可以在這一層寫case做測試。也不需要涉及到viewcontroller的初始化這些東東。這樣的模式里,模塊的邊界變得顯而易見。
RxSwift
RxSwift是一個長著同步臉的異步框架
它有函數式編程的一面,用來處理異步事件,比如各種轉換還有別的東東。也引入了很好的架構模式,其實跟iOS的開發關聯可以非常緊密的。
進一步的資料可參考
ReactiveX.io
Rx有多種平臺多種語言的實現,在這個網站上你可以找到一些官方的API說明,包含Swift,Java,JS和Skala版本哦
RXSwift.org,rx-marin.com
這里有我寫的一些關于Rx的東東,可以看看,加深理解
最后,非常感謝鼓勵我的Ash Furrow,幫助我理解基礎要點的Jens Ravens,早期幫我改代碼的Florent Pillet,跟我一起玩Rx的朋友,Junior Bontognali,以及發起RxSwift項目的牛人Krunoslav Zaher