1. 程式人生 > >對 RAC 中 RACCommand 的理解和應用

對 RAC 中 RACCommand 的理解和應用

RACSignal 和 RACCommand

RACCommand 是 RAC 中的最複雜的一個類之一,它也是一種廣義上的訊號。RAC 中訊號其實是一種物件(或者是不同程式碼塊)之間通訊機制,在面向物件中,類之間的通訊方式主要是方法呼叫,而訊號也是一種呼叫,只不過它是函式式的,因此訊號不僅僅可以在物件之間相互呼叫(傳參),也可以在不同程式碼塊(block)之間進行呼叫。

一般來說,RAC 中用 RACSignal 來代表訊號。一個物件建立 RACSignal 訊號,建立訊號時會包含一個 block,這個 block 的作用是傳送訊號給訂閱者(類似方法返回值或回撥函式)。另一個物件(或同一個物件)可以用這個訊號進行訂閱,從而獲得傳送者傳送的資料。這個過程和方法呼叫一樣,訊號相當於暴露給其它物件的方法,訂閱者訂閱訊號相當於呼叫訊號中的方法(block),只不過返回值的獲得變成了通過 block 來獲得。此外,你無法直接向 RACSignal 傳遞引數,要向訊號傳遞引數,需要提供一個方法,將要傳遞的引數作為方法引數,建立一個訊號,通過 block 的捕獲區域性變數方式將引數捕獲到訊號的 block 中。

而 RACCommand 不同,RACCommand 的訂閱不使用 subscribeNext 方法而是用 execute: 方法。而且 RACCommand 可以在訂閱/執行(即 excute:方法)時傳遞引數。因此當需要向訊號傳遞引數的時候,RACComand 更好用。

此外,RACCommand 包含了一個 executionSignal 的訊號,這個訊號是對使用者透明的,它是自動建立的,由 RACCommand 進行管理。許多資料中把它稱之為訊號中的訊號,是因為這個訊號會發送其它訊號——即 RACCommand 在初始化的 signalBlock 中建立(return)的訊號。這個訊號是 RACCommand 建立時由我們建立的,一般是用於處理一些非同步操作,比如網路請求等。請看程式碼:

    @weakify(self);
    RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(NSString* input) {

            @strongify(self);

            NSDictionary *body = @{@"memberCode": input};

            // 進行網路操作,同時將這個操作封裝成訊號 return
            return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
                // 一些網路操作
                ...
return nil; }]; }];

在這段程式碼中,需要注意:

  1. initWithSignalBlock 方法初始化一個 RACCommand,這個方法需要提供一個 signalBlock 塊引數。
  2. signalBlock 塊的簽名中,有一個入參 input,它是訂閱者在訂閱/執行(呼叫 RACCommand 的 execute: 方法)時傳入的。可以是任意型別(id),這裡我們為了簡單起見,定義為 NSString(它真的就是一個 NSString),從而減少型別轉換的程式碼。
  3. signalBlock 塊的返回值是一個訊號,因此在塊體中,我們用 createSignal 建立了一個訊號作為返回值。這是必須的,因為這個訊號中定義了一些我們需要進行的處理,比如網路請求等。真正的任務是在這個訊號中進行的,外部的訂閱者通過 execute: 方法訂閱/執行這個 RACCommand 時,這些程式碼就得以執行。這些程式碼的具體內容在這裡並不重要,請忽略。

從上面來看,其實用一個簡單的 RACSignal 也能完成同樣的工作。那為什麼還要用 RACCommand 呢?

這就是 RACCommand 的另一個優點了,它可以監聽 RACCommand 自身的執行狀態,比如開始、進行中、完成、錯誤等。用 RACSignal 可以監聽到完成(complete)、錯誤(error)、進行中(next)。但開始就無法實現了,而且實現起來程式碼比較分散和難看(吐槽一下,RAC 絕大多數時候並沒有為我們提供新功能,只不過是一種程式碼美學的處理而已)。

RACCommand 的解決辦法很簡單,就是用一個訊號來監聽另一個訊號的執行。也就是 executionSignal 訊號的來由。在本文中,我們會叫他外層訊號。而 signalBlock 中的那個訊號(真正執行主要工作的)則叫內層訊號。

理論足夠了,來做一些實驗吧。

開始

新建一個 Single View 專案。UI 很簡單,就一個 ViewController 類,上面包含了一個 UIButton 和 UITextView,隨便你怎麼佈局它們。然後建立相應的連線。

重要的是 ViewModel 類,我們用它簡單實現一個 MVVM 架構。在 ViewModel.h 裡宣告一些屬性:

@property(nonatomic, strong) RACCommand *requestData;
@property(nonatomic, assign) HTTPRequestStatus requestStatus;

@property (strong, nonatomic) NSDictionary *data;
@property (strong, nonatomic) NSError* error;
  1. requestData 就是本文的核心了,一個 RACCommand 類,提供一些訊號給 controller 用於更新 UI(比如小菊花)。
  2. requestStatus 記錄網路請求的狀態,比如開始、完成、出錯等,它是一個列舉,定義如下:

    typedef NS_ENUM(NSUInteger, HTTPRequestStatus) {
    HTTPRequestStatusBegin,
    HTTPRequestStatusEnd,
    HTTPRequestStatusError,
    };
  3. data 用於儲存成功請求後獲得的資料。
  4. error 用於儲存請求失敗後的錯誤。

接下來是實現了,首先看 RACCommand 的建立。

建立 RACCommand

在 ViewModel.m 中,我們通過懶載入方式來初始化 RACCommand 物件:

- (RACCommand *)requestData {
    if (!_requestData) {
        @weakify(self);
        _requestData = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(NSString* input) {
            @strongify(self);
            NSDictionary *body = @{@"memberCode": input};
            // 進行網路操作,同時將這個操作封裝成訊號 return
            return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
                [self postUrl:kSubscribeURL params:body requestType:@"json" success:^(id  _Nullable responseObject) {
                    [subscriber sendNext:responseObject];
                    [subscriber sendCompleted];
                } failure:^(NSError *error) {
                    [subscriber sendError:error];
                }];

                return nil;
            }];
        }];
    }
    return _requestData;
}

這段程式碼大部分都在前面講過了,只有 [self postUrl: … 這部分程式碼是新的。這部分程式碼是一個將 HTTP 請求封裝為 RACSignal 的典型示例,相信每個人都不陌生,你可以替換成自己的程式碼。

訂閱訊號

RACCommand 中封裝了各種訊號,我們只用到了外層訊號(executionSignal)和內層訊號。訂閱這些訊號能夠讓我們實現兩個目的:拿到請求返回的資料、跟蹤 RACCommand 開始結束狀態。定義一個方法來做這些事情:

- (void)subcribeCommandSignals {
    @weakify(self)
        // 1. 訂閱外層訊號
    [self.requestData.executionSignals subscribeNext:^(RACSignal* innerSignal) {
        @strongify(self)
        // 2. 訂閱內層訊號
        [innerSignal subscribeNext:^(NSDictionary* x) {
            self.data = x;
            self.requestStatus = HTTPRequestStatusEnd;
        }];

        self.error = nil;
        self.requestStatus = HTTPRequestStatusBegin;
    }];
    // 3. 訂閱 errors 訊號
    [self.requestData.errors subscribeNext:^(NSError * _Nullable x) {
        @strongify(self)
        self.error = x;
        self.data = nil;
        self.requestStatus = HTTPRequestStatusError; // 這一句必須放在最後一句,否者 controller 拿不到資料
    }];
}

這裡需要注意:

  1. 訂閱外層訊號(即 executionSignals)。外層訊號在訂閱或執行(即 execute: )時傳送。因此我們可以將它視作請求即將開始之前的訊號,在這裡將 self.error 清空,將 requestStatus 修改為 begin。
  2. 訂閱內層訊號,因為內層訊號由外層訊號(executionSignals)作為資料傳送(sendNext:),而傳送的資料一般是作為 subcribeNext:時的 block 的引數來接收的,因此在這個塊中,塊的引數就是內層訊號。這樣我們就可以訂閱內層訊號了,同時獲取資料(儲存到 data 屬性)並修改 requestStatus 為 end。
  3. RACCommand 比較特殊的一點是 error 訊號需要在 errors 中訂閱,而不能在 executionSignals 中訂閱。在這裡我們訂閱了 errors 訊號,並修改 data、error 和 requestStatus 屬性值。

最後,在 init 方法中呼叫這個方法,來完成對相關訊號的訂閱。

- (id)init {
    self = [super init];
    if (self) {
        [self subcribeCommandSignals];
    }
    return self;
}

訊號部分處理完了,接下來是 UI。

Controller

UI 需要關心 RACCommand 的開始、完成、失敗狀態,以便顯示隱藏小菊花,同時 UI 需要關心 RACCommand 獲取的資料並做展示(這裡為了簡單起見,直接用 Text View 顯示出資料)。這其實是對 ViewModel 中的 data 屬性和 requestStatus 屬性的監聽,因此,接下來的一步就是在 controller 中將 View 和 ViewModel 進行綁定了。繫結的程式碼如下:

-(void)bindViewModel{
@weakify(self)
        // 1. 
    [[RACObserve(_viewModel, requestStatus) skip:1] subscribeNext:^(NSNumber* x) {
        @strongify(self)
        switch ([x intValue]) {
                case HTTPRequestStatusBegin:
                [MBProgressHUD showHUDAddedTo:self.view animated:YES];
                break;
                case HTTPRequestStatusEnd:
                [MBProgressHUD hideHUDForView:self.view animated:YES];
                break;
                case HTTPRequestStatusError:
                [MBProgressHUD hideHUDForView:self.view animated:YES];
                [MBProgressHUD showError:self.viewModel.error.localizedDescription toView:self.view];
                break;
         }
    }];

    // 2. 
    RAC(self.textView,text) = [[RACObserve(_viewModel, data) skip:1] map:^id _Nullable(NSDictionary* value) {
        return dic2str(value);
    }];

    // 3.
//    _button.rac_command = _viewModel.requestData;
    [[_button rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
        @strongify(self)
        [self.viewModel.requestData execute:@"96671e1a812e46dfa4264b9b39f3e225"];
    }];

}
  1. 監聽 ViewModel 的 requestStatus 屬性,當屬性為 begin 時顯示小菊花,當屬性為 end 時隱藏小菊花,當屬性為 error 時隱藏小菊花並顯示錯誤訊息。這裡需要注意的是,RAC 在第一次繫結時會自動傳送一條訊號,這時 requestStatus 的初始值是預設值 0,這樣的訊息顯然是多餘的,我們要用 skip:1 過濾掉。
  2. 將 ViewModel 的 data 屬性和 textView 進行繫結。因為 data 是一個 NSDictionary,而 textView 的 text 屬性是 NSString,顯然無法做這樣的繫結,於是我們用 map: 方法把 data 從 NSDictionary 轉換為 NSString。這裡的 dic2str 便利函式替我們完成這個工作。同樣 RACObserve 會在第一次繫結時傳送一條多餘訊號,我們用 skip:1 過濾掉。
  3. 訂閱按鈕的 rac 訊號進行事件處理。這裡沒有使用 _button.rac_command = _viewModel.requestData 這樣的方式,雖然它看起來比較簡單,但卻無法在呼叫 RACCommand 時傳遞引數。因此我們手工訂閱了按鈕的 rac 訊號,並在訂閱塊中手動呼叫了 execute: 方法,以此來傳遞了一個字串引數。作為演示,這裡的引數傳遞的是一個常量,你可以傳入任意值(id型別)。

最後,在 viewDidLoad 方法中,我們需要初始化 ViewModel,並呼叫 bindViewModel 方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    _viewModel = [ViewModel new];

    [self bindViewModel];
}

注: 這裡使用的 RAC 是 ReactiveObjC 3.0,如果你使用了 ReactiveCocoa 的其它版本,有的程式碼可能需要修改。