iOS開發之ReactiveCocoa下的MVVM(乾貨分享)
最近工作比較忙,但還是出來更新部落格了,今天給大家分享一些ReactiveCocoa以及MVVM的一些東西,幹活還是比較足的。在之前發表過一篇博文,名字叫做,大體上講的就是使用Block回撥的方式實現MVVM的。在寫上篇文章時也知道有ReactiveCocoa這個函式響應式程式設計的框架,並且有許多人用它來更好的實現MVVM。所以在上篇部落格發表後,有些同行給評論建議看一下ReactiveCocoa的東西,所以就係統的看了一下ReactiveCocoa的東西。不過有一點要說明的就是,不使用ReactiveCocoa是可以實現MVVM的,並非使用MVVM模式你就必須的使用ReactiveCocoa的東西,你可以使用KVO,Block,Delegate,Navigation等手段,而ReactiveCocoa更優雅的實現了這個過程。ReactiveCocoa就是一個響應式程式設計的框架,它會使MVVM每層之間互動起來更為方便,所以長和MVVM聯絡在一起。
一.函式響應式程式設計(Function Reactive Programming)
關於函式響應式程式設計的東西,我想引用國外這個ReactiveCocoa教學視訊(視訊連結)中的一張PPT來簡單的說一下什麼是函式響應式程式設計。那就直接上圖,下圖是上方視訊連結的截圖,很形象的解釋了什麼是函式響應式程式設計。簡單的說下方c = a + b 定義好後,當a的值變化後,c的值就會自動變化。不過a的值變化時會產生一個訊號,這個訊號會通知c根據a變化的值來變化自己的值。b的值變化同樣也影響c的值。下圖很好的表達了這個思想。在此就不做贅述了。
二. ReactiveCocoa簡介
先簡單的介紹一下什麼是ReactiveCocoa框架,然後通過例項好好的去搞一搞這個框架,最後就是如何在專案中使用了。關於ReactiveCocoa的理解一些部落格(見本篇部落格中的連結分享)中把ReactiveCocoa比喻成管道,ReactiveCocoa中的Signal就是管道中的水流。使用ReactiveCocoa可以方便的在MVVM各層之間架起溝通的管道,便於每層之間的互動。現在在我們做的工程中已經在使用ReactiveCocoa框架了,用起來的感覺是非常爽的,好用!
可以說ReactiveCocoa中核心是訊號量機制,Signal在ReactiveCocoa中發揮著強大的不可代替的作用,可謂是ReactiveCocoa的靈魂所。Signal是可以攜帶一些物件和引數的,你可以獲取該物件並且可以對該訊號量攜帶的值進行map, filter等常用操作,操作後的值會和該訊號量進行繫結。先簡單的這麼一說,後邊的部分回詳細的介紹如何讓訊號量發揮強大的作用。
ReactiveCocoa中對Block的使用可謂是淋漓盡致,如果對Block使用不熟的朋友可以補一下Block的東西,然後在回頭看一下ReactiveCocoa的東西。關於ReactiveCocoa更多的東西,請參考Github上的連結()。
三. 在工程中引入ReactiveCocoa
1.你可以使用Github上的加入方式如下所示,本人感覺比較麻煩,就沒有使用,採用的第二種方法(CocoaPods)。
2.上面的步驟難免有些麻煩,所以用CocoaPods更為便捷一些,Profile檔案中的內容如下所示,我用的是2.5版本。3.0後就支援Swift了,因為我沒有用Swift寫東西,所以就用的是2.5版本,設定完Profile檔案後,pod install即可。
你可以pod search ReactiveCocoa看一下版本,選擇你需要的版本即可。
四.使用ReactiveCocoa
下方會通過一些簡單的例項來介紹一下訊號量機制和一些常用的方法。
1.引入相應的標頭檔案
在工程中引入下方的標頭檔案(建議在Pch檔案中引入)就可以使用我們的ReactiveCocoa框架了
Objective-C12 | #import <ReactiveCocoa/ReactiveCocoa.h>#import <ReactiveCocoa/RACEXTScope.h> |
2. Sequence和Map
Sequence:佇列,是ReactiveCocoa中引入的一個型別,它類似於陣列,我們可以暫且把Sequence看做繫結訊號量的陣列吧。在OC中的NSArray可以通過rac_sequence方法轉換成ReactiveCocoa中的Sequence,然後就可以呼叫處理訊號的一些方法了。
參考以下例項程式碼:
(1)把NSArray通過rac_sequence方法生成RAC中的Sequence
(2)獲取該Sequence物件的訊號量
(3)呼叫Signal的Map方法,使每個元素的首字母大寫
(4)通過subscribNext方法對其進行遍歷輸出
Objective-C12345678910111213141516171819 | //uppercaseString use map-(void)uppercaseString{RACSequence*sequence=[@[@"you",@"are",@"beautiful"] rac_sequence];RACSignal*signal=sequence.signal;RACSignal*capitalizedSignal=[signal map:^id(NSString*value){return[value capitalizedString];}];[signal subscribeNext:^(NSString*x){NSLog(@"signal --- %@",x);}];[capitalizedSignal subscribeNext:^(NSString*x){NSLog(@"capitalizedSignal --- %@",x);}];} |
下方截圖是上個這個方法中的執行結果,從執行結果不難看出,通過Signal相應的方法處理完後,處理的結果會與新返回的訊號量所繫結。原訊號量中的值保持不變。每次訊號量呼叫相應的方法處理完資料後,都會返回一個新的訊號量,而這個訊號量是獨立於原訊號量的。
由上面的介紹可知,上面方法中的一坨程式碼可以寫成下方的一串。因為一個方法呼叫後會返回一個持有新結果的新的訊號量,然後在這個訊號量的基礎上再次呼叫訊號量其他的方法。Signal還有其他一些好用的方法,用法和map方法類似,在此就不一一贅述了,gitHub上有相應的例項文件。
Objective-C123456789 | -(void)uppercaseString{[[[@[@"you",@"are",@"beautiful"] rac_sequence].signal map:^id(NSString*value){return[value capitalizedString];}] subscribeNext:^(idx){NSLog(@"capitalizedSignal --- %@",x);}];} |
3.訊號量開關(Switch)
上面把訊號量比喻成水管,那麼Switch就是水龍頭呢。通過Switch我們可以控制那個訊號量起作用,並且可以在訊號量之間進行切換。也可以這麼理解,把Switch看成另一段水管,Switch對接那個水管,就流那個水管的水,這樣比喻應該更為貼切一些。下方是一個關於Switch的一個小例項。
(1) 首先建立3個自定義訊號量(3個水管),前兩個水管是用來接通不同的水源的(google, baidu), 而最後一個訊號量是用來對接不同水源水管的水管(signalOfSignal)。signalOfSignal接baidu水管上,他就流baidu水源的水,接google水管上就流google水源的水。
(2) 把signalOfSignal訊號量通過switchToLatest方法加工成開關訊號量。
(3) 緊接著是對通過開關資料進行處理。
(4) 開關對接baidu訊號量,然後baidu和google訊號量同時往水管裡灌入資料,那麼起作用的是baidu訊號量。
(5) 開關對接google訊號量,google和baidu訊號量傳送資料,則google訊號量輸出到signalOfSignal中
Objective-C123456789101112131415161718192021222324252627 | //訊號開關Switch-(void)signalSwitch{//建立3個自定義訊號RACSubject*google=[RACSubjectsubject];RACSubject*baidu=[RACSubjectsubject];RACSubject*signalOfSignal=[RACSubjectsubject];//獲取開關訊號RACSignal*switchSignal=[signalOfSignal switchToLatest];//對通過開關的訊號量進行操作[[switchSignal map:^id(NSString*value){return[@"https//www." stringByAppendingFormat:@"%@",value];}] subscribeNext:^(NSString*x){NSLog(@"%@",x);}];//通過開關開啟baidu[signalOfSignal sendNext:baidu];[baidu sendNext:@"baidu.com"];[google sendNext:@"google.com"];//通過開關開啟google[signalOfSignal sendNext:google];[baidu sendNext:@"baidu.com/"];[google sendNext:@"google.com/"];} |
上面程式碼輸出結果如下:
4.訊號量的合併
訊號量的合併說白了就是把兩個水管中的水合成一個水管中的水。但這個合併有個限制,當兩個水管中都有水的時候才合併。如果一個水管中有水,另一個水管中沒有水,那麼有水的水管會等到無水的水管中來水了,在與這個水管中的水按規則進行合併。下面這個例項就是把兩個訊號量進行合併。
(1) 首先建立兩個自定義的訊號量letters和numbers
(2) 吧兩個訊號量通過combineLatest函式進行合併,combineLatest說明要合併訊號量中最後傳送的值
(3) reduce塊中是合併規則:把numbers中的值拼接到letters訊號量中的值後邊。
(4) 經過上面的步驟就是建立所需的相關訊號量,也就是相當於架好運輸的管道。接著我們就可以通過sendNext方法來往訊號量中傳送值了,也就是往管道中進行灌水。
Objective-C123456789101112131415161718192021 | //組合訊號-(void)combiningLatest{RACSubject*letters=[RACSubjectsubject];RACSubject*numbers=[RACSubjectsubject];[[RACSignal combineLatest:@[letters,numbers] reduce:^(NSString*letter,NSString*number){return[letter stringByAppendingString:number];}] subscribeNext:^(NSString*x){NSLog(@"%@",x);}];//B1 C1 C2[letters sendNext:@"A"];[letters sendNext:@"B"];[numbers sendNext:@"1"];[letters sendNext:@"C"];[numbers sendNext:@"2"];} |
上面示例的執行輸出結果如下:
下面是自己畫的原理圖,思路應該還算是清晰。
5.訊號的合併(merge)
訊號合併就理解起來就比較簡單了,merge訊號量規則比較簡單,就是把多個訊號量,放入陣列中通過merge函式來合併陣列中的所有訊號量為一個。類比一下,合併後,無論哪個水管中有水都會在merge產生的水管中流出來的。下方是merge訊號量的程式碼:
(1) 建立三個自定義訊號量, 用於merge
(2) 合併上面建立的3個訊號量
(3) 往訊號裡灌入資料
Objective-C12345678910111213141516 | //合併訊號-(void)merge{RACSubject*letters=[RACSubjectsubject];RACSubject*numbers=[RACSubjectsubject];RACSubject*chinese=[RACSubjectsubject];[[RACSignal merge:@[letters,numbers,chinese]] subscribeNext:^(idx){NSLog(@"merge:%@",x);}];[letters sendNext:@"AAA"];[numbers sendNext:@"666"];[chinese sendNext:@"你好!"];} |
上面程式碼執行結果如下:
上面示例的原理圖如下:
五. 在MVVM中引入RactiveCocoa
學以致用,最後來個簡單的例項,來感受一下如何在MVVM中使用RactiveCocoa。當然今天RAC的應用是非常簡單的,但原理就是這樣的。接下啦我們要使用RAC模擬一下登入功能,當然,網路請求也是模擬的,這不是重點。重點在於如何在MVVM各層之間使用RAC的訊號量來更方便的在各個層之間進行響應式資料互動。下面這個例項的UI是非常簡單的,並且實現起來也是灰常簡單的,關鍵還是在於RAC的應用。
1.搭建Demo所需UI,使用者介面非常簡單,公有兩個使用者介面,一個是登入頁面(兩個輸入框,一個登入按鈕),一個是登入後跳轉的頁面(一個展示使用者名稱和密碼的Label)。下方是使用Storyboard實現的使用者登入頁面。實現完後,個兩個頁面各自關聯一個ViewContorller類。
2.下方是整個小Demo的工程目錄,因為我們今天的重點是如何在MVVM中使用RAC, 所以重點在於RAC的應用,對於MVVM的分層就簡化一些。下方有VC層,在VC層中有兩個檢視控制器,一個是登入使用的檢視控制器(ViewContorller)另一個是登入成功後的檢視控制器(LoginSuccessViewController)。而ViewModel中則是負責登入的ViewModel業務邏輯層,該層中負責資料驗證,網路請求,資料儲存等一些與UI無關的業務邏輯。
3.實現登入的ViewModel層
因為ViewModel層是獨立於UI層而存在的,所以可以在沒有UI的情況下我們就可以去實現相應模組的ViewModel層。這正好減少了個個層次間的耦合性,同時也提高了可測試性,總體上改善了可維護性。好廢話少說,接下來要實現登入的ViewModel層。
(1) 登入ViewModel層對應的類的標頭檔案中的內容如下所示(VCViewModel.h), 其實下方一些常用的訊號量可以抽象出來放到ViewModel的父類中,這為了簡化Demo沒有做父類的抽象。下方就是VCViewModel中interface定義的公有屬性和公有方法(Public)。userName和password(NSString型別) 用來繫結使用者輸入的使用者名稱和密碼。下方三個自定義訊號量successObject, failureObject, errorObject 用來發送網路請求的資料。successObject負責處理網路請求成功且符合正常業務邏輯的事件, failureObject負責網路請求成功不符合正常業務邏輯的處理,errorObject負責網路異常處理。
Objective-C1234567891011121314151617181920 | //// VCViewModel.h// ReactiveCocoaDemo//// Created by Mr.LuDashi on 15/10/19.// Copyright © 2015年 ZeluLi. All rights reserved.//#import <Foundation/Foundation.h>@interface VCViewModel : NSObject@property(nonatomic,strong)NSString*userName;@property(nonatomic,strong)NSString*password;@property(nonatomic,strong)RACSubject*successObject;@property(nonatomic,strong)RACSubject*failureObject;@property(nonatomic,strong)RACSubject*errorObject;-(id)buttonIsValid;-(void)login;@end |
上面可能說的有些抽象,結合專案中的例項來解釋一下什麼時候傳送successObject訊號量,如何傳送failureObject訊號量,何時使用errorObject訊號量。
以某些理財App中購買理財產品的業務流程為例。在使用者下單之前先去判斷使用者是否實名認證以及繫結銀行卡,如果使用者已經實名和繫結銀行卡就走正常支付流程(使用者就是想去下單購買),VM就往VC傳送successObject訊號,當前VC就會根據訊號量的指示跳轉到下單支付頁面。 但是如果使用者沒有實名或者綁卡,那麼VM就給VC傳送failureObject訊號,根據訊號量中的引數來判斷是走實名認證流程還是走繫結銀行卡流程。 errorObject就比較簡單了,網路異常,後臺伺服器丟擲的異常等不需要iOS這邊做業務邏輯處理的,就放在errorObject中負責錯誤資訊的展示。
文字說完了,如果有些小夥伴還不太明白,那看下面這張原理圖吧。把三種訊號量我們可以類比成十字路口的紅綠燈。successObject就是綠燈,可以走正常流程。failureObject是黃燈,先等一下,完成該做的就可以走綠燈了。而errorObject就是一紅燈,報錯異常,終止業務流程並提升錯誤資訊。有圖有真相,到這兒如果還不理解我就沒招了。
在Public方法中– (id) buttonIsValid; 負責返回登入按鈕是否可用的訊號量。– (void)login;發起網路請求,呼叫登入網路介面。
(2)程式碼的具體實現如下(VCViewModel.m中的程式碼),私有屬性如下。userNameSignal用來儲存使用者名稱的訊號量,passwordSignal是用來儲存密碼的訊號量。reqestData則是用來儲存返回資料的。