MVVM+RAC項目實戰用法

前言

因為公司項目的原因,開始接觸MVVM+RAC的這種模式,剛開始并不是很適應這種函數式響應式的編程思想,感覺使用起來非常繁瑣,大大的增加了開發的負擔.但是隨著自己學習的深入和項目的實踐,這種模式的優點也隨之顯現.所以寫這篇文章希望記錄自己學習的過程,如果有寫的不對的地方也希望大家指正.

本篇文章主要針對的是Objective-C語言來講解ReactiveCocoa的應用,使用的也是公認最穩定的ReactiveCocoa v2.5,ReactiveCocoa在3.0以后的版本就是針對Swift的版本,所以大家可以根據自己需要來做下載.

目錄

  • 1:MVVM由來
  • 2:RAC淺析
  • 3:實戰使用

一:MVVM由來

大家都知道MVC是iOS App推薦的用來組織代碼的權威規范,大部分的App也都遵循這樣的構建,但是這樣的設計模式卻會隨著項目的不斷發展,業務邏輯的不斷復雜讓Controller變得臃腫,使得MVC從Model View Controller變成了Massive View Controller,這時傳統的MVC設計模式已經不能滿足我們的需求.而MVVM的出現極大的解決了這一問題,他是MVC的進一步發展,將Controller里面的業務邏輯全部抽離到ViewModel里面,我們只需要在Controller里面處理邏輯的回調結果即可.

當然MVVM使我們的Controller完成了瘦身,但是ViewModel的出現,也使得我們需要在Controller中引入ViewModel這個類,使得我們所管理的類又多了一個,之間的交互就變得更加的麻煩.此時RAC的出現就正好接管這一套邏輯上的交互,用“信號流”的概念使得邏輯變得扁平化,我們只要關心“信號流”的流向即可.

二:RAC淺析

RAC 中最核心的概念之一就是信號RACStream,RACStream中包含的兩個子類——RACSignal 和 RACSequence.因為本篇文章只是介紹RAC在實戰中的用法,所以會以RACSignal來介紹(好吧,其實是因為筆者了解的太淺了-u-).如果想知道具體內部實現可以去看下霜神關于RAC源碼解讀的文章.

1:RACSignal

不說廢話了,直接開干,首先來看一段我們常見的signal創建->訂閱->銷毀信號的整個流程代碼.

     //創建信號
 RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id subscriber) {
    //發送信號
    [subscriber sendNext:@"啊哈啦啦啦"];
    [subscriber sendCompleted];

    //取消訂閱 可以選擇在此做資源釋放的操作
    return [RACDisposable disposableWithBlock:^{
        NSLog(@"signal dispose");
    }];
}];

RACDisposable *disposable = [signal subscribeNext:^(id x) {
    NSLog(@"subscribe value = %@",x);
    //輸出結果: subscribe value = 啊哈啦啦啦
}];

   //取消訂閱
[disposable dispose];

內部邏輯實現:
(1):RACSingal調用createSignal:創建信號,內部會去調用其子類RACDynamicSignal去創建信號.
(2):RACDynamicSignal調用createSignal:方法,后面唯一的參數是個叫didSubscribe的block,當執行sendNext發送信號時,會將發送的內容保存在didSubscribe的block中.
(3):signal信號執行subscribeNext方法,會把之前保存在didSubscribe的內容取出來.
(4):取消訂閱,執行disposableWithBlock這個block.

這樣RACSignal的創建->訂閱->銷毀信號的一整個流程代碼就完成了.這種只有當訂閱者完成了訂閱才會發送信號,所以我們稱其為冷信號.他就像是在一條生產線上,打開了機器,但是這個時候沒有工人上班,那么工廠也不會正常運作.

2:RACSubject

通過查看源碼我們發現RACSubject是繼承自RACSignal的一個子類,并且遵循了<RACSubscriber>協議,意味著它既可以訂閱信號,也能發送信號.

RACSubject的例子應用

//調用subject方法創建信號
 RACSubject *subject = [RACSubject subject]; 
 //訂閱信號
   [subject subscribeNext:^(id x) {
    NSLog(@"x = %@",x);    
   }];
  //發送信號
 [subject sendNext:@"啊哈啦啦啦"];

內部實現邏輯:
(1):調用subject方法,創建信號.內部創建一個_subscribers可變數組,用來存儲訂閱信號的訂閱者.
(2):調用sendNext方法,發送消息.這時內部調用enumerateSubscribersUsingBlock方法對訂閱者進行遍歷,并發送消息.
(3):所有訂閱過該subject信號的訂閱者會收到此消息,并完成打印x內容.

到這里RACSubject的一整套流程就完成了. RACSubject中不管有沒有信號被訂閱它都會去發送消息,這種特性的信號我們稱之為熱信號.就好比工廠里的生產線一直在運作,有工人訂閱了就會用數組存起來,等到有任務(消息)下發了,就會去執行這個任務.

3:RACCommand

查看源碼我們知道,RACCommand和之前的“信號流”概念不太一樣,它是一個繼承自NSObject的類,它的主要目的是為了管理和訂閱RACSignal的類.在我們做UI組件交互的時候, RACCommand能夠幫助我們更快的處理業務,降低代碼的復雜度,節省開發的時間.

使用場景:監聽按鈕的點擊事件、網絡請求與回調處理.

知道了RACCommand的用途和使用場景,為了更好的理解RACCommand,我們先來看看RACCommand的兩個初始化方法和執行方法:

初始化方法:

- (id)initWithSignalBlock:(RACSignal * (^)(id input))signalBlock;
- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock;

執行方法:

- (RACSignal *)execute:(id)input;
- (void)setRac_command:(RACCommand *)command;

方法介紹:
1:我們知道RACCommand的作用是管理RACSignal的信號,所以初始化方法的signalBlock的返回類型就是我們需要管理的RACSignal,他的入參input,就是我們執行該Command時所傳入的數據.
2:初始化第二個方法較第一個方法多了個RACSignal類型的參數enabledSignal;這個參數的目的主要是為了過濾信號,只有當該信號中傳遞的參數為真時, Command才能夠被執行.
3:執行方法中第一個方法是RACCommand里面用于執行的方法,直接調用即可.
4:執行方法中第二個方法是UIButton的分類方法,具體使用后面會做介紹.

知道了MVVM、RACSingal、RACSubject、RACCommand的介紹和用法,接下來我們就可以在實際項目中進行應用了.

三:實戰使用

首先我們需要在ViewModel.h文件中聲明一個command,用于管理我們的信號.

//聲明屬性testCommand
@property (nonatomic, strong) RACCommand *testCommand;

注意:這里聲明的testCommand必須使用strong修飾強引用,否則接受不到RACCommand內部的信號.

然后ViewModel.m文件中在get方法中進行初始化操作.

//testCommand
- (RACCommand *)testCommand
{
    if (!_testCommand) {
    
      _testCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
        
          return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            
              //需要傳遞的參數,如果傳入的input就是網絡請求需要的參數,直接傳input即可
              NSDictionary *sendParams = @{@"test":@"我是啊哈啦啦啦"};
            
              //在這里面進行網絡請求的操作,筆者自己把網絡請求封裝成了一個信號,方便訂閱處理.
             [[YWApiManager sendApi:SCApiTypeTest withParam:sendParams] subscribeNext:^(NSDictionary *json{
                  //json:是網絡請求回調后,轉換后取得的json
                  [subscriber sendNext:json];
                 //一定要加上sendCompleted這個方法,不然無法再次執行該command
                  [subscriber sendCompleted];
                
              } error:^(NSError *error) {
               //錯誤信息 sendError 內部已經取消訂閱信號 不用執行sendCompleted方法
                  [subscriber sendError:error];
              }];
            
             return nil;
      
         }];
        
     }];
    
 }
      return _testCommand;
}

上面的方法中,筆者直接采用了initWithSignalBlock這個方法初始化RACCommand,如果說你在執行方法時已將需要需要傳遞的參數字典傳入,那么可以直接將input當成sendParams傳入.
注意:在發送消息后,一定要執行[subscriber sendCompleted]; 表示發送消息已經結束,取消信號的訂閱.不然的話該command會一直處于執行中,不能再次執行該command.

寫到這里 已經成功的將我們Controller中的網絡請求和回調處理好了,接下來我們需要在Controller里面對信號發送的json進行處理,看下面Controller中的代碼.

首先我們需要在Controller中導入ViewModel,并且聲明對象viewModel.具體操作看下面的代碼

[self.viewModel.testCommand.executionSignals subscribeNext:^(RACSignal * _Nullable execution) {
    
    [execution subscribeNext:^(id x) {
        //x為網絡請求的回調結果,可以在這里對數據進行處理
        NSLog(@"json = %@",x);
    }];
    
}];

使用testCommand的executionSignals信號進行訂閱操作. executionSignals是一個內部裝有RACSignal的高階信號,所以我們對他進行降階操作拿到execution信號,并再次訂閱此信號,此時入參的x就是我們之前傳遞的網絡請求回調“json”.

如果我們在非并發RACCommand中我們可以用switchToLatest進行降階操作,這樣寫比較直觀,也是筆者在項目中常用的方法.

[self.viewModel.testCommand.executionSignals.switchToLatest subscribeNext:^(id x) {
    //x為網絡請求的回調結果,可以在這里對x做處理,修改UI
    NSLog(@"json = %@",x);
}];

對于錯誤信號的訂閱:

[self.viewModel.testCommand.errors subscribeNext:^(NSError *error) {
    NSLog(@"error = %@",error);
}];

注意:我們不應該使用subscribeError:這個方法取訂閱錯誤信號,因為executionSignals這個信號是不會發送error事件的.所以需使用subscribeNext:訂閱錯誤信號.

最后我們只要執行該方法就行了,執行代碼地方傳的參數可以為空,或者傳入需要用到的參數,這個可以根據需求自己來決定.

 [self.viewModel.testCommand execute:@"啊哈啦啦啦"];

然后我們再來看看第二種執行方法,這種方法會使按鈕綁定上testCommand,如果RACCommand是以initWithEnabled這種方式初始化的,按鈕的enabled屬性會隨enabledSignal傳入的值的改變而改變.即傳入值為真,按鈕不可點擊.

 testButton.rac_command = self.viewModel.testCommand;

以上代碼都是針對冷信號來處理,讓我們在看下RACSubject在項目中的用法.

應用場景:比如現在我們有個tableView的列表,每個cell的點擊事件跳到新的界面,在新的界面中我們會選擇一些數據并回傳到之前的界面,最后刷新tableView,把選擇的數據展示在tableView上.這樣的一個操作我們就可以使用RACSubject來完成,看下面代碼.

 @property (nonatomic, strong) RACSubject *reloadSignal;

首先我在ViewModel里面聲明了一個熱信號reloadSignal,然后初始化testCommand.

 //testCommand
- (RACCommand *)testCommand
{
     if (!_testCommand) {
         @weakify(self);
     _testCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
        
        return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            
                [subscriber sendNext:@"我是阿哈啦啦啦,我要被發送了"];
                [subscriber sendCompleted];
                return nil;
            
            }]doNext:^(id x) {
               @strongify(self);
               //doNext的入參x是sendNext發送的參數
               [self.reloadSignal sendNext:x];
            
           }];
        
        }];
    
    }
      return _testCommand;
}

和第一個例子不同的是,這里發送的數據會進行一步操作,調用doNext:方法(將sendNext的參數傳遞給doNext的入參).然后熱信號reloadSignal發送入參x.

最后在Controller中的操作,和例1中是一樣的,要注意的是調用時需要使用reloadSignal進行訂閱.熱信號的優點在于,對于需要進行多次reload的這種操作,我們不用去重復訂閱.

 [self.viewModel.reloadSignal subscribeNext:^(id x) {
    NSLog(@"x = %@",x);
}];

最后

關于RAC這塊筆者自己還在學習之中,所以希望拋磚引玉,大家互相討論共同進步.以上就是關于ReactiveCocoa的一個簡單用法,比較簡單實用,希望能幫到新學習RAC的各位.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。