今天給大家分享一些ReactiveCocoa以及MVVM的一些東西,幹活還是比較足的。在之前發表過一篇博文,名字叫做《iOS開發之淺談MVVM的架構設計與團隊協作》,大體上講的就是使用Block回調的方式實現MVVM的。在寫上篇文章時也知道有ReactiveCocoa這個函數響應式編程的框架,並且有許多人用它來更好的實現MVVM。所以在上篇博客發表後,有些同行給評論建議看一下ReactiveCocoa的東西,所以就係統的看了一下ReactiveCocoa的東西。不過有一點要說明的就是,不使用ReactiveCocoa是可以實現MVVM的,並非使用MVVM模式你就必須的使用ReactiveCocoa的東西,你可以使用KVO,Block,Delegate,Navigation等手段,而ReactiveCocoa更優雅的實現了這個過程。ReactiveCocoa就是一個響應式編程的框架,它會使MVVM每層之間交互起來更爲方便,所以長和MVVM聯繫在一起。
一.函數響應式編程(Function Reactive Programming)
關於函數響應式編程的東西,我想引用國外這個ReactiveCocoa教學視頻(視頻鏈接https://vimeo.com/65637501)中的一張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上的鏈接(https://github.com/ReactiveCocoa/ReactiveCocoa)。
三. 在工程中引入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框架了
1 #import <ReactiveCocoa/ReactiveCocoa.h> 2 #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方法對其進行遍歷輸出
1 //uppercaseString use map 2 - (void)uppercaseString { 3 4 RACSequence *sequence = [@[@"you", @"are", @"beautiful"] rac_sequence]; 5 6 RACSignal *signal = sequence.signal; 7 8 RACSignal *capitalizedSignal = [signal map:^id(NSString * value) { 9 return [value capitalizedString]; 10 }]; 11 12 [signal subscribeNext:^(NSString * x) { 13 NSLog(@"signal --- %@", x); 14 }]; 15 16 [capitalizedSignal subscribeNext:^(NSString * x) { 17 NSLog(@"capitalizedSignal --- %@", x); 18 }]; 19 }
下方截圖是上個這個方法中的運行結果,從運行結果不難看出,通過Signal相應的方法處理完後,處理的結果會與新返回的信號量所綁定。原信號量中的值保持不變。每次信號量調用相應的方法處理完數據後,都會返回一個新的信號量,而這個信號量是獨立於原信號量的。
由上面的介紹可知,上面方法中的一坨代碼可以寫成下方的一串。因爲一個方法調用後會返回一個持有新結果的新的信號量,然後在這個信號量的基礎上再次調用信號量其他的方法。Signal還有其他一些好用的方法,用法和map方法類似,在此就不一一贅述了,gitHub上有相應的實例文檔。
1 - (void)uppercaseString { 2 3 [[[@[@"you", @"are", @"beautiful"] rac_sequence].signal 4 map:^id(NSString * value) { 5 return [value capitalizedString]; 6 }] subscribeNext:^(id x) { 7 NSLog(@"capitalizedSignal --- %@", x); 8 }]; 9 }
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中
1 //信號開關Switch 2 - (void)signalSwitch { 3 //創建3個自定義信號 4 RACSubject *google = [RACSubject subject]; 5 RACSubject *baidu = [RACSubject subject]; 6 RACSubject *signalOfSignal = [RACSubject subject]; 7 8 //獲取開關信號 9 RACSignal *switchSignal = [signalOfSignal switchToLatest]; 10 11 //對通過開關的信號量進行操作 12 [[switchSignal map:^id(NSString * value) { 13 return [@"https//www." stringByAppendingFormat:@"%@", value]; 14 }] subscribeNext:^(NSString * x) { 15 NSLog(@"%@", x); 16 }]; 17 18 19 //通過開關打開baidu 20 [signalOfSignal sendNext:baidu]; 21 [baidu sendNext:@"baidu.com"]; 22 [google sendNext:@"google.com"]; 23 24 //通過開關打開google 25 [signalOfSignal sendNext:google]; 26 [baidu sendNext:@"baidu.com/"]; 27 [google sendNext:@"google.com/"]; 28 }
上面代碼輸出結果如下:
4.信號量的合併
信號量的合併說白了就是把兩個水管中的水合成一個水管中的水。但這個合併有個限制,當兩個水管中都有水的時候才合併。如果一個水管中有水,另一個水管中沒有水,那麼有水的水管會等到無水的水管中來水了,在與這個水管中的水按規則進行合併。下面這個實例就是把兩個信號量進行合併。
(1) 首先創建兩個自定義的信號量letters和numbers
(2) 吧兩個信號量通過combineLatest函數進行合併,combineLatest說明要合併信號量中最後發送的值
(3) reduce塊中是合併規則:把numbers中的值拼接到letters信號量中的值後邊。
(4) 經過上面的步驟就是創建所需的相關信號量,也就是相當於架好運輸的管道。接着我們就可以通過sendNext方法來往信號量中發送值了,也就是往管道中進行灌水。
1 //組合信號 2 - (void)combiningLatest{ 3 RACSubject *letters = [RACSubject subject]; 4 RACSubject *numbers = [RACSubject subject]; 5 6 [[RACSignal 7 combineLatest:@[letters, numbers] 8 reduce:^(NSString *letter, NSString *number){ 9 return [letter stringByAppendingString:number]; 10 }] 11 subscribeNext:^(NSString * x) { 12 NSLog(@"%@", x); 13 }]; 14 15 //B1 C1 C2 16 [letters sendNext:@"A"]; 17 [letters sendNext:@"B"]; 18 [numbers sendNext:@"1"]; 19 [letters sendNext:@"C"]; 20 [numbers sendNext:@"2"]; 21 }
上面示例的運行輸出結果如下:
下面是自己畫的原理圖,思路應該還算是清晰。
5.信號的合併(merge)
信號合併就理解起來就比較簡單了,merge信號量規則比較簡單,就是把多個信號量,放入數組中通過merge函數來合併數組中的所有信號量爲一個。類比一下,合併後,無論哪個水管中有水都會在merge產生的水管中流出來的。下方是merge信號量的代碼:
(1) 創建三個自定義信號量, 用於merge
(2) 合併上面創建的3個信號量
(3) 往信號裏灌入數據
1 //合併信號 2 - (void)merge { 3 RACSubject *letters = [RACSubject subject]; 4 RACSubject *numbers = [RACSubject subject]; 5 RACSubject *chinese = [RACSubject subject]; 6 7 [[RACSignal 8 merge:@[letters, numbers, chinese]] 9 subscribeNext:^(id x) { 10 NSLog(@"merge:%@", x); 11 }]; 12 13 [letters sendNext:@"AAA"]; 14 [numbers sendNext:@"666"]; 15 [chinese sendNext:@"你好!"]; 16 }
上面代碼運行結果如下:
上面示例的原理圖如下:
五. 在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負責網絡異常處理。
1 // 2 // VCViewModel.h 3 // ReactiveCocoaDemo 4 // 5 // Created by Mr.LuDashi on 15/10/19. 6 // Copyright © 2015年 ZeluLi. All rights reserved. 7 // 8 9 #import <Foundation/Foundation.h> 10 11 @interface VCViewModel : NSObject 12 @property (nonatomic, strong) NSString *userName; 13 @property (nonatomic, strong) NSString *password; 14 @property (nonatomic, strong) RACSubject *successObject; 15 @property (nonatomic, strong) RACSubject *failureObject; 16 @property (nonatomic, strong) RACSubject *errorObject; 17 18 - (id) buttonIsValid; 19 - (void)login; 20 @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則是用來存儲返回數據的。
1 @interface VCViewModel () 2 @property (nonatomic, strong) RACSignal *userNameSignal; 3 @property (nonatomic, strong) RACSignal *passwordSignal; 4 @property (nonatomic, strong) NSArray *requestData; 5 @end
(3)VCViewModel的初始化方法如下,負責初始化屬性。
1 - (instancetype)init 2 { 3 self = [super init]; 4 if (self) { 5 [self initialize]; 6 } 7 return self; 8 } 9 10 - (void)initialize { 11 _userNameSignal = RACObserve(self, userName); 12 _passwordSignal = RACObserve(self, password); 13 _successObject = [RACSubject subject]; 14 _failureObject = [RACSubject subject]; 15 _errorObject = [RACSubject subject]; 16 }
(4) 發送登錄按鈕是否可用信號的方法如下,主要用到了信號量的合併。
//合併兩個輸入框信號,並返回按鈕bool類型的值 - (id) buttonIsValid { RACSignal *isValid = [RACSignal combineLatest:@[_userNameSignal, _passwordSignal] reduce:^id(NSString *userName, NSString *password){ return @(userName.length >= 3 && password.length >= 3); }]; return isValid; }
(5) 模擬網絡請求的發送,併發出網絡請求成功的信號,具體代碼如下
1 - (void)login{ 2 3 //網絡請求進行登錄 4 _requestData = @[_userName, _password]; 5 6 //成功發送成功的信號 7 [_successObject sendNext:_requestData]; 8 9 //業務邏輯失敗和網絡請求失敗發送fail或者error信號並傳參 10 11 }
4. 上面是VM的實現,如果要進行單元測試的話,就對相應的VM類進行初始化,調用相應的函數進行單元測試即可。接着就是看如何在相應的VC模塊中使用VM。
(1) 在VC中實例化相應的VM類,並綁定相應的參數和實現接收不同信號的方法,具體代碼如下:
1 //關聯ViewModel 2 - (void)bindModel { 3 _viewModel = [[VCViewModel alloc] init]; 4 5 6 RAC(self.viewModel, userName) = self.userNameTextField.rac_textSignal; 7 RAC(self.viewModel, password) = self.passwordTextField.rac_textSignal; 8 RAC(self.loginButton, enabled) = [_viewModel buttonIsValid]; 9 10 @weakify(self); 11 12 //登錄成功要處理的方法 13 [self.viewModel.successObject subscribeNext:^(NSArray * x) { 14 @strongify(self); 15 LoginSuccessViewController *vc = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:@"LoginSuccessViewController"]; 16 vc.userName = x[0]; 17 vc.password = x[1]; 18 [self presentViewController:vc animated:YES completion:^{ 19 20 }]; 21 }]; 22 23 //fail 24 [self.viewModel.failureObject subscribeNext:^(id x) { 25 26 }]; 27 28 //error 29 [self.viewModel.errorObject subscribeNext:^(id x) { 30 31 }]; 32 33 }
(2) 點擊登錄按鈕,調用VM中登錄相應的網絡請求方法即可
1 - (void)onClick { 2 //按鈕點擊事件 3 [[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside] 4 subscribeNext:^(id x) { 5 [_viewModel login]; 6 }]; 7 }
到此爲止,一個完整模擬登錄模塊的RAC下的MVVM就實現完畢。當然上面的Demo是非常簡陋的,還有好多地方需要進化。不過麻雀雖小,道理你懂得。主要是通過上面的Demo來感受一下RAC中的信號量機制以及應用場景。
5.上面代碼寫完,我們就可以運行看一下運行效果了,下方是運行後的效果,
上述工程GitHub分享鏈接:https://github.com/lizelu/MVVMWithReactiveCocoa
其他參考資料:
https://github.com/ReactiveCocoa/ReactiveViewModel
http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/
http://www.teehanlax.com/blog/getting-started-with-reactivecocoa/
http://nshipster.cn/reactivecocoa/
http://limboy.me/ios/2013/06/19/frp-reactivecocoa.html
https://vimeo.com/65637501
http://southpeak.github.io/blog/2014/08/08/mvvmzhi-nan-yi-:flickrsou-suo-shi-li/
http://southpeak.github.io/blog/2014/08/02/reactivecocoazhi-nan-%5B%3F%5D-:xin-hao/
http://southpeak.github.io/blog/2014/08/02/reactivecocoazhi-nan-er-:twittersou-suo-shi-li/
ViewModel:
Kicking off network or database requests
Determining when information should be hidden or shown
Date and number formatting
Localization
ViewController:
Layout
Animations
Device rotation
View and window transitions
Presenting loaded UI