ReactiveCocoa教程:上半部【譯】

原文:ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2
下半部翻譯:ReactiveCocoa教程:下半部【翻譯】

譯注:在學(xué)習(xí)ReactiveCocoa的過程中看了不少文章,但對我而言最適合入門的當數(shù)此系列。
雖然已經(jīng)是14年的文章,但基本對于ReactiveCocoa在OC上的使用仍然基本符合。這上下兩篇文章最好之處在于通過兩個應(yīng)用實例囊括了大部分ReactiveCocoa的常見用法,雖然沒有提及RACCommand具體用法,也瑕不掩瑜。
為便于之后自己查看,同時加深理解,故對文章作簡單翻譯。因為第一次做翻譯,如果翻譯中有不妥之處,敬請諒解,亦歡迎大家交流:)

作為一個iOS開發(fā)人員,幾乎你寫的每一行代碼都是為了處理各種事件:按鈕的點擊、網(wǎng)絡(luò)的反饋、屬性的變更(KVO)、位置的改變(CoreLocation)等等。但與此同時,這些事件相應(yīng)又有著如action、delegate、KVO、callback等各種不一樣的實現(xiàn)。ReactiveCocoa為此定義了一套接口與事件的標準,以便于用戶憑借一套基礎(chǔ)的工具對這些事件進行鏈接,過濾及重組。

看到這里,你是感到迷惑?興奮?還是迫不及待?那就繼續(xù)往下讀吧 :]

ReactiveCocoa是如下兩種編程風(fēng)格的集合體:

  • 使用高階函數(shù)的函數(shù)式編程,如:以某一函數(shù)作為另一函數(shù)的入?yún)?/li>
  • 注重數(shù)據(jù)流及變更傳遞的響應(yīng)式編程

故而ReactiveCocoa被稱為響應(yīng)式函數(shù)編程(Functional Reactive Programming 或FRP)框架。

讀到這里你可能感到相當迷惑,但請放心,解惑這正是本教程的目的!雖然編程范式也是一個引人入勝的話題,但相比學(xué)術(shù)理論,本教程余下的部分,將希望通過完成一個實例讓你更好的理解ReactiveCocoa。

響應(yīng)式樂園

在本教程中,你將通過一個非常簡單的應(yīng)用實例學(xué)習(xí)響應(yīng)式編程。在開始之前,作為準備工作,請先下載初始工程并編譯運行。

ReactivePlayground是一個包含用戶登錄界面的簡單應(yīng)用。在用戶名及密碼校驗成功后,你將看到一只非常可愛的小貓。


接下來我們最好花點時間去熟悉一下初始工程的源碼。由于工程相當?shù)暮唵?,所以這不會花費太多的時間。

打開查看RWViewController.m文件。嘗試回答以下問題:點擊Sign In按鈕會觸發(fā)哪些事件?顯示/隱藏登錄失敗的文本需要哪些條件?在這個簡單例子中,你可能只需一兩分鐘就能解答這些問題。但如果情況復(fù)雜一點,你就可能需要花比較多的的時間去做相同的分析。

而使用ReactiveCocoa的話,你就能夠更清晰地理解應(yīng)用實際的意圖了。事不宜遲,讓我們現(xiàn)在立馬開始吧!

添加 ReactiveCocoa 框架

往項目中添加ReactiveCocoa框架最簡單的途徑是使用CocoaPods。如果你之前從來沒有使用過CocoaPods,你最好學(xué)習(xí)一下CocoaPods官網(wǎng)上的教程,或者至少跟著官網(wǎng)教程的初始步驟操作一遍,來保證你已經(jīng)達到了安裝的先決條件。

注意:如果基于某些原因你實在不想使用CocoaPods的話,只要你跟隨ReactiveCocoa GitHub上文檔進行導(dǎo)入操作,也同樣可以使用ReactiveCocoa。

如果你還打開著ReactivePlayground項目的話,現(xiàn)在請先關(guān)閉。CocoaPods會創(chuàng)建一個Xcode workspace文件,用以替代原有的項目文件。

打開終端。跳轉(zhuǎn)到你項目所在的文件夾,并輸入以下指令:

touch Podfile
open -e Podfile

這創(chuàng)建了一個名為Podfile的文件并通過TextEdit打開。然后復(fù)制粘貼以下文本到TextEdit窗口中:(譯注:使用Sublime等文本編輯器亦可,不過不要使用系統(tǒng)自帶的文本編輯器)

platform :ios, '7.0'
 
pod 'ReactiveCocoa', '2.1.8'

上面文本規(guī)定了使用平臺為iOS,最小的SDK版本號為7.0,并添加ReactiveCocoa框架作為依賴包。文檔保存后,請返回終端窗口,并輸入以下命令:(譯注:ReactiveCocoa2.X為OC版本,之后包括最新的版本都基于Swift)

pod install

你將會會看到與下文相似的輸出:

Analyzing dependencies
Downloading dependencies
Installing ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project
 
[!] From now on use `RWReactivePlayground.xcworkspace`.

這說明ReactiveCocoa已經(jīng)成功下載,而且CocoaPods已經(jīng)創(chuàng)建好了Xcode workspace文件用以整合相關(guān)的框架到你現(xiàn)有的應(yīng)用中。打開新生成的workspace文件RWReactivePlayground.xcworkspace,瀏覽由CocoaPods在項目導(dǎo)航中生成的結(jié)構(gòu):

CocoaPods創(chuàng)建了一個新的工作空間,并在原始項目RWReactivePlayground外添加了包含有ReactiveCocoa 框架的Pods項目。CocoaPods就這樣輕而易舉地達到了管理依賴包的目的!

你想必已經(jīng)注意到個項目叫做響應(yīng)式樂園(ReactivePlayground),那么接下來,自然就是我們的游樂時間了……

游樂時間

正如在介紹中提到的,ReactiveCocoa提供了一套接口標準來處理應(yīng)用中出現(xiàn)的各式事件流。這在ReactiveCocoa的術(shù)語中被稱作信號,對應(yīng)的是RACSignal類。

下面打開應(yīng)用的初始視圖控制器RWViewController.m,在文件開頭添加以下代碼來導(dǎo)入ReactiveCocoa的頭文件:

#import <ReactiveCocoa/ReactiveCocoa.h>

你暫時還不需要替換現(xiàn)有的代碼,現(xiàn)在要做的只是在原有的代碼基礎(chǔ)上豐富一下。先在viewDidLoad方法的結(jié)尾處添加下面代碼:

[self.usernameTextField.rac_textSignal subscribeNext:^(id x) {
  NSLog(@"%@", x);
}];

編譯運行應(yīng)用,在用戶名輸入框中隨意輸入點文本。留意一下控制臺你就能發(fā)現(xiàn)如下相似的輸出:

2013-12-24 14:48:50.359 RWReactivePlayground[9193:a0b] i
2013-12-24 14:48:50.436 RWReactivePlayground[9193:a0b] is
2013-12-24 14:48:50.541 RWReactivePlayground[9193:a0b] is 
2013-12-24 14:48:50.695 RWReactivePlayground[9193:a0b] is t
2013-12-24 14:48:50.831 RWReactivePlayground[9193:a0b] is th
2013-12-24 14:48:50.878 RWReactivePlayground[9193:a0b] is thi
2013-12-24 14:48:50.901 RWReactivePlayground[9193:a0b] is this
2013-12-24 14:48:51.009 RWReactivePlayground[9193:a0b] is this 
2013-12-24 14:48:51.142 RWReactivePlayground[9193:a0b] is this m
2013-12-24 14:48:51.236 RWReactivePlayground[9193:a0b] is this ma
2013-12-24 14:48:51.335 RWReactivePlayground[9193:a0b] is this mag
2013-12-24 14:48:51.439 RWReactivePlayground[9193:a0b] is this magi
2013-12-24 14:48:51.535 RWReactivePlayground[9193:a0b] is this magic
2013-12-24 14:48:51.774 RWReactivePlayground[9193:a0b] is this magic?

你會發(fā)現(xiàn)每次你修改文本框中的輸入,block中的代碼塊就會執(zhí)行。不需要target-action,不需要delegate——只憑借信號和block就能實現(xiàn)。這多棒!

ReactiveCocoa的信號(表現(xiàn)為RACSignal類)會向他的訂閱者們發(fā)送事件流。發(fā)送的事件分為三種類型:next,errorcompleted。一個信號在因為報錯或完成的終止前可以發(fā)送若干個事件。在教程的上半部分將會把重點放在next事件上。你可以在教程的下半部學(xué)習(xí)有關(guān)errorcompleted的知識。

RACSignal提供了若干的方法用以訂閱這些不同的事件類型。每個方法都會接受一個或多個block作為入?yún)?,當新的事件出現(xiàn)時,block中的邏輯代碼就會執(zhí)行。在這個例子中,你可以看到subscribeNext:方法就提供了一個block,在每個next事件到達時執(zhí)行里面的代碼。

ReactiveCocoa框架使用category為很多標準的UIKit控件添加了信號,借此你可以對它們的各種事件進行訂閱。這正是文本輸入框的rac_textSignal屬性的由來。

理論學(xué)習(xí)就到此為止,現(xiàn)在是時候使用ReactiveCocoa來實現(xiàn)一些真正的功能了!

ReactiveCocoa有多個用于操作事件流的方法。舉個例子,假如你僅僅關(guān)心長度超過3字節(jié)的用戶名,你可以使用filter方法來達到這個目的。更新你剛剛添加到viewDidLoad方法的代碼如下:

[[self.usernameTextField.rac_textSignal
  filter:^BOOL(id value) {
    NSString *text = value;
    return text.length > 3;
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  }];

編譯運行,并往文本輸入框中隨意輸入一些內(nèi)容,此時控制臺就只會打印文本長度超過3個字節(jié)的文本。

2013-12-26 08:17:51.335 RWReactivePlayground[9654:a0b] is t
2013-12-26 08:17:51.478 RWReactivePlayground[9654:a0b] is th
2013-12-26 08:17:51.526 RWReactivePlayground[9654:a0b] is thi
2013-12-26 08:17:51.548 RWReactivePlayground[9654:a0b] is this
2013-12-26 08:17:51.676 RWReactivePlayground[9654:a0b] is this 
2013-12-26 08:17:51.798 RWReactivePlayground[9654:a0b] is this m
2013-12-26 08:17:51.926 RWReactivePlayground[9654:a0b] is this ma
2013-12-26 08:17:51.987 RWReactivePlayground[9654:a0b] is this mag
2013-12-26 08:17:52.141 RWReactivePlayground[9654:a0b] is this magi
2013-12-26 08:17:52.229 RWReactivePlayground[9654:a0b] is this magic
2013-12-26 08:17:52.486 RWReactivePlayground[9654:a0b] is this magic?

你剛創(chuàng)建的是一個非常簡單的管道(譯注:pipeline,指一個信號被訂閱的完整過程)。這正是響應(yīng)式編程的的本質(zhì),通過傳遞一系列的數(shù)據(jù)流實現(xiàn)你應(yīng)用中的功能。

下圖中描繪了整個流程:


可以看到,rac_textSignal是這個事件的源頭。之后數(shù)據(jù)流通過一個過濾器,只允許包含長度超過3的的字符串的事件通過。符合條件的事件最后來到subscribeNext:方法,事件傳遞的值在block中打印。

值得注意的是,filter方法的返回值也是一個RACSignal實例。你可以通過以下代碼來分解這個管道的幾個環(huán)節(jié):

RACSignal *usernameSourceSignal = 
    self.usernameTextField.rac_textSignal;
 
RACSignal *filteredUsername = [usernameSourceSignal  
  filter:^BOOL(id value) {
    NSString *text = value;
    return text.length > 3;
  }];
 
[filteredUsername subscribeNext:^(id x) {
  NSLog(@"%@", x);
}];

正因為每個對RACSignal的操作都同樣返回一個RACSignal實例,這種被稱為流式接口(fluent interface)的特性讓你可以構(gòu)造一個連續(xù)的管道而不必用本地變量分割每一步操作。

注意:ReactiveCocoa大量的使用了block。如果你對block并不熟悉,你可能需要閱讀蘋果官方的Blocks Programming Topics文檔。如果你像我一樣已經(jīng)對block相當熟悉,但是難以記住相關(guān)的語法,這個有趣的網(wǎng)站f*****gblocksyntax.com應(yīng)該能夠幫到你?。ū苊饷胺?,我們屏蔽了相關(guān)單詞,但這個鏈接還是相當實用的。)

簡單轉(zhuǎn)換

如果你剛才把代碼分解成數(shù)個RACSignal,現(xiàn)在就需要將它恢復(fù)成流式語法:

[[self.usernameTextField.rac_textSignal
  filter:^BOOL(id value) {
    NSString *text = value; // implicit cast
    return text.length > 3;
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  }];

在上面注釋所在的地方,id類型被隱式轉(zhuǎn)換為NSString類型,這樣做并不簡練。但幸運的是,由于這個block接收的值總為NSString類型,所以你可以直接改變它的入?yún)㈩愋?。更新代碼如下:

[[self.usernameTextField.rac_textSignal
  filter:^BOOL(NSString *text) {
    return text.length > 3;
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  }];

編譯運行,確保運行結(jié)果跟先前的一致。

什么是事件(Event)

目前為止本教程提到了幾種不同的事件類型,但未曾詳述這些事件的結(jié)構(gòu)。而實際上,事件中可以包含任何類型的值!

為了證明這個觀點,你需要往先前的管道中添加新的操作。更新viewDidLoad中你的代碼如下:

[[[self.usernameTextField.rac_textSignal
  map:^id(NSString *text) {
    return @(text.length);
  }]
  filter:^BOOL(NSNumber *length) {
    return [length integerValue] > 3;
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  }];

編譯運行后,此時控制臺打印的是文本的長度而非文本內(nèi)容:

2013-12-26 12:06:54.566 RWReactivePlayground[10079:a0b] 4
2013-12-26 12:06:54.725 RWReactivePlayground[10079:a0b] 5
2013-12-26 12:06:54.853 RWReactivePlayground[10079:a0b] 6
2013-12-26 12:06:55.061 RWReactivePlayground[10079:a0b] 7
2013-12-26 12:06:55.197 RWReactivePlayground[10079:a0b] 8
2013-12-26 12:06:55.300 RWReactivePlayground[10079:a0b] 9
2013-12-26 12:06:55.462 RWReactivePlayground[10079:a0b] 10
2013-12-26 12:06:55.558 RWReactivePlayground[10079:a0b] 11
2013-12-26 12:06:55.646 RWReactivePlayground[10079:a0b] 12

新添加的map方法通過提供的block對事件中的數(shù)據(jù)進行了轉(zhuǎn)換。其對每一個接收到的事件都通過提供的block進行處理,并將返回值作為next事件發(fā)送出去。在上面的代碼中,map方法就得到了NSString類型的輸入并獲取到它的長度,之后轉(zhuǎn)換為NSNumber類型返回給下一步。

我們可以結(jié)合下圖進一步理解它是如何運作的:


如你所見,在map方法之后的所有環(huán)節(jié)現(xiàn)在都接收到NSNumber類型的實例。你可以使用map方法將接收到的數(shù)據(jù)轉(zhuǎn)換成任何你想要的類型,只要轉(zhuǎn)換的目標是一個對象。

注意:在上述例子中,text.length的類型是原始類型NSUInteger。為了將其作為事件的內(nèi)容,必須對其進行封裝(boxed)。幸而Objective-C 已經(jīng)提供了一個語法糖去實現(xiàn)這一個功能——@(text.length)。

游樂時間到此結(jié)束了!是時候運用你目前為止學(xué)到的概念對ReactivePlaygroundapp的代碼進行更新。你可以先把你從開始學(xué)習(xí)本教程后添加的代碼統(tǒng)統(tǒng)清除掉。

創(chuàng)建狀態(tài)驗證信號

你首先要做的是創(chuàng)建兩個信號用以驗證用戶名跟密碼是否有效。在RWViewController.mviewDidLoad方法末添加如下代碼:

RACSignal *validUsernameSignal =
  [self.usernameTextField.rac_textSignal
    map:^id(NSString *text) {
      return @([self isValidUsername:text]);
    }];
 
RACSignal *validPasswordSignal =
  [self.passwordTextField.rac_textSignal
    map:^id(NSString *text) {
      return @([self isValidPassword:text]);
    }];

上述代碼使用map方法對兩個文本輸入框的rac_textSignal進行轉(zhuǎn)換。輸出為一個封裝成NSNumber的布爾值。

下一步要做的是將這些信號繼續(xù)轉(zhuǎn)換,為文本輸入框提供一個合適的背景色。基本上來說,你可以訂閱這個信號并將結(jié)果直接應(yīng)用到文本輸入框上更新背景色。其中一種可行的實現(xiàn)如下:

[[validPasswordSignal
  map:^id(NSNumber *passwordValid) {
    return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    self.passwordTextField.backgroundColor = color;
  }];

(請先不要如上添加代碼,更加優(yōu)雅的實現(xiàn)還在后頭?。?/em>

從概念上講,你的目的是將這個信號的輸出直接賦值給文本輸入框的backgroundColor屬性。然而上面代碼是一種相當糟糕的實現(xiàn),已經(jīng)完全落后了!

很幸運,ReactiveCocoa提供了一個宏讓你可以優(yōu)雅地實現(xiàn)這一點。直接在viewDidLoad的兩個信號下添加如下代碼:

RAC(self.passwordTextField, backgroundColor) =
  [validPasswordSignal
    map:^id(NSNumber *passwordValid) {
      return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
    }];
 
RAC(self.usernameTextField, backgroundColor) =
  [validUsernameSignal
    map:^id(NSNumber *passwordValid) {
     return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
    }];

RAC宏將一個信號的輸出和一個對象的屬性綁定起來。宏接受兩個參數(shù),第一個是包含改變屬性的對象,第二個為屬性的名字。每次當信號發(fā)出一個新的事件,事件的值就會傳遞給綁定的屬性。

這難道不是一個相當優(yōu)雅的解決方案么?

在編譯運行前,找到updateUIState方法并移除頭兩行代碼:

self.usernameTextField.backgroundColor = self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor];
self.passwordTextField.backgroundColor = self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];

去掉原來的非響應(yīng)式代碼。

這時再編譯運行。文本輸入框就會在內(nèi)容無效的情況下高亮,在有效時底色又變回透明。

由于圖像更有助理解,我們把現(xiàn)在的邏輯圖像化。在下圖你可以看到兩個獲取文本信號的管道,先把他們映射為檢驗有效性的布爾值,然后再映射為與文本輸入框的底色屬性綁定的UIColor


你是否對單獨創(chuàng)建validPasswordSignalvalidUsernameSignal信號感到不解?為什么不直接為文本輸入框創(chuàng)建一個連續(xù)流暢的管道呢?親愛的讀者耐心點,這瘋狂舉動背后的真正目的將立馬揭曉!

信號合成

在當前App,登錄按鈕設(shè)定只有在用戶名和密碼都有效輸入時才能使用。我們現(xiàn)在要做的就是用響應(yīng)式來重實現(xiàn)這個功能!

現(xiàn)在的代碼已經(jīng)實現(xiàn)了兩個發(fā)送布爾值的信號validUsernameSignalvalidPasswordSignal來對用戶名和密碼的輸入進行驗證。接下來的任務(wù)就是合成這兩個信號,用以共同決定登錄按鈕是否可用。

viewDidLoad的末端添加如下代碼:

RACSignal *signUpActiveSignal =
  [RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
                    reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid) {
                      return @([usernameValid boolValue] && [passwordValid boolValue]);
                    }];

上面的代碼使用combineLatest:reduce:方法獲取validUsernameSignalvalidPasswordSignal的最近一個信號值并組合成一個全新的信號。每當兩個源信號的其中一個發(fā)送新值,reduce里的block代碼塊就會執(zhí)行,其返回的值會作為合成信號的值發(fā)送出去。

注意:RACSignal合成方法可以合成任意數(shù)量的信號,而reduce block的入?yún)⒑驮葱盘栆灰粚?yīng)。ReactiveCocoa有一個巧妙的工具類RACBlockTrampoline,用以內(nèi)部處理reduce block的可變?nèi)雲(yún)⒘斜怼嶋H上,ReactiveCocoa的實現(xiàn)上還隱藏著很多精妙的技巧,非常值得我們深入探究!

現(xiàn)在你已經(jīng)有一個合適的信號了,在viewDidLoad的末端添加以下代碼。把信號與按鈕的可用屬性關(guān)聯(lián)起來。

[signUpActiveSignal subscribeNext:^(NSNumber *signupActive) {
   self.signInButton.enabled = [signupActive boolValue];
 }];

在運行這代碼前,先把舊的實現(xiàn)去除。把文件頂部的這兩個屬性刪掉:

@property (nonatomic) BOOL passwordIsValid;
@property (nonatomic) BOOL usernameIsValid;

再把viewDidLoad的以下代碼刪掉:

// handle text changes for both text fields
[self.usernameTextField addTarget:self
                           action:@selector(usernameTextFieldChanged)
                 forControlEvents:UIControlEventEditingChanged];
[self.passwordTextField addTarget:self 
                           action:@selector(passwordTextFieldChanged)
                 forControlEvents:UIControlEventEditingChanged];

同時也去除updateUIState,usernameTextFieldChangedpasswordTextFieldChanged`方法。哇!你剛剛可是刪除了一大堆非響應(yīng)式的代碼??!你之后絕對會慶幸你做的一切的。

最后,也別忘了從viewDidLoad中移除updateUIState的調(diào)用。

此時如果你編譯運行,并留意一下登錄按鈕。就會發(fā)現(xiàn)它像原來一樣只有在用戶名和密碼有效的情況下有效。

更新應(yīng)用的邏輯圖如下:


這體現(xiàn)了兩個非常重要的概念,你可以憑借這兩點用ReactiveCocoa實現(xiàn)一些相當強大的功能。

  • 拆分:信號可以有多個訂閱者和作為多個管道后續(xù)環(huán)節(jié)的來源。在上圖中,驗證賬號和密碼的布爾值信號就被單獨拆分,并用于兩個不同的方面。
  • 組合:多個信號可以組合為全新的信號。在這個例子中,兩個布爾值信號被組合到了一起。但不局限于此,實際上你可以組合任意值類型的信號。

這些改動讓應(yīng)用不再需要保留用以記錄兩個文本輸入框是否有效的私有屬性。這是你選用響應(yīng)式編程的其中一個關(guān)鍵差異——你不再需要使用實例變量去記錄這些瞬時狀態(tài)。

響應(yīng)式登錄

應(yīng)用現(xiàn)在已經(jīng)如上圖說明那樣的響應(yīng)式管道去管理文本輸入框和按鈕的狀態(tài)。但是,按鈕的點擊事件任然是使用action機制,所以下一步我們就把剩下的應(yīng)用邏輯完全用響應(yīng)式操作替換掉!

登錄按鈕的觸摸事件(Touch Up Inside event)是在storyboard中與RWViewController.msignInButtonTouched方法進行綁定的。由于你接下來需要使用響應(yīng)式的等效實現(xiàn)對其進行替換,所以你首先要做的就是斷開storyboard現(xiàn)有的綁定。

打開Main.storyboard,按著ctrl同時點擊登錄按鈕,喚起outlet / action關(guān)聯(lián)視圖并點擊x刪除關(guān)聯(lián)。如果你感到迷惑,下圖為你標記了刪除按鈕所在:


你已經(jīng)見識過ReactiveCocoa框架是怎樣在UIKit控件中添加屬性和方法的了。至今為止你使用過當文本改變時發(fā)送事件的rac_textSignal。為了處理相關(guān)事件,你現(xiàn)在需要使用另一個ReactiveCocoa添加到UIKit的方法:rac_signalForControlEvents。

回到RWViewController.m,在viewDidLoad末端添加以下代碼:

[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   subscribeNext:^(id x) {
     NSLog(@"button clicked");
   }];

上面的代碼基于按鈕的UIControlEventTouchUpInside事件創(chuàng)建了一個信號并添加了訂閱,每當這個事件觸發(fā)時就會進行日志打印。

編譯運行以確認日志信息真能打印出來。記住按鈕只有在用戶名和密碼都有效時才能使用,所以在點擊前記得往兩個文本輸入框中輸入一些內(nèi)容。

你會在Xcode的控制臺看到相似如下的信息:

2013-12-28 08:05:10.816 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:11.675 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.605 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.766 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.917 RWReactivePlayground[18203:a0b] button clicked

現(xiàn)在登錄按鈕已經(jīng)有了一個點擊事件的信號,下一步就是將它與登錄過程關(guān)聯(lián)起來。那么問題來了——但這是好事,你也無懼任何困難對嗎?打開RWDummySignInService.h并觀察提供的接口:

typedef void (^RWSignInResponse)(BOOL);
 
@interface RWDummySignInService : NSObject
 
- (void)signInWithUsername:(NSString *)username
                  password:(NSString *)password 
                  complete:(RWSignInResponse)completeBlock;
 
@end

這個服務(wù)接收用戶名,密碼和一個block作為參數(shù)。提供的block在登錄成功或失敗后執(zhí)行。你可以直接在subscribeNext:的block種使用這個接口,但為什么要這樣做?處理這種異步的、事件推動的行為對ReactiveCocoa來說簡直是小菜一碟!

注意:為了簡化,這個教程中使用了一個假的服務(wù),所以你不需要依賴于任何外部的接口。那么問題來了,怎樣在不使用信號的情況下使用API呢?(譯注:原文為“However, you’ve now run up against a very real problem, how do you use APIs not expressed in terms of signals?”語義貌似與前后文不符,翻譯時相當有些困惑,歡迎大家指教)

創(chuàng)建信號

很幸運,現(xiàn)有的異步API很容易就能改造成信號的形式。首先,從RWViewController.m文件移除signInButtonTouched: method方法。因為之后會有等效的響應(yīng)式實現(xiàn),所以這不再需要了。

繼續(xù)在RWViewController.m添加以下方法:

-(RACSignal *)signInSignal {
  return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [self.signInService
     signInWithUsername:self.usernameTextField.text
     password:self.passwordTextField.text
     complete:^(BOOL success) {
       [subscriber sendNext:@(success)];
       [subscriber sendCompleted];
     }];
    return nil;
  }];
}

上面的方法用用戶名和密碼創(chuàng)建了一個信號?,F(xiàn)在先來分析一下它的組成部分。

上面的代碼使用RACSignalcreateSignal:方法創(chuàng)建信號。描述這個信號的block是這個方法唯一的入?yún)ⅰ.斶@個信號有訂閱者的時候,block中的代碼就會執(zhí)行。

這個block傳入了一個遵守RACSubscriber協(xié)議的subscriber實例,實例中包含發(fā)送事件的方法;你可以發(fā)送任意數(shù)量的事件到下一環(huán)節(jié),也可以通過error或者complete事件終止信號。在這個例子中,subscriber實例發(fā)送了表示登錄結(jié)果的next事件,緊跟一個complete事件。

這個block的返回類型是一個RACDisposable對象,這讓你可以處理一些可能需要的清除工作,比如當一個訂閱被取消或廢棄的時候。由于這個信號并不需要作清除處理,所以直接結(jié)果返回nil。

如你所見,把一個異步API包裝到信號中去是出乎意料的簡單??!

現(xiàn)在去使用一下這個新信號。更新在上一部分你添加到viewDidLoad末端的代碼如下:

[[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   map:^id(id x) {
     return [self signInSignal];
   }]
   subscribeNext:^(id x) {
     NSLog(@"Sign in result: %@", x);
   }];

上面的代碼使用早前使用過的map方法將按鈕點擊信號轉(zhuǎn)換為登錄信號。然后訂閱者簡單地打印了結(jié)果。

如果編譯運行并點擊登錄事件,你就會在Xcode的控制臺看到上面代碼的運行結(jié)果……
跟你想象中的大相徑庭!

2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign in result:
                                   <RACDynamicSignal: 0xa068a00> name: +createSignal:

subscribeNext的block的確已經(jīng)接收了一個信號,但是并不是登錄信號的結(jié)果!

是時候分析一下這個管道好讓你明白中間發(fā)生了什么了:


當你點擊按鈕的時候,rac_signalForControlEvents發(fā)送了一個next事件(源按鈕作為事件的值)。之后在map方法中創(chuàng)建并返回了登錄信號,意味著接下來的管道環(huán)節(jié)現(xiàn)在接受到的是一個RACSignal。也就是你在subscribeNext:環(huán)節(jié)所獲取到的。

上面的狀況有時被稱為信號里的信號(signal of signals);換句話說就是一個外部的信號包含著一個內(nèi)部的信號。如果你真的想這么做的話,你可以在外部信號的subscribeNext:block中訂閱內(nèi)部的信號。但這想必會讓代碼變得相當混亂。幸而這是一個常見問題,ReactiveCocoa已經(jīng)為這種情形提供了解決方法。

信號里的信號

這種問題的解決方法相當直接,只要把map方法像下面一樣替換成flattenMap方法就可以了:

[[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   flattenMap:^id(id x) {
     return [self signInSignal];
   }]
   subscribeNext:^(id x) {
     NSLog(@"Sign in result: %@", x);
   }];

這把按鈕的點擊事件像之前一樣映射給了登錄信號,但也將內(nèi)部信號的事件發(fā)送給了外部信號。
編譯運行并留意控制臺。這時打印的應(yīng)該是登錄是否成功了:

2013-12-28 18:20:08.156 RWReactivePlayground[22993:a0b] Sign in result: 0
2013-12-28 18:25:50.927 RWReactivePlayground[22993:a0b] Sign in result: 1

多么令人振奮?。?/p>

現(xiàn)在這管道已經(jīng)想你期望一樣運作了,最后一步需要做的就是在subscribeNext添加登錄成功后的導(dǎo)航邏輯。替換代碼如下:

[[[self.signInButton
  rac_signalForControlEvents:UIControlEventTouchUpInside]
  flattenMap:^id(id x) {
    return [self signInSignal];
  }]
  subscribeNext:^(NSNumber *signedIn) {
    BOOL success = [signedIn boolValue];
    self.signInFailureText.hidden = success;
    if (success) {
      [self performSegueWithIdentifier:@"signInSuccess" sender:self];
    }
  }];

subscribeNext:block中獲取了登錄信號的結(jié)果,根據(jù)結(jié)果更新signInFailureText文本框的內(nèi)容,同時按需導(dǎo)航到下一頁面。編譯運行,再次欣賞一下這可愛的貓咪吧!

你是否注意到現(xiàn)在的應(yīng)用有一個小小的用戶習(xí)慣問題?當?shù)卿浄?wù)驗證時,登錄按鈕應(yīng)當是不可用的。這能夠避免用戶重復(fù)登錄。而且,如果登錄失敗了,當用戶再次嘗試登錄時錯誤信息應(yīng)當隱藏起來。

但是怎樣添加這些邏輯到現(xiàn)有的管道中呢?影響按鈕可用狀態(tài)的跟改變,過濾或者其他你現(xiàn)今遇到過的概念掛不上勾。相對的,這其實是一種副作用,或者說是當新的事件發(fā)生時你希望管道執(zhí)行的邏輯,而那并不改變對應(yīng)事件的本質(zhì)。

添加副作用

替換現(xiàn)有代碼如下:

[[[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   doNext:^(id x) {
     self.signInButton.enabled = NO;
     self.signInFailureText.hidden = YES;
   }]
   flattenMap:^id(id x) {
     return [self signInSignal];
   }]
   subscribeNext:^(NSNumber *signedIn) {
     self.signInButton.enabled = YES;
     BOOL success = [signedIn boolValue];
     self.signInFailureText.hidden = success;
     if (success) {
       [self performSegueWithIdentifier:@"signInSuccess" sender:self];
     }
   }];

你可以看到上面在按鈕點擊事件創(chuàng)建后添加了一個doNext:環(huán)節(jié)。注意doNext:是一個副作用,所以block沒有返回任何值;它并不影響事件的內(nèi)容。

doNext:的block中把按鈕的的可用屬性設(shè)為NO,同時隱藏了失敗文本。直到subscribeNext:的block中按鈕才再次變?yōu)榭捎?,并根?jù)登錄的結(jié)果決定顯示或隱藏失敗文本。

更新包含了副作用的管道圖表如下。


編譯運行應(yīng)用,確保登錄按鈕像預(yù)想一樣切換可用與不可用的狀態(tài)。

而到了這里,你的工作已經(jīng)完成了——應(yīng)用已經(jīng)完全實現(xiàn)了響應(yīng)式重構(gòu)。

如果你在過程中感到疑惑,你可以下載最終的項目(包含了框架引用),你也可以從GitHub上獲取代碼,那里有對應(yīng)教程中每一步操作的提交記錄。

注意:在異步操作時禁用按鈕也是一個很常見的問題,ReactiveCocoa同樣給出了解決方案。RACCommand封裝了這個概念,通過一個可用信號去關(guān)聯(lián)按鈕的可用屬性。你回頭也可以試試使用這個類。

最后

希望這篇教程能夠為你在自己的項目上使用ReactiveCocoa建立了一個良好的基礎(chǔ)。要熟練運用這些概念還需要一定的練習(xí),但如同其他編程語言一樣,一旦你掌握了,也不過如此。ReactiveCocoa最為核心的就是信號,說白了也就是事件流。還有比這個更加簡單的么?

同時我發(fā)現(xiàn)在ReactiveCocoa中有趣的是,你能使用好幾種方式去解決同一個問題。你可以試試在這個應(yīng)用中實踐一下,自己調(diào)整信號與管道的拆分與組合來達到相同的效果。

謹記使用ReactiveCocoa最重要的目的是使你的代碼更加整潔和易懂。就我個人而言,使用清晰的管道和流式語法能讓我更容易理解應(yīng)用的工作流程。

在本系列教程的下半部,你會學(xué)到一些更深層的主題,比如錯誤的處理和在其他線程中執(zhí)行代碼。而在那之前,就先愉快地實踐一下暫時所學(xué)吧!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容