原文: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,error 和 completed。一個信號在因為報錯或完成的終止前可以發(fā)送若干個事件。在教程的上半部分將會把重點放在next事件上。你可以在教程的下半部學(xué)習(xí)有關(guān)error 和 completed的知識。
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.m的viewDidLoad方法末添加如下代碼:
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)建
validPasswordSignal和validUsernameSignal信號感到不解?為什么不直接為文本輸入框創(chuàng)建一個連續(xù)流暢的管道呢?親愛的讀者耐心點,這瘋狂舉動背后的真正目的將立馬揭曉!
信號合成
在當前App,登錄按鈕設(shè)定只有在用戶名和密碼都有效輸入時才能使用。我們現(xiàn)在要做的就是用響應(yīng)式來重實現(xiàn)這個功能!
現(xiàn)在的代碼已經(jīng)實現(xiàn)了兩個發(fā)送布爾值的信號validUsernameSignal 和 validPasswordSignal來對用戶名和密碼的輸入進行驗證。接下來的任務(wù)就是合成這兩個信號,用以共同決定登錄按鈕是否可用。
在viewDidLoad的末端添加如下代碼:
RACSignal *signUpActiveSignal =
[RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid) {
return @([usernameValid boolValue] && [passwordValid boolValue]);
}];
上面的代碼使用combineLatest:reduce:方法獲取validUsernameSignal和validPasswordSignal的最近一個信號值并組合成一個全新的信號。每當兩個源信號的其中一個發(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,usernameTextFieldChanged和passwordTextFieldChanged`方法。哇!你剛剛可是刪除了一大堆非響應(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.m的signInButtonTouched方法進行綁定的。由于你接下來需要使用響應(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)在先來分析一下它的組成部分。
上面的代碼使用RACSignal的createSignal:方法創(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é)吧!