iOS開發之ReactiveCocoa下的MVVM


最近工作比較忙,但還是出來更新博客了,今天給大家分享一些ReactiveCocoa以及MVVM的一些東西,干活還是比較足的。在之前發表過一篇博文,名字叫做iOS開發之淺談MVVM的架構設計與團隊協作,大體上講的就是使用Block回調的方式實現MVVM的。在寫上篇文章時也知道有ReactiveCocoa這個函數響應式編程的框架,並且有許多人用它來更好的實現MVVM。所以在上篇博客發表后,有些同行給評論建議看一下ReactiveCocoa的東西,所以就系統的看了一下ReactiveCocoa的東西。不過有一點要說明的就是,不使用ReactiveCocoa是可以實現MVVM的,並非使用MVVM模式你就必須的使用ReactiveCocoa的東西,你可以使用KVO,Block,Delegate,Notification等手段,而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了,設置完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.信號的合並(combineLatest)

信號的合並說白了就是把兩個水管中的水合成一個水管中的水。但這個合並有個限制,當兩個水管中都有水的時候才合並。如果一個水管中有水,另一個水管中沒有水,那么有水的水管會等到無水的水管中來水了,在與這個水管中的水按規則進行合並。下面這個實例就是把兩個信號進行合並。

(1) 首先創建兩個自定義的信號lettersnumbers

(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的應用。

搭建Demo所需UI,用戶界面非常簡單,公有兩個用戶界面,一個是登錄頁面(兩個輸入框,一個登錄按鈕),一個是登錄后跳轉的頁面(一個展示用戶名和密碼的Label)。下方是使用Storyboard實現的用戶登錄頁面。實現完后,個兩個頁面各自關聯一個ViewContorller類。

 

下方是整個小Demo的工程目錄,因為我們今天的重點是如何在MVVM中使用RAC, 所以重點在於RAC的應用,對於MVVM的分層就簡化一些。下方有VC層,在VC層中有兩個視圖控制器,一個是登錄使用的視圖控制器(ViewContorller)另一個是登錄成功后的視圖控制器(LoginSuccessViewController)。而ViewModel中則是負責登錄的ViewModel業務邏輯層,該層中負責數據驗證,網絡請求,數據存儲等一些與UI無關的業務邏輯。

 

因為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 }

 

上面是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中的信號機制以及應用場景。

上面代碼寫完,我們就可以運行看一下運行效果了,下方是運行后的效果,

  

 

  上述工程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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM