原文:ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2
下半部翻譯:ReactiveCocoa教程:下半部【翻譯】
譯注:在學(xué)習(xí)ReactiveCocoa的過(guò)程中看了不少文章,但對(duì)我而言最適合入門的當(dāng)數(shù)此系列。
雖然已經(jīng)是14年的文章,但基本對(duì)于ReactiveCocoa在OC上的使用仍然基本符合。這上下兩篇文章最好之處在于通過(guò)兩個(gè)應(yīng)用實(shí)例囊括了大部分ReactiveCocoa的常見(jiàn)用法,雖然沒(méi)有提及RACCommand
具體用法,也瑕不掩瑜。
為便于之后自己查看,同時(shí)加深理解,故對(duì)文章作簡(jiǎn)單翻譯。因?yàn)榈谝淮巫龇g,如果翻譯中有不妥之處,敬請(qǐng)諒解,亦歡迎大家交流:)
作為一個(gè)iOS開(kāi)發(fā)人員,幾乎你寫(xiě)的每一行代碼都是為了處理各種事件:按鈕的點(diǎn)擊、網(wǎng)絡(luò)的反饋、屬性的變更(KVO)、位置的改變(CoreLocation)等等。但與此同時(shí),這些事件相應(yīng)又有著如action、delegate、KVO、callback等各種不一樣的實(shí)現(xiàn)。ReactiveCocoa為此定義了一套接口與事件的標(biāo)準(zhǔn),以便于用戶憑借一套基礎(chǔ)的工具對(duì)這些事件進(jìn)行鏈接,過(guò)濾及重組。
看到這里,你是感到迷惑?興奮?還是迫不及待?那就繼續(xù)往下讀吧 :]
ReactiveCocoa是如下兩種編程風(fēng)格的集合體:
- 使用高階函數(shù)的函數(shù)式編程,如:以某一函數(shù)作為另一函數(shù)的入?yún)?/li>
- 注重?cái)?shù)據(jù)流及變更傳遞的響應(yīng)式編程
故而ReactiveCocoa被稱為響應(yīng)式函數(shù)編程(Functional Reactive Programming 或FRP)框架。
讀到這里你可能感到相當(dāng)迷惑,但請(qǐng)放心,解惑這正是本教程的目的!雖然編程范式也是一個(gè)引人入勝的話題,但相比學(xué)術(shù)理論,本教程余下的部分,將希望通過(guò)完成一個(gè)實(shí)例讓你更好的理解ReactiveCocoa。
響應(yīng)式樂(lè)園
在本教程中,你將通過(guò)一個(gè)非常簡(jiǎn)單的應(yīng)用實(shí)例學(xué)習(xí)響應(yīng)式編程。在開(kāi)始之前,作為準(zhǔn)備工作,請(qǐng)先下載初始工程并編譯運(yùn)行。
ReactivePlayground是一個(gè)包含用戶登錄界面的簡(jiǎn)單應(yīng)用。在用戶名及密碼校驗(yàn)成功后,你將看到一只非常可愛(ài)的小貓。
接下來(lái)我們最好花點(diǎn)時(shí)間去熟悉一下初始工程的源碼。由于工程相當(dāng)?shù)暮?jiǎn)單,所以這不會(huì)花費(fèi)太多的時(shí)間。
打開(kāi)查看RWViewController.m
文件。嘗試回答以下問(wèn)題:點(diǎn)擊Sign In按鈕會(huì)觸發(fā)哪些事件?顯示/隱藏登錄失敗的文本需要哪些條件?在這個(gè)簡(jiǎn)單例子中,你可能只需一兩分鐘就能解答這些問(wèn)題。但如果情況復(fù)雜一點(diǎn),你就可能需要花比較多的的時(shí)間去做相同的分析。
而使用ReactiveCocoa的話,你就能夠更清晰地理解應(yīng)用實(shí)際的意圖了。事不宜遲,讓我們現(xiàn)在立馬開(kāi)始吧!
添加 ReactiveCocoa 框架
往項(xiàng)目中添加ReactiveCocoa框架最簡(jiǎn)單的途徑是使用CocoaPods。如果你之前從來(lái)沒(méi)有使用過(guò)CocoaPods,你最好學(xué)習(xí)一下CocoaPods官網(wǎng)上的教程,或者至少跟著官網(wǎng)教程的初始步驟操作一遍,來(lái)保證你已經(jīng)達(dá)到了安裝的先決條件。
注意:如果基于某些原因你實(shí)在不想使用CocoaPods的話,只要你跟隨ReactiveCocoa GitHub上文檔進(jìn)行導(dǎo)入操作,也同樣可以使用ReactiveCocoa。
如果你還打開(kāi)著ReactivePlayground
項(xiàng)目的話,現(xiàn)在請(qǐng)先關(guān)閉。CocoaPods會(huì)創(chuàng)建一個(gè)Xcode workspace文件,用以替代原有的項(xiàng)目文件。
打開(kāi)終端。跳轉(zhuǎn)到你項(xiàng)目所在的文件夾,并輸入以下指令:
touch Podfile
open -e Podfile
這創(chuàng)建了一個(gè)名為Podfile
的文件并通過(guò)TextEdit
打開(kāi)。然后復(fù)制粘貼以下文本到TextEdit
窗口中:(譯注:使用Sublime等文本編輯器亦可,不過(guò)不要使用系統(tǒng)自帶的文本編輯器)
platform :ios, '7.0'
pod 'ReactiveCocoa', '2.1.8'
上面文本規(guī)定了使用平臺(tái)為iOS,最小的SDK版本號(hào)為7.0,并添加ReactiveCocoa框架作為依賴包。文檔保存后,請(qǐng)返回終端窗口,并輸入以下命令:(譯注:ReactiveCocoa2.X為OC版本,之后包括最新的版本都基于Swift)
pod install
你將會(huì)會(huì)看到與下文相似的輸出:
Analyzing dependencies
Downloading dependencies
Installing ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project
[!] From now on use `RWReactivePlayground.xcworkspace`.
這說(shuō)明ReactiveCocoa已經(jīng)成功下載,而且CocoaPods已經(jīng)創(chuàng)建好了Xcode workspace文件用以整合相關(guān)的框架到你現(xiàn)有的應(yīng)用中。打開(kāi)新生成的workspace文件RWReactivePlayground.xcworkspace
,瀏覽由CocoaPods在項(xiàng)目導(dǎo)航中生成的結(jié)構(gòu):
CocoaPods創(chuàng)建了一個(gè)新的工作空間,并在原始項(xiàng)目RWReactivePlayground
外添加了包含有ReactiveCocoa 框架的Pods
項(xiàng)目。CocoaPods就這樣輕而易舉地達(dá)到了管理依賴包的目的!
你想必已經(jīng)注意到個(gè)項(xiàng)目叫做響應(yīng)式樂(lè)園
(ReactivePlayground),那么接下來(lái),自然就是我們的游樂(lè)時(shí)間了……
游樂(lè)時(shí)間
正如在介紹中提到的,ReactiveCocoa提供了一套接口標(biāo)準(zhǔn)來(lái)處理應(yīng)用中出現(xiàn)的各式事件流。這在ReactiveCocoa的術(shù)語(yǔ)中被稱作信號(hào),對(duì)應(yīng)的是RACSignal
類。
下面打開(kāi)應(yīng)用的初始視圖控制器RWViewController.m
,在文件開(kāi)頭添加以下代碼來(lái)導(dǎo)入ReactiveCocoa的頭文件:
#import <ReactiveCocoa/ReactiveCocoa.h>
你暫時(shí)還不需要替換現(xiàn)有的代碼,現(xiàn)在要做的只是在原有的代碼基礎(chǔ)上豐富一下。先在viewDidLoad
方法的結(jié)尾處添加下面代碼:
[self.usernameTextField.rac_textSignal subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
編譯運(yùn)行應(yīng)用,在用戶名輸入框中隨意輸入點(diǎn)文本。留意一下控制臺(tái)你就能發(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?
你會(huì)發(fā)現(xiàn)每次你修改文本框中的輸入,block中的代碼塊就會(huì)執(zhí)行。不需要target-action,不需要delegate——只憑借信號(hào)和block就能實(shí)現(xiàn)。這多棒!
ReactiveCocoa的信號(hào)(表現(xiàn)為RACSignal類)會(huì)向他的訂閱者們發(fā)送事件流。發(fā)送的事件分為三種類型:next
,error
和 completed
。一個(gè)信號(hào)在因?yàn)閳?bào)錯(cuò)或完成的終止前可以發(fā)送若干個(gè)事件。在教程的上半部分將會(huì)把重點(diǎn)放在next
事件上。你可以在教程的下半部學(xué)習(xí)有關(guān)error
和 completed
的知識(shí)。
RACSignal
提供了若干的方法用以訂閱這些不同的事件類型。每個(gè)方法都會(huì)接受一個(gè)或多個(gè)block作為入?yún)ⅲ?dāng)新的事件出現(xiàn)時(shí),block中的邏輯代碼就會(huì)執(zhí)行。在這個(gè)例子中,你可以看到subscribeNext:
方法就提供了一個(gè)block,在每個(gè)next
事件到達(dá)時(shí)執(zhí)行里面的代碼。
ReactiveCocoa框架使用category為很多標(biāo)準(zhǔn)的UIKit控件添加了信號(hào),借此你可以對(duì)它們的各種事件進(jìn)行訂閱。這正是文本輸入框的rac_textSignal
屬性的由來(lái)。
理論學(xué)習(xí)就到此為止,現(xiàn)在是時(shí)候使用ReactiveCocoa來(lái)實(shí)現(xiàn)一些真正的功能了!
ReactiveCocoa有多個(gè)用于操作事件流的方法。舉個(gè)例子,假如你僅僅關(guān)心長(zhǎng)度超過(guò)3字節(jié)的用戶名,你可以使用filter
方法來(lái)達(dá)到這個(gè)目的。更新你剛剛添加到viewDidLoad
方法的代碼如下:
[[self.usernameTextField.rac_textSignal
filter:^BOOL(id value) {
NSString *text = value;
return text.length > 3;
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
編譯運(yùn)行,并往文本輸入框中隨意輸入一些內(nèi)容,此時(shí)控制臺(tái)就只會(huì)打印文本長(zhǎng)度超過(guò)3個(gè)字節(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)建的是一個(gè)非常簡(jiǎn)單的管道(譯注:pipeline,指一個(gè)信號(hào)被訂閱的完整過(guò)程)。這正是響應(yīng)式編程的的本質(zhì),通過(guò)傳遞一系列的數(shù)據(jù)流實(shí)現(xiàn)你應(yīng)用中的功能。
下圖中描繪了整個(gè)流程:
可以看到,
rac_textSignal
是這個(gè)事件的源頭。之后數(shù)據(jù)流通過(guò)一個(gè)過(guò)濾器,只允許包含長(zhǎng)度超過(guò)3的的字符串的事件通過(guò)。符合條件的事件最后來(lái)到subscribeNext:
方法,事件傳遞的值在block中打印。
值得注意的是,filter
方法的返回值也是一個(gè)RACSignal
實(shí)例。你可以通過(guò)以下代碼來(lái)分解這個(gè)管道的幾個(gè)環(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);
}];
正因?yàn)槊總€(gè)對(duì)RACSignal
的操作都同樣返回一個(gè)RACSignal
實(shí)例,這種被稱為流式接口(fluent interface)的特性讓你可以構(gòu)造一個(gè)連續(xù)的管道而不必用本地變量分割每一步操作。
注意:ReactiveCocoa大量的使用了block。如果你對(duì)block并不熟悉,你可能需要閱讀蘋(píng)果官方的Blocks Programming Topics文檔。如果你像我一樣已經(jīng)對(duì)block相當(dāng)熟悉,但是難以記住相關(guān)的語(yǔ)法,這個(gè)有趣的網(wǎng)站f*****gblocksyntax.com應(yīng)該能夠幫到你!(避免冒犯,我們屏蔽了相關(guān)單詞,但這個(gè)鏈接還是相當(dāng)實(shí)用的。)
簡(jiǎn)單轉(zhuǎn)換
如果你剛才把代碼分解成數(shù)個(gè)RACSignal
,現(xiàn)在就需要將它恢復(fù)成流式語(yǔ)法:
[[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
類型,這樣做并不簡(jiǎn)練。但幸運(yùn)的是,由于這個(gè)block接收的值總為NSString
類型,所以你可以直接改變它的入?yún)㈩愋汀8麓a如下:
[[self.usernameTextField.rac_textSignal
filter:^BOOL(NSString *text) {
return text.length > 3;
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
編譯運(yùn)行,確保運(yùn)行結(jié)果跟先前的一致。
什么是事件(Event)
目前為止本教程提到了幾種不同的事件類型,但未曾詳述這些事件的結(jié)構(gòu)。而實(shí)際上,事件中可以包含任何類型的值!
為了證明這個(gè)觀點(diǎn),你需要往先前的管道中添加新的操作。更新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);
}];
編譯運(yùn)行后,此時(shí)控制臺(tái)打印的是文本的長(zhǎng)度而非文本內(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方法通過(guò)提供的block對(duì)事件中的數(shù)據(jù)進(jìn)行了轉(zhuǎn)換。其對(duì)每一個(gè)接收到的事件都通過(guò)提供的block進(jìn)行處理,并將返回值作為next
事件發(fā)送出去。在上面的代碼中,map方法就得到了NSString
類型的輸入并獲取到它的長(zhǎng)度,之后轉(zhuǎn)換為NSNumber
類型返回給下一步。
我們可以結(jié)合下圖進(jìn)一步理解它是如何運(yùn)作的:
如你所見(jiàn),在map
方法之后的所有環(huán)節(jié)現(xiàn)在都接收到NSNumber
類型的實(shí)例。你可以使用map
方法將接收到的數(shù)據(jù)轉(zhuǎn)換成任何你想要的類型,只要轉(zhuǎn)換的目標(biāo)是一個(gè)對(duì)象。
注意:在上述例子中,
text.length
的類型是原始類型NSUInteger
。為了將其作為事件的內(nèi)容,必須對(duì)其進(jìn)行封裝(boxed)。幸而Objective-C 已經(jīng)提供了一個(gè)語(yǔ)法糖去實(shí)現(xiàn)這一個(gè)功能——@(text.length)
。
游樂(lè)時(shí)間到此結(jié)束了!是時(shí)候運(yùn)用你目前為止學(xué)到的概念對(duì)ReactivePlayground
app的代碼進(jìn)行更新。你可以先把你從開(kāi)始學(xué)習(xí)本教程后添加的代碼統(tǒng)統(tǒng)清除掉。
創(chuàng)建狀態(tài)驗(yàn)證信號(hào)
你首先要做的是創(chuàng)建兩個(gè)信號(hào)用以驗(yàn)證用戶名跟密碼是否有效。在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
方法對(duì)兩個(gè)文本輸入框的rac_textSignal
進(jìn)行轉(zhuǎn)換。輸出為一個(gè)封裝成NSNumber
的布爾值。
下一步要做的是將這些信號(hào)繼續(xù)轉(zhuǎn)換,為文本輸入框提供一個(gè)合適的背景色。基本上來(lái)說(shuō),你可以訂閱這個(gè)信號(hào)并將結(jié)果直接應(yīng)用到文本輸入框上更新背景色。其中一種可行的實(shí)現(xiàn)如下:
[[validPasswordSignal
map:^id(NSNumber *passwordValid) {
return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
self.passwordTextField.backgroundColor = color;
}];
(請(qǐng)先不要如上添加代碼,更加優(yōu)雅的實(shí)現(xiàn)還在后頭!)
從概念上講,你的目的是將這個(gè)信號(hào)的輸出直接賦值給文本輸入框的backgroundColor
屬性。然而上面代碼是一種相當(dāng)糟糕的實(shí)現(xiàn),已經(jīng)完全落后了!
很幸運(yùn),ReactiveCocoa提供了一個(gè)宏讓你可以優(yōu)雅地實(shí)現(xiàn)這一點(diǎn)。直接在viewDidLoad
的兩個(gè)信號(hào)下添加如下代碼:
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
宏將一個(gè)信號(hào)的輸出和一個(gè)對(duì)象的屬性綁定起來(lái)。宏接受兩個(gè)參數(shù),第一個(gè)是包含改變屬性的對(duì)象,第二個(gè)為屬性的名字。每次當(dāng)信號(hào)發(fā)出一個(gè)新的事件,事件的值就會(huì)傳遞給綁定的屬性。
這難道不是一個(gè)相當(dāng)優(yōu)雅的解決方案么?
在編譯運(yùn)行前,找到updateUIState
方法并移除頭兩行代碼:
self.usernameTextField.backgroundColor = self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor];
self.passwordTextField.backgroundColor = self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];
去掉原來(lái)的非響應(yīng)式代碼。
這時(shí)再編譯運(yùn)行。文本輸入框就會(huì)在內(nèi)容無(wú)效的情況下高亮,在有效時(shí)底色又變回透明。
由于圖像更有助理解,我們把現(xiàn)在的邏輯圖像化。在下圖你可以看到兩個(gè)獲取文本信號(hào)的管道,先把他們映射為檢驗(yàn)有效性的布爾值,然后再映射為與文本輸入框的底色屬性綁定的UIColor
。
你是否對(duì)單獨(dú)創(chuàng)建
validPasswordSignal
和validUsernameSignal
信號(hào)感到不解?為什么不直接為文本輸入框創(chuàng)建一個(gè)連續(xù)流暢的管道呢?親愛(ài)的讀者耐心點(diǎn),這瘋狂舉動(dòng)背后的真正目的將立馬揭曉!
信號(hào)合成
在當(dāng)前App,登錄按鈕設(shè)定只有在用戶名和密碼都有效輸入時(shí)才能使用。我們現(xiàn)在要做的就是用響應(yīng)式來(lái)重實(shí)現(xiàn)這個(gè)功能!
現(xiàn)在的代碼已經(jīng)實(shí)現(xiàn)了兩個(gè)發(fā)送布爾值的信號(hào)validUsernameSignal
和 validPasswordSignal
來(lái)對(duì)用戶名和密碼的輸入進(jìn)行驗(yàn)證。接下來(lái)的任務(wù)就是合成這兩個(gè)信號(hào),用以共同決定登錄按鈕是否可用。
在viewDidLoad
的末端添加如下代碼:
RACSignal *signUpActiveSignal =
[RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid) {
return @([usernameValid boolValue] && [passwordValid boolValue]);
}];
上面的代碼使用combineLatest:reduce:
方法獲取validUsernameSignal
和validPasswordSignal
的最近一個(gè)信號(hào)值并組合成一個(gè)全新的信號(hào)。每當(dāng)兩個(gè)源信號(hào)的其中一個(gè)發(fā)送新值,reduce里的block代碼塊就會(huì)執(zhí)行,其返回的值會(huì)作為合成信號(hào)的值發(fā)送出去。
注意:
RACSignal
合成方法可以合成任意數(shù)量的信號(hào),而reduce block的入?yún)⒑驮葱盘?hào)一一對(duì)應(yīng)。ReactiveCocoa有一個(gè)巧妙的工具類RACBlockTrampoline
,用以內(nèi)部處理reduce block的可變?nèi)雲(yún)⒘斜怼?shí)際上,ReactiveCocoa的實(shí)現(xiàn)上還隱藏著很多精妙的技巧,非常值得我們深入探究!
現(xiàn)在你已經(jīng)有一個(gè)合適的信號(hào)了,在viewDidLoad
的末端添加以下代碼。把信號(hào)與按鈕的可用屬性關(guān)聯(lián)起來(lái)。
[signUpActiveSignal subscribeNext:^(NSNumber *signupActive) {
self.signInButton.enabled = [signupActive boolValue];
}];
在運(yùn)行這代碼前,先把舊的實(shí)現(xiàn)去除。把文件頂部的這兩個(gè)屬性刪掉:
@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];
同時(shí)也去除updateUIState
,usernameTextFieldChanged
和passwordTextFieldChanged`方法。哇!你剛剛可是刪除了一大堆非響應(yīng)式的代碼啊!你之后絕對(duì)會(huì)慶幸你做的一切的。
最后,也別忘了從viewDidLoad
中移除updateUIState
的調(diào)用。
此時(shí)如果你編譯運(yùn)行,并留意一下登錄按鈕。就會(huì)發(fā)現(xiàn)它像原來(lái)一樣只有在用戶名和密碼有效的情況下有效。
更新應(yīng)用的邏輯圖如下:
這體現(xiàn)了兩個(gè)非常重要的概念,你可以憑借這兩點(diǎn)用ReactiveCocoa實(shí)現(xiàn)一些相當(dāng)強(qiáng)大的功能。
- 拆分:信號(hào)可以有多個(gè)訂閱者和作為多個(gè)管道后續(xù)環(huán)節(jié)的來(lái)源。在上圖中,驗(yàn)證賬號(hào)和密碼的布爾值信號(hào)就被單獨(dú)拆分,并用于兩個(gè)不同的方面。
- 組合:多個(gè)信號(hào)可以組合為全新的信號(hào)。在這個(gè)例子中,兩個(gè)布爾值信號(hào)被組合到了一起。但不局限于此,實(shí)際上你可以組合任意值類型的信號(hào)。
這些改動(dòng)讓?xiě)?yīng)用不再需要保留用以記錄兩個(gè)文本輸入框是否有效的私有屬性。這是你選用響應(yīng)式編程的其中一個(gè)關(guān)鍵差異——你不再需要使用實(shí)例變量去記錄這些瞬時(shí)狀態(tài)。
響應(yīng)式登錄
應(yīng)用現(xiàn)在已經(jīng)如上圖說(shuō)明那樣的響應(yīng)式管道去管理文本輸入框和按鈕的狀態(tài)。但是,按鈕的點(diǎn)擊事件任然是使用action機(jī)制,所以下一步我們就把剩下的應(yīng)用邏輯完全用響應(yīng)式操作替換掉!
登錄按鈕的觸摸事件(Touch Up Inside event)是在storyboard中與RWViewController.m
的signInButtonTouched
方法進(jìn)行綁定的。由于你接下來(lái)需要使用響應(yīng)式的等效實(shí)現(xiàn)對(duì)其進(jìn)行替換,所以你首先要做的就是斷開(kāi)storyboard現(xiàn)有的綁定。
打開(kāi)Main.storyboard
,按著ctrl同時(shí)點(diǎn)擊登錄按鈕,喚起outlet / action關(guān)聯(lián)視圖并點(diǎn)擊x刪除關(guān)聯(lián)。如果你感到迷惑,下圖為你標(biāo)記了刪除按鈕所在:
你已經(jīng)見(jiàn)識(shí)過(guò)ReactiveCocoa框架是怎樣在UIKit控件中添加屬性和方法的了。至今為止你使用過(guò)當(dāng)文本改變時(shí)發(fā)送事件的
rac_textSignal
。為了處理相關(guān)事件,你現(xiàn)在需要使用另一個(gè)ReactiveCocoa添加到UIKit的方法:rac_signalForControlEvents
。
回到RWViewController.m
,在viewDidLoad
末端添加以下代碼:
[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
NSLog(@"button clicked");
}];
上面的代碼基于按鈕的UIControlEventTouchUpInside
事件創(chuàng)建了一個(gè)信號(hào)并添加了訂閱,每當(dāng)這個(gè)事件觸發(fā)時(shí)就會(huì)進(jìn)行日志打印。
編譯運(yùn)行以確認(rèn)日志信息真能打印出來(lái)。記住按鈕只有在用戶名和密碼都有效時(shí)才能使用,所以在點(diǎn)擊前記得往兩個(gè)文本輸入框中輸入一些內(nèi)容。
你會(huì)在Xcode的控制臺(tái)看到相似如下的信息:
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)有了一個(gè)點(diǎn)擊事件的信號(hào),下一步就是將它與登錄過(guò)程關(guān)聯(lián)起來(lái)。那么問(wèn)題來(lái)了——但這是好事,你也無(wú)懼任何困難對(duì)嗎?打開(kāi)RWDummySignInService.h
并觀察提供的接口:
typedef void (^RWSignInResponse)(BOOL);
@interface RWDummySignInService : NSObject
- (void)signInWithUsername:(NSString *)username
password:(NSString *)password
complete:(RWSignInResponse)completeBlock;
@end
這個(gè)服務(wù)接收用戶名,密碼和一個(gè)block作為參數(shù)。提供的block在登錄成功或失敗后執(zhí)行。你可以直接在subscribeNext:
的block種使用這個(gè)接口,但為什么要這樣做?處理這種異步的、事件推動(dòng)的行為對(duì)ReactiveCocoa來(lái)說(shuō)簡(jiǎn)直是小菜一碟!
注意:為了簡(jiǎn)化,這個(gè)教程中使用了一個(gè)假的服務(wù),所以你不需要依賴于任何外部的接口。那么問(wèn)題來(lái)了,怎樣在不使用信號(hào)的情況下使用API呢?(譯注:原文為“However, you’ve now run up against a very real problem, how do you use APIs not expressed in terms of signals?”語(yǔ)義貌似與前后文不符,翻譯時(shí)相當(dāng)有些困惑,歡迎大家指教)
創(chuàng)建信號(hào)
很幸運(yùn),現(xiàn)有的異步API很容易就能改造成信號(hào)的形式。首先,從RWViewController.m
文件移除signInButtonTouched: method
方法。因?yàn)橹髸?huì)有等效的響應(yīng)式實(shí)現(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)建了一個(gè)信號(hào)。現(xiàn)在先來(lái)分析一下它的組成部分。
上面的代碼使用RACSignal
的createSignal:
方法創(chuàng)建信號(hào)。描述這個(gè)信號(hào)的block是這個(gè)方法唯一的入?yún)ⅰ.?dāng)這個(gè)信號(hào)有訂閱者的時(shí)候,block中的代碼就會(huì)執(zhí)行。
這個(gè)block傳入了一個(gè)遵守RACSubscriber
協(xié)議的subscriber
實(shí)例,實(shí)例中包含發(fā)送事件的方法;你可以發(fā)送任意數(shù)量的事件到下一環(huán)節(jié),也可以通過(guò)error
或者complete
事件終止信號(hào)。在這個(gè)例子中,subscriber
實(shí)例發(fā)送了表示登錄結(jié)果的next
事件,緊跟一個(gè)complete
事件。
這個(gè)block的返回類型是一個(gè)RACDisposable
對(duì)象,這讓你可以處理一些可能需要的清除工作,比如當(dāng)一個(gè)訂閱被取消或廢棄的時(shí)候。由于這個(gè)信號(hào)并不需要作清除處理,所以直接結(jié)果返回nil
。
如你所見(jiàn),把一個(gè)異步API包裝到信號(hào)中去是出乎意料的簡(jiǎn)單啊!
現(xiàn)在去使用一下這個(gè)新信號(hào)。更新在上一部分你添加到viewDidLoad
末端的代碼如下:
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
map:^id(id x) {
return [self signInSignal];
}]
subscribeNext:^(id x) {
NSLog(@"Sign in result: %@", x);
}];
上面的代碼使用早前使用過(guò)的map
方法將按鈕點(diǎn)擊信號(hào)轉(zhuǎn)換為登錄信號(hào)。然后訂閱者簡(jiǎn)單地打印了結(jié)果。
如果編譯運(yùn)行并點(diǎn)擊登錄事件,你就會(huì)在Xcode的控制臺(tái)看到上面代碼的運(yùn)行結(jié)果……
跟你想象中的大相徑庭!
2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign in result:
<RACDynamicSignal: 0xa068a00> name: +createSignal:
subscribeNext
的block的確已經(jīng)接收了一個(gè)信號(hào),但是并不是登錄信號(hào)的結(jié)果!
是時(shí)候分析一下這個(gè)管道好讓你明白中間發(fā)生了什么了:
當(dāng)你點(diǎn)擊按鈕的時(shí)候,rac_signalForControlEvents
發(fā)送了一個(gè)next
事件(源按鈕作為事件的值)。之后在map方法中創(chuàng)建并返回了登錄信號(hào),意味著接下來(lái)的管道環(huán)節(jié)現(xiàn)在接受到的是一個(gè)RACSignal
。也就是你在subscribeNext:
環(huán)節(jié)所獲取到的。
上面的狀況有時(shí)被稱為信號(hào)里的信號(hào)(signal of signals);換句話說(shuō)就是一個(gè)外部的信號(hào)包含著一個(gè)內(nèi)部的信號(hào)。如果你真的想這么做的話,你可以在外部信號(hào)的subscribeNext:
block中訂閱內(nèi)部的信號(hào)。但這想必會(huì)讓代碼變得相當(dāng)混亂。幸而這是一個(gè)常見(jiàn)問(wèn)題,ReactiveCocoa已經(jīng)為這種情形提供了解決方法。
信號(hào)里的信號(hào)
這種問(wèn)題的解決方法相當(dāng)直接,只要把map
方法像下面一樣替換成flattenMap
方法就可以了:
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
flattenMap:^id(id x) {
return [self signInSignal];
}]
subscribeNext:^(id x) {
NSLog(@"Sign in result: %@", x);
}];
這把按鈕的點(diǎn)擊事件像之前一樣映射給了登錄信號(hào),但也將內(nèi)部信號(hào)的事件發(fā)送給了外部信號(hào)。
編譯運(yùn)行并留意控制臺(tái)。這時(shí)打印的應(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
多么令人振奮啊!
現(xiàn)在這管道已經(jīng)想你期望一樣運(yùn)作了,最后一步需要做的就是在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中獲取了登錄信號(hào)的結(jié)果,根據(jù)結(jié)果更新signInFailureText
文本框的內(nèi)容,同時(shí)按需導(dǎo)航到下一頁(yè)面。編譯運(yùn)行,再次欣賞一下這可愛(ài)的貓咪吧!
你是否注意到現(xiàn)在的應(yīng)用有一個(gè)小小的用戶習(xí)慣問(wèn)題?當(dāng)?shù)卿浄?wù)驗(yàn)證時(shí),登錄按鈕應(yīng)當(dāng)是不可用的。這能夠避免用戶重復(fù)登錄。而且,如果登錄失敗了,當(dāng)用戶再次嘗試登錄時(shí)錯(cuò)誤信息應(yīng)當(dāng)隱藏起來(lái)。
但是怎樣添加這些邏輯到現(xiàn)有的管道中呢?影響按鈕可用狀態(tài)的跟改變,過(guò)濾或者其他你現(xiàn)今遇到過(guò)的概念掛不上勾。相對(duì)的,這其實(shí)是一種副作用,或者說(shuō)是當(dāng)新的事件發(fā)生時(shí)你希望管道執(zhí)行的邏輯,而那并不改變對(duì)應(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];
}
}];
你可以看到上面在按鈕點(diǎn)擊事件創(chuàng)建后添加了一個(gè)doNext:
環(huán)節(jié)。注意doNext:
是一個(gè)副作用,所以block沒(méi)有返回任何值;它并不影響事件的內(nèi)容。
doNext:
的block中把按鈕的的可用屬性設(shè)為NO
,同時(shí)隱藏了失敗文本。直到subscribeNext:
的block中按鈕才再次變?yōu)榭捎茫⒏鶕?jù)登錄的結(jié)果決定顯示或隱藏失敗文本。
更新包含了副作用的管道圖表如下。
編譯運(yùn)行應(yīng)用,確保登錄按鈕像預(yù)想一樣切換可用與不可用的狀態(tài)。
而到了這里,你的工作已經(jīng)完成了——應(yīng)用已經(jīng)完全實(shí)現(xiàn)了響應(yīng)式重構(gòu)。
如果你在過(guò)程中感到疑惑,你可以下載最終的項(xiàng)目(包含了框架引用),你也可以從GitHub上獲取代碼,那里有對(duì)應(yīng)教程中每一步操作的提交記錄。
注意:在異步操作時(shí)禁用按鈕也是一個(gè)很常見(jiàn)的問(wèn)題,ReactiveCocoa同樣給出了解決方案。
RACCommand
封裝了這個(gè)概念,通過(guò)一個(gè)可用信號(hào)去關(guān)聯(lián)按鈕的可用屬性。你回頭也可以試試使用這個(gè)類。
最后
希望這篇教程能夠?yàn)槟阍谧约旱捻?xiàng)目上使用ReactiveCocoa建立了一個(gè)良好的基礎(chǔ)。要熟練運(yùn)用這些概念還需要一定的練習(xí),但如同其他編程語(yǔ)言一樣,一旦你掌握了,也不過(guò)如此。ReactiveCocoa最為核心的就是信號(hào),說(shuō)白了也就是事件流。還有比這個(gè)更加簡(jiǎn)單的么?
同時(shí)我發(fā)現(xiàn)在ReactiveCocoa中有趣的是,你能使用好幾種方式去解決同一個(gè)問(wèn)題。你可以試試在這個(gè)應(yīng)用中實(shí)踐一下,自己調(diào)整信號(hào)與管道的拆分與組合來(lái)達(dá)到相同的效果。
謹(jǐn)記使用ReactiveCocoa最重要的目的是使你的代碼更加整潔和易懂。就我個(gè)人而言,使用清晰的管道和流式語(yǔ)法能讓我更容易理解應(yīng)用的工作流程。
在本系列教程的下半部,你會(huì)學(xué)到一些更深層的主題,比如錯(cuò)誤的處理和在其他線程中執(zhí)行代碼。而在那之前,就先愉快地實(shí)踐一下暫時(shí)所學(xué)吧!