RxSwift和RxCocoa入門

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文主要來自"},{"type":"link","attrs":{"href":"https://www.raywenderlich.com/1228891-getting-started-with-rxswift-and-rxcocoa","title":""},"content":[{"type":"text","text":"Getting Started With RxSwift and RxCocoa"}]},{"type":"text","text":"這篇文章"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"命令式編程 vs 響應式編程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"目前大部分面嚮對象語言都是命令式的編程範式,即通過代碼下達命令給系統;雖然可以通過各種技術手段知道數據的變化,但是不能做到自動化地通知。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"響應式編程則是想讓數據的變化可以自動地通知,你不再需要關心特定狀態,這個狀態通知到響應的過程,響應式編程(庫)已經幫你處理了"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"維基百科上有一個更明確的例子:"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於命令式編程,當a = b + c, a由b+c計算得到;但是之後,b和c發生了變化,這個變化對a並不生效,a還是保持原有的值;而當使用響應式編程時,當b和c發生變化,程序不需要再次執行a = b + c公式,a的值就會自動更新。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"響應式編程庫現狀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前分爲兩派"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ReactiveX主導的rx系列庫,語言支持如RxSwift, RxKotlin, 平臺支持如RxAndroid, RxCocoa"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ReactiveCocoa(GitHub發起)庫,語言僅支持swift,objective-c,平臺僅支持iOS"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相同點:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"都是Reactive Functional Programming(響應式函數編程)的實現"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"二者github star數不分伯仲,均接近20k級別"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"都有很好的社區支持"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不同點:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Hot、Code Signal實現API不同:RAC設計了兩個api分別對應hot、code signal,RxSwift僅有一個"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"錯誤處理,RAC相對容易一些"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"UI Bindings,RAC有不少歷史包袱,RxSwift則更容易使用"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rx系列支持更多語言和平臺"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於這兩個庫的詳細對比,見:"},{"type":"link","attrs":{"href":"https://www.raywenderlich.com/1190-reactivecocoa-vs-rxswift","title":""},"content":[{"type":"text","text":"ReactiveCocoa vs RxSwift"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"RxSwift和RxCocoa"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RxSwift: 響應式編程在Swift語言領域的實現庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RxCocoa: 針對Cocoa平臺(iOS & Mac OS)的響應式編程庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Observables and Observers"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個"},{"type":"text","marks":[{"type":"italic"},{"type":"strong"}],"text":"Observable"},{"type":"text","text":"發送變化通知"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個"},{"type":"text","marks":[{"type":"italic"},{"type":"strong"}],"text":"Observer"},{"type":"text","text":"訂閱一個Observable,當Observable有變化時會被通知"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多個Observer可以監聽一個Observable,當發生變化時,所有Observer都會被通知"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"DisposeBag"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"DisposeBag是RxSwift提供的處理ARC和內存管理的工具。銷燬一個父對象時,會使得DisposeBag中的Observer對象同時銷燬。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當持有DisposeBag的對象的"},{"type":"codeinline","content":[{"type":"text","text":"deinit()"}]},{"type":"text","text":"調用時,每個disposable Observer都會取消對監聽對象的訂閱,這時ARC就可以正常回收內存了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果沒有DisposeBag的話,可能會出現兩種情況,要麼是observer會保留下來不被銷燬,繼續監聽;要麼被釋放,造成崩潰。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"建立Observer對象時,記得同時添加到DisposeBag中,來讓DisposeBag幫助你回收該對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"開始吧"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先下載"},{"type":"link","attrs":{"href":"https://koenig-media.raywenderlich.com/uploads/2019/04/Materials_rxswift5.zip","title":""},"content":[{"type":"text","text":"示例工程"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"打開編譯後即可看到如下界面"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2a/2a092c225915e88d0e4c5e5d9a56fa96.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個例子功能很簡單:選擇巧克力後,點擊右上角可以結賬,然後進行模擬支付。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"非reactive的實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"text","marks":[{"type":"strong"}],"text":"ChocolatesOfTheWorldViewController.swift"},{"type":"text","text":"中可以看到實現"},{"type":"codeinline","content":[{"type":"text","text":"UITableViewDelegate"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"UITableViewDataSource"}]},{"type":"text","text":"的extension。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在觀察一下"},{"type":"codeinline","content":[{"type":"text","text":"updateCartButton()"}]},{"type":"text","text":"這個方法,它用來更新購物車中的巧克力數量,在下面兩種情況時調用:"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"viewWillAppear(_:)"},{"type":"text","text":":在view controller顯示之前"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"tableView(_:didSelectRowAt:)"},{"type":"text","text":":當點擊列表,添加巧克力到購物車時"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這就是命令式編程方式實現:你必須手動調用方法來更新購物車巧克力數量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"使用RxSwift改寫購物車商品數量"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"購物車商品信息保存在"},{"type":"codeinline","content":[{"type":"text","text":"ShoppingCart.sharedCart"}]},{"type":"text","text":"這個單例中"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定義在ShoppingCart.sharedCart中:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"var chocolates: [Chocolate] = []"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然可以給其定義中添加一個"},{"type":"codeinline","content":[{"type":"text","text":"didSet"}]},{"type":"text","text":"閉包,但問題是,這種做法只能在整個數組更新時才能被通知,而不是數組中任意元素變化就可以得到通知。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對這種情況,RxSwift提供瞭解決方案,按照如下方式創建"},{"type":"codeinline","content":[{"type":"text","text":"chocolates"}]},{"type":"text","text":"變量:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"let chocolates: BehaviorRelay = BehaviorRelay(value: [])"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用RxSwift的"},{"type":"codeinline","content":[{"type":"text","text":"BehaviorRelay"}]},{"type":"text","text":"對象,持有一個Chocolate數組類型的值。這麼做的目的是:通過"},{"type":"codeinline","content":[{"type":"text","text":"BehaviorRelay"}]},{"type":"text","text":"對象的"},{"type":"codeinline","content":[{"type":"text","text":"asObservable()"}]},{"type":"text","text":"可以得到一個observable,這樣我們就可以添加監聽者來訂閱"},{"type":"codeinline","content":[{"type":"text","text":"BehaviorRelay"}]},{"type":"text","text":"對象的"},{"type":"codeinline","content":[{"type":"text","text":"value"}]},{"type":"text","text":"(chocolate數組)的變化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述做法的缺點是,對chocolate數組的修改必須修改爲使用"},{"type":"codeinline","content":[{"type":"text","text":"accept(_:)"}]},{"type":"text","text":",這是"},{"type":"codeinline","content":[{"type":"text","text":"BehaviorRelay"}]},{"type":"text","text":"爲修改"},{"type":"codeinline","content":[{"type":"text","text":"value"}]},{"type":"text","text":"屬性提供的方法。由於對數據的訪問方式發生了變化,代碼中相應的地方也要做修改。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"text","marks":[{"type":"strong"}],"text":"ShoppingCart.swift"},{"type":"text","text":"中"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"totalCost()"}]},{"type":"text","text":"方法的修改:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"return chocolates.reduce(0) {\n// 修改爲\nreturn chocolates.value.reduce(0) {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"itemCountString()"}]},{"type":"text","text":"方法的修改:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"guard chocolates.count > 0 else {\n// 修改爲\nguard chocolates.value.count > 0 else {\n\nlet setOfChocolates = Set(chocolates)\n// 修改爲\nlet setOfChocolates = Set(chocolates.value)\n\nlet count: Int = chocolates.reduce(0) {\n// 修改爲\nlet count: Int = chocolates.value.reduce(0) {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"text","marks":[{"type":"strong"}],"text":"CartViewController.swift"},{"type":"text","text":"中"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"reset()"}]},{"type":"text","text":"方法的修改:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"ShoppingCart.sharedCart.chocolates = []\n// 修改爲\nShoppingCart.sharedCart.chocolates.accept([])"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"text","marks":[{"type":"strong"}],"text":"ChocolatesOfTheWorldViewController.swift"},{"type":"text","text":"中"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"updateCartButton()"}]},{"type":"text","text":"方法的修改:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"// 修改爲\ncartButton.title = \"\\(ShoppingCart.sharedCart.chocolates.value.count) \\u{1f36b}\""}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"tableView(_:didSelectRowAt:)"}]},{"type":"text","text":"方法的修改:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"ShoppingCart.sharedCart.chocolates.append(chocolate)\n// 修改爲\nlet newValue = ShoppingCart.sharedCart.chocolates.value + [chocolate]\nShoppingCart.sharedCart.chocolates.accept(newValue)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完成上述修改後,我們就可以來對"},{"type":"codeinline","content":[{"type":"text","text":"chocolates"}]},{"type":"text","text":"添加observer了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在ChocolatesOfTheWorldViewController.swift中,新增:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"private let disposeBag = DisposeBag()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"codeinline","content":[{"type":"text","text":"//MARK: Rx Setup"}]},{"type":"text","text":"的extension中添加:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"func setupCartObserver() {\n //1\n ShoppingCart.sharedCart.chocolates.asObservable()\n .subscribe(onNext: { //2\n [unowned self] chocolates in\n self.cartButton.title = \"\\(chocolates.count) \\u{1f36b}\"\n })\n .disposed(by: disposeBag) //3\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述代碼即可實現對購物車的自動更新。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,RxSwift大量使用函數鏈,就是說每一個函數接收前一個函數的結果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對上述代碼的解釋:"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"從購物車中"},{"type":"codeinline","content":[{"type":"text","text":"chocolates"}]},{"type":"text","text":"得到一個"},{"type":"codeinline","content":[{"type":"text","text":"Observable"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"對"},{"type":"codeinline","content":[{"type":"text","text":"Observable"}]},{"type":"text","text":"調用subscribe(onNext:)來監聽其值變化。"},{"type":"codeinline","content":[{"type":"text","text":"subscribe(onNext:)"}]},{"type":"text","text":"接收的閉包在每次值變化時都會被執行。閉包中的入參就是"},{"type":"codeinline","content":[{"type":"text","text":"Observable"}]},{"type":"text","text":"變化後的最新值。除非你取消訂閱,或者dispose訂閱,你會一直收到變化通知。這個方法的返回的一個"},{"type":"codeinline","content":[{"type":"text","text":"Disposable"}]},{"type":"text","text":"(也是一個"},{"type":"codeinline","content":[{"type":"text","text":"Observer"}]},{"type":"text","text":")。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"將上一步返回的"},{"type":"codeinline","content":[{"type":"text","text":"Observer"}]},{"type":"text","text":"加入到我們的定義"},{"type":"codeinline","content":[{"type":"text","text":"disposeBag"}]},{"type":"text","text":"中。這會讓被訂閱對象被銷燬時,訂閱者也被處理。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"最後,刪除"},{"type":"codeinline","content":[{"type":"text","text":"updateCartButton()"}]},{"type":"text","text":"方法的調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"然後執行代碼,你會看到巧克力列表:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/10/10f2c45406ff73ede3f55e994e6dad47.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但此時,點擊單個巧克力,購物車位置一直顯示的是“Item”。這是因爲"},{"type":"codeinline","content":[{"type":"text","text":"setupCartObserver()"}]},{"type":"text","text":"沒有被調用,導致"},{"type":"codeinline","content":[{"type":"text","text":"Observer"}]},{"type":"text","text":"並沒有建立起來。在"},{"type":"text","marks":[{"type":"strong"}],"text":"ChocolatesOfTheWorldViewController.swift"},{"type":"text","text":"的"},{"type":"codeinline","content":[{"type":"text","text":"viewDidLoad()"}]},{"type":"text","text":"方法最後調用該它。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再次編譯運行,就會看到,當點擊巧克力時,購物車自動更新了。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/56/5671880ee158663d3e7bcfca1d6b7622.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"使用RxCocoa對TableView進行Reactive改造"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RxCocoa爲原生UI組件添加了響應式API。這本例中,使用TableView的響應式API,可以不再用自己實現"},{"type":"codeinline","content":[{"type":"text","text":"UITableViewDataSource"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"UITableViewDelegate"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現步驟:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一步:代碼中刪掉data source和delegate相關代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二步是將table view使用到的數據從數組,改爲一個Observable:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"let europeanChocolates = Observable.just(Chocolate.ofEurope)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"just(_:)"}]},{"type":"text","text":"表明"},{"type":"codeinline","content":[{"type":"text","text":"Observable"}]},{"type":"text","text":"持有的值(value)是不變的。"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注:對於不變化的值,是沒有必要使用響應式編程的。所以在實際應用中,要避免拿着錘子看什麼都是釘子,要實際分析一下你是否真的需要使用Rx。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第三步在"},{"type":"codeinline","content":[{"type":"text","text":"//MARK: - Rx Setup"}]},{"type":"text","text":"添加如下代碼:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"func setupCellConfiguration() {\n //1\n europeanChocolates\n .bind(to: tableView\n .rx //2\n .items(cellIdentifier: ChocolateCell.Identifier,\n cellType: ChocolateCell.self)) { //3\n row, chocolate, cell in\n cell.configureWithChocolate(chocolate: chocolate) //4\n }\n .disposed(by: disposeBag) //5\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼釋義:"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"調用"},{"type":"codeinline","content":[{"type":"text","text":"bind(to:)"}]},{"type":"text","text":"將"},{"type":"codeinline","content":[{"type":"text","text":"europeanChocolates"}]},{"type":"text","text":"這個Observable關聯到table view每行所要執行的代碼上"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"調用rx後,讓你可以訪問table view的Rx擴展"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"代用Rx方法"},{"type":"codeinline","content":[{"type":"text","text":"items(cellIdentifier:cellType:)"}]},{"type":"text","text":"傳遞相關參數。這樣Rx框架根據這些信息實現一個wrapper data source,再設置給table view。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"當拿到了新的cell item時,通過這個block來進行設置"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"將bind方法結果加入到disposeBag"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"再在viewDidLoad中調用下上面的setupCellConfiguration()方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"這時運行程序,就會再次看到巧克力列表數據。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2a/2a092c225915e88d0e4c5e5d9a56fa96.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第四步來添加事件處理:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在//MARK: - Rx Setup位置添加如下代碼"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"func setupCellTapHandling() {\n tableView\n .rx\n .modelSelected(Chocolate.self) //1\n .subscribe(onNext: { [unowned self] chocolate in // 2\n let newValue = ShoppingCart.sharedCart.chocolates.value + [chocolate]\n ShoppingCart.sharedCart.chocolates.accept(newValue) //3\n \n if let selectedRowIndexPath = self.tableView.indexPathForSelectedRow {\n self.tableView.deselectRow(at: selectedRowIndexPath, animated: true)\n } //4\n })\n .disposed(by: disposeBag) //5\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼釋義:"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"modelSelected(_:)"}]},{"type":"text","text":"傳入Chocolate model類型,得到Observable對象"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"subscribe(onNext:)"}]},{"type":"text","text":"是給Observable對象註冊事件處理,這個閉包代碼會在一個model被選中時調用"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"閉包中首先將選中的chocolate加入到購物車"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"反選table row"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"將"},{"type":"codeinline","content":[{"type":"text","text":"subscribe(onNext:)"}]},{"type":"text","text":"返回的"},{"type":"codeinline","content":[{"type":"text","text":"Disposable"}]},{"type":"text","text":"加入到"},{"type":"codeinline","content":[{"type":"text","text":"disposeBag"}]}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"最後在viewDidLoad中調用"},{"type":"codeinline","content":[{"type":"text","text":"setupCellTapHandling()"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"然後運行,就可以看到經過Rx API重寫過的樣例功能,和之前一樣:點擊列表中的巧克力,將他們添加到購物車中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"使用RxSwift處理用戶輸入(Direct Text Input)"}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"RxSwift也可以用來處理用戶輸入和表單驗證。"}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"非Reactive的做法是,實現"},{"type":"codeinline","content":[{"type":"text","text":"UITextFieldDelegate"}]},{"type":"text","text":",然後實現很多"},{"type":"codeinline","content":[{"type":"text","text":"if/else"}]},{"type":"text","text":"來分別處理輸入及其對應的動作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"Reactive Programming的做法則是更直接地將輸入處理動作和邏輯綁定到input field上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以通過下面這個例子來具體體會一下。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先在"},{"type":"text","marks":[{"type":"italic"}],"text":"BillingInfoViewController.swift"},{"type":"text","text":"中創建一個DisposeBag:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"private let disposeBag = DisposeBag()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後在"},{"type":"codeinline","content":[{"type":"text","text":"//MARK: - Rx Setup"}]},{"type":"text","text":"評論下的extension中添加如下代碼:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"func setupCardImageDisplay() {\n cardType\n .asObservable() //1\n .subscribe(onNext: { [unowned self] cardType in\n self.creditCardImageView.image = cardType.image\n }) //2\n .disposed(by: disposeBag) //3\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼釋義:"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"給"},{"type":"codeinline","content":[{"type":"text","text":"BehaviorRelay"}]},{"type":"text","text":"的值添加一個"},{"type":"codeinline","content":[{"type":"text","text":"Observable"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"訂閱這個"},{"type":"codeinline","content":[{"type":"text","text":"Observable"}]},{"type":"text","text":"來獲得"},{"type":"codeinline","content":[{"type":"text","text":"cardType"}]},{"type":"text","text":"的變化"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"將Observer的銷燬交給"},{"type":"codeinline","content":[{"type":"text","text":"disposeBag"}]}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"上述代碼就實現了一個Reactive的例子:當cardType變化時,creditCardImageView的圖片會變成對應type定義的圖片。接下來實現對文字變化的處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"考慮到用戶輸入可能會很快,如果每次輸入都執行驗證代碼可能會導致UI卡頓。所以我們需要實現一個throttle來控制,在throttle的時間間隔後纔會再次驗證輸入,這樣就可以避免對UI的阻塞。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RxSwift是支持Throttling的,因爲有不少場景需要控制對變化的響應頻次。下面我們來看看如何實現。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,在BillingInfoViewController中定義一個常量來表示throttle的間隔毫秒時間:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"private let throttleIntervalInMilliseconds = 100"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後,在"},{"type":"codeinline","content":[{"type":"text","text":"RX Setup"}]},{"type":"text","text":" extension中加入下面的代碼:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"func setupTextChangeHandling() {\n let creditCardValid = creditCardNumberTextField\n .rx\n .text //1\n .observeOn(MainScheduler.asyncInstance)\n .distinctUntilChanged()\n .throttle(.milliseconds(throttleIntervalInMilliseconds), scheduler: MainScheduler.instance) //2\n .map { [unowned self] in\n self.validate(cardText: $0) //3\n }\n \n creditCardValid\n .subscribe(onNext: { [unowned self] in\n self.creditCardNumberTextField.valid = $0 //4\n })\n .disposed(by: disposeBag) //5\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼釋義:"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"text是RxCocoa的擴展,它從一個UITextField獲得其value的一個Observable"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"基於之前定義的throttle時間間隔,創建一個throttle;scheduler目前先綁定到主線程"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"將結果交由給"},{"type":"codeinline","content":[{"type":"text","text":"validate(cardText:)"}]},{"type":"text","text":",它可以將輸入轉換爲creditCardValid的值,如果輸入有效,creditCardValid的值會是true"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"監聽creditCardValid,根據其結果來改變text field的狀態"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"銷燬事宜"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"實現CVV的校驗和上述思路一樣。"}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"先在"},{"type":"codeinline","content":[{"type":"text","text":"setupTextChangeHandling()"}]},{"type":"text","text":"底部添加如下代碼:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"let expirationValid = expirationDateTextField\n .rx\n .text\n .observeOn(MainScheduler.asyncInstance)\n .distinctUntilChanged()\n .throttle(.milliseconds(throttleIntervalInMilliseconds), scheduler: MainScheduler.instance)\n .map { [unowned self] in\n self.validate(expirationDateText: $0)\n}\n \nexpirationValid\n .subscribe(onNext: { [unowned self] in\n self.expirationDateTextField.valid = $0\n })\n .disposed(by: disposeBag)\n \nlet cvvValid = cvvTextField\n .rx\n .text\n .observeOn(MainScheduler.asyncInstance)\n .distinctUntilChanged()\n .map { [unowned self] in\n self.validate(cvvText: $0)\n}\n \ncvvValid\n .subscribe(onNext: { [unowned self] in\n self.cvvTextField.valid = $0\n })\n .disposed(by: disposeBag)"}]},{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"最後,將三個text field結合起來做驗證,在"},{"type":"codeinline","content":[{"type":"text","text":"setupTextChangeHandling()"}]},{"type":"text","text":"加入以下代碼:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"let everythingValid = Observable\n .combineLatest(creditCardValid, expirationValid, cvvValid) {\n $0 && $1 && $2 //All must be true\n}\n \neverythingValid\n .bind(to: purchaseButton.rx.isEnabled)\n .disposed(by: disposeBag)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"combineLatest(_:)將三個observable結合到一起再生成一個observable,everythingValid的值會是true或false。接着再講everythingValid關聯到purchaseButton的rx擴展屬性isEnabled,以此來實現對該button是否可用狀態的控制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,在viewDidLoad中添加如下代碼:"}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"setupCardImageDisplay()\nsetupTextChangeHandling()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"運行代碼,然後選擇巧克力後,進入購物車:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/39/39a791d7d5377fccaade7c3ad5f3d998.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後點擊checkout,進行支付界面:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f9/f96c9d0629033d01a19a522aa7a7dcf2.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏可以看到,輸入4,後面圖標就顯示了visa的圖標。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接着,輸入合法的CVV和有效期限,Buy Chocolate按鈕就是可用狀態了:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/fd/fdcc447cec1297fef08c40a6505ef047.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,我們就實現了一個簡單的對用戶輸入的reactive方式的校驗實現了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章