原文:https://www.raywenderlich.com/62699/reactivecocoa-tutorial-pt1
作為一個iOS開發(fā)者,你寫的每一行代碼幾乎都是在響應(yīng)某個事件,例如按鈕的點擊,收到網(wǎng)絡(luò)消息,屬性的變化(通過KVO)或者用戶位置的變化(通過CoreLocation)铛漓。但是這些事件都用不同的方式來處理支示,比如action悔耘、delegate查邢、KVO蔗崎、callback等。ReactiveCocoa為事件定義了一個標準接口扰藕,從而可以使用一些基本工具來更容易的連接缓苛、過濾和組合。
如果你對上面說的還比較疑惑邓深,那還是繼續(xù)往下看吧未桥。
ReactiveCocoa結(jié)合了幾種編程風(fēng)格:
函數(shù)式編程(Functional Programming):使用高階函數(shù),例如函數(shù)用其他函數(shù)作為參數(shù)芥备。
響應(yīng)式編程(Reactive Programming):關(guān)注于數(shù)據(jù)流和變化傳播冬耿。
所以,你可能聽說過ReactiveCocoa被描述為函數(shù)響應(yīng)式編程(FRP)框架萌壳。
這就是這篇教程要講的內(nèi)容亦镶。編程范式是個不錯的主題,但是本篇教程的其余部分將會通過一個例子來實踐袱瓮。
Reactive Playground
通過這篇教程缤骨,一個簡單的范例應(yīng)用Reactive Playground,你將會了解到響應(yīng)式編程尺借。下載初始工程绊起,然后編譯運行一下確保你已經(jīng)把一切都設(shè)置正確了。
Reactive Playground是一個非常簡單的應(yīng)用燎斩,它為用戶展示了一個登錄頁虱歪。在用戶名框輸入user,在密碼框輸入password栅表,然后你就能看到有一只可愛小貓咪的歡迎頁了笋鄙。
在可以花一些時間來看一下初始工程的代碼。很簡單谨读,用不了多少時間局装。
打開RWViewController.m看一下。你多快能找到控制登錄按鈕是否可用的條件劳殖?判斷顯示/隱藏登錄失敗label的條件是什么铐尚?在這個相對簡單的例子里,可能只用一兩分鐘就能回答這些問題哆姻。但是對于更復(fù)雜的例子宣增,這些所花的時間可能就比較多了。
使用ReactiveCocoa矛缨,可以使應(yīng)用的基本邏輯變得相當簡潔爹脾。是時候開始啦帖旨。
添加ReactiveCocoa框架
添加ReactiveCocoa框架最簡單的方法就是用CocoaPods。如果你從沒用過CocoaPods灵妨,那還是先去看看CocoaPods簡介這篇教程吧解阅。請至少看完教程中初始化的步驟,這樣你才能安裝框架泌霍。
注意:如果不想用CocoaPods货抄,你仍然可以使用ReactiveCocoa,具體查看Github文檔中引入ReactiveCocoa的步驟描述朱转。
譯注:我就是不喜歡用CocoaPods的那波人蟹地。所以我首先使用了Github上提供的方法,但是在第二步執(zhí)行bootstrap時提示缺少xctool藤为,我就果斷放棄了怪与,還是乖乖用CocoaPods吧。
具體怎么使用CocoaPods安裝就不詳細講解了缅疟。
開始
就像在介紹中提到的分别,RAC為應(yīng)用中發(fā)生的不同事件流提供了一個標準接口。在ReactiveCocoa術(shù)語中這個叫做信號(signal)存淫,由RACSignal類表示茎杂。
打開應(yīng)用的初始view controller,RWViewController.m 纫雁,引入ReactiveCocoa的頭文件。
#import <ReactiveCocoa.h>
不要替換已有的代碼倾哺,將下面的代碼添加到viewDidLoad方法的最后:
[self.usernameTextField.rac_textSignal subscribeNext:^(id x){
NSLog(@"%@", x);
}];
編譯運行轧邪,在用戶名輸入框中輸幾個字。注意console的輸出應(yīng)該和下面的類似羞海。
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?
可以看到每次改變文本框中的文字忌愚,block中的代碼都會執(zhí)行。沒有target-action却邓,沒有delegate硕糊,只有signal和block。令人激動不是嗎腊徙?
ReactiveCocoa signal(RACSignal)發(fā)送事件流給它的subscriber简十。目前總共有三種類型的事件:next、error撬腾、completed螟蝙。一個signal在因error終止或者完成前,可以發(fā)送任意數(shù)量的next事件民傻。在本教程的第一部分胰默,我們將會關(guān)注next事件场斑。在第二部分,將會學(xué)習(xí)error和completed事件牵署。
RACSignal有很多方法可以來訂閱不同的事件類型漏隐。每個方法都需要至少一個block,當事件發(fā)生時就會執(zhí)行block中的邏輯奴迅。在上面的例子中可以看到每次next事件發(fā)生時青责,subscribeNext:方法提供的block都會執(zhí)行。
ReactiveCocoa框架使用category來為很多基本UIKit控件添加signal半沽。這樣你就能給控件添加訂閱了爽柒,text field的rac_textSignal就是這么來的。
原理就說這么多者填,是時候開始讓ReactiveCocoa干活了浩村。
ReactiveCocoa有很多操作來控制事件流。假設(shè)你只關(guān)心超過3個字符長度的用戶名占哟,那么你可以使用filter操作來實現(xiàn)這個目的心墅。把之前加在viewDidLoad中的代碼更新成下面的:
[[self.usernameTextField.rac_textSignal
filter:^BOOL(id value){
NSString*text = value;
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
編譯運行,在text field只能怪輸入幾個字榨乎,你會發(fā)現(xiàn)只有當輸入超過3個字符時才會有l(wèi)og怎燥。
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)建的只是一個很簡單的管道。這就是響應(yīng)式編程的本質(zhì)蜜暑,根據(jù)數(shù)據(jù)流來表達應(yīng)用的功能铐姚。
用圖形來表達就是下面這樣的:
從上面的圖中可以看到,rac_textSignal是起始事件肛捍。然后數(shù)據(jù)通過一個filter隐绵,如果這個事件包含一個長度超過3的字符串,那么該事件就可以通過拙毫。管道的最后一步就是subscribeNext:依许,block在這里打印出事件的值。
filter操作的輸出也是RACSignal缀蹄,這點先放到一邊峭跳。你可以像下面那樣調(diào)整一下代碼來展示每一步的操作。
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缺前,這在術(shù)語上叫做連貫接口(fluent interface)蛀醉。這個功能可以讓你直接構(gòu)建管道,而不用每一步都使用本地變量衅码。
注意:ReactiveCocoa大量使用block滞欠。如果你是block新手,你可能想看看Apple官方的block編程指南肆良。如果你熟悉block筛璧,但是覺得block的語法有些奇怪和難記逸绎,你可能會想看看這個有趣又實用的網(wǎng)頁f*****gblocksyntax.com。
類型轉(zhuǎn)換
如果你之前把代碼分成了多個步驟夭谤,現(xiàn)在再把它改回來吧棺牧。
[[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醉锄,所以你可以直接修改參數(shù)類型乏悄,把代碼更新成下面的這樣的:
[[self.usernameTextField.rac_textSignal
filter:^BOOL(NSString*text){
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
編譯運行,確保沒什么問題恳不。
什么是事件呢檩小?
到目前為止,本篇教程已經(jīng)描述了不同的事件類型烟勋,但是還沒有說明這些事件的結(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);
}];
編譯運行丛塌,你會發(fā)現(xiàn)log輸出變成了文本的長度而不是內(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ù)畜疾。map從上一個next事件接收數(shù)據(jù)姨伤,通過執(zhí)行block把返回值傳給下一個next事件。在上面的代碼中庸疾,map以NSString為輸入,取字符串的長度当编,返回一個NSNumber届慈。
來看下面的圖片:
能看到map操作之后的步驟收到的都是NSNumber實例。你可以使用map操作來把接收的數(shù)據(jù)轉(zhuǎn)換成想要的類型忿偷,只要它是個對象金顿。
注意:在上面的例子中text.length返回一個NSUInteger,是一個基本類型鲤桥。為了將它作為事件的內(nèi)容揍拆,NSUInteger必須被封裝。幸運的是Objective-C literal syntax提供了一種簡單的方法來封裝——@ (text.length)茶凳。
現(xiàn)在差不多是時候用所學(xué)的內(nèi)容來更新一下ReactivePlayground應(yīng)用了嫂拴。你可以把之前的添加代碼都刪除了播揪。
創(chuàng)建有效狀態(tài)信號
首先要做的就是創(chuàng)建一些信號,來表示用戶名和密碼輸入框中的輸入內(nèi)容是否有效筒狠。把下面的代碼添加到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]);
}];
可以看到猪狈,上面的代碼對每個輸入框的rac_textSignal應(yīng)用了一個map轉(zhuǎn)換。輸出是一個用NSNumber封裝的布爾值辩恼。
下一步是轉(zhuǎn)換這些信號雇庙,從而能為輸入框設(shè)置不同的背景顏色≡钜粒基本上就是疆前,你訂閱這些信號,然后用接收到的值來更新輸入框的背景顏色聘萨。下面有一種方法:
[[validPasswordSignal
map:^id(NSNumber *passwordValid){
return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
}]
subscribeNext:^(UIColor *color){
self.passwordTextField.backgroundColor = color;
}];
(不要使用這段代碼竹椒,下面有一種更好的寫法!)
從概念上來說匈挖,就是把之前信號的輸出應(yīng)用到輸入框的backgroundColor屬性上碾牌。但是上面的用法不是很好。
幸運的是儡循,ReactiveCocoa提供了一個宏來更好的完成上面的事情舶吗。把下面的代碼直接加到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宏允許直接把信號的輸出應(yīng)用到對象的屬性上。RAC宏有兩個參數(shù)择膝,第一個是需要設(shè)置屬性值的對象誓琼,第二個是屬性名。每次信號產(chǎn)生一個next事件肴捉,傳遞過來的值都會應(yīng)用到該屬性上腹侣。
你不覺得這種方法很好嗎?
在編譯運行之前齿穗,找到updateUIState方法傲隶,把頭兩行刪掉。
self.usernameTextField.backgroundColor =
self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor];
self.passwordTextField.backgroundColor =
self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];
這樣就把不相關(guān)的代碼刪掉了窃页。
編譯運行跺株,可以發(fā)現(xiàn)當輸入內(nèi)容無效時,輸入框看起來高亮了脖卖,有效時又透明了乒省。
現(xiàn)在的邏輯用圖形來表示就是下面這樣的。能看到有兩條簡單的管道畦木,兩個文本信號袖扛,經(jīng)過一個map轉(zhuǎn)為表示是否有效的布爾值,再經(jīng)過一個map轉(zhuǎn)為UIColor十籍,而這個UIColor已經(jīng)和輸入框的背景顏色綁定了蛆封。
你是否好奇為什么要創(chuàng)建兩個分開的validPasswordSignal和validUsernameSignal呢唇礁,而不是每個輸入框一個單獨的管道呢?(娶吞?)稍安勿躁垒迂,答案就在下面。
原文:Are you wondering why you created separate validPasswordSignal and validUsernameSignal signals, as opposed to a single fluent pipeline for each text field? Patience dear reader, the method behind this madness will become clear shortly!
聚合信號
目前在應(yīng)用中妒蛇,登錄按鈕只有當用戶名和密碼輸入框的輸入都有效時才工作』希現(xiàn)在要把這里改成響應(yīng)式的。
現(xiàn)在的代碼中已經(jīng)有可以產(chǎn)生用戶名和密碼輸入框是否有效的信號了——validUsernameSignal和validPasswordSignal了⌒宥幔現(xiàn)在需要做的就是聚合這兩個信號來決定登錄按鈕是否可用吏奸。
把下面的代碼添加到viewDidLoad的末尾:
RACSignal *signUpActiveSignal =
[RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
reduce:^id(NSNumber*usernameValid, NSNumber *passwordValid){
return @([usernameValid boolValue]&&[passwordValid boolValue]);
}];
上面的代碼使用combineLatest:reduce:方法把validUsernameSignal和validPasswordSignal產(chǎn)生的最新的值聚合在一起,并生成一個新的信號陶耍。每次這兩個源信號的任何一個產(chǎn)生新值時奋蔚,reduce block都會執(zhí)行,block的返回值會發(fā)給下一個信號烈钞。
注意:RACsignal的這個方法可以聚合任意數(shù)量的信號泊碑,reduce block的參數(shù)和每個源信號相關(guān)。ReactiveCocoa有一個工具類RACBlockTrampoline毯欣,它在內(nèi)部處理reduce block的可變參數(shù)馒过。實際上在ReactiveCocoa的實現(xiàn)中有很多隱藏的技巧,值得你去看看酗钞。
現(xiàn)在已經(jīng)有了合適的信號腹忽,把下面的代碼添加到viewDidLoad的末尾。這會把信號和按鈕的enabled屬性綁定砚作。
[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方法刪掉。
最后確保把viewDidLoad中updateUIState的調(diào)用刪掉米同。
編譯運行骇扇,看看登錄按鈕。當用戶名和密碼輸入有效時窍霞,按鈕就是可用的,和以前一樣拯坟。
現(xiàn)在應(yīng)用的邏輯就是下面這樣的:
上圖展示了一些重要的概念但金,你可以使用ReactiveCocoa來完成一些重量級的任務(wù)。
- 分割——信號可以有很多subscriber郁季,也就是作為很多后續(xù)步驟的源冷溃。注意上圖中那個用來表示用戶名和密碼有效性的布爾信號钱磅,它被分割成多個,用于不同的地方似枕。
- 聚合——多個信號可以聚合成一個新的信號盖淡,在上面的例子中,兩個布爾信號聚合成了一個凿歼。實際上你可以聚合并產(chǎn)生任何類型的信號褪迟。
這些改動的結(jié)果就是,代碼中沒有用來表示兩個輸入框有效狀態(tài)的私有屬性了答憔。這就是用響應(yīng)式編程的一個關(guān)鍵區(qū)別味赃,你不需要使用實例變量來追蹤瞬時狀態(tài)。
響應(yīng)式的登錄
應(yīng)用目前使用上面圖中展示的響應(yīng)式管道來管理輸入框和按鈕的狀態(tài)虐拓。但是按鈕按下的處理用的還是action心俗,所以下一步就是把剩下的邏輯都替換成響應(yīng)式的。
在storyboard中蓉驹,登錄按鈕的Touch Up Inside事件和RWViewController.m中的signInButtonTouched方法是綁定的城榛。下面會用響應(yīng)的方法替換,所以首先要做的就是斷開當前的storyboard action态兴。
打開Main.storyboard狠持,找到登錄按鈕,按住ctrl鍵單擊诗茎,打開outlet/action連接框工坊,然后點擊x來斷開連接。如果你找不到的話敢订,下圖中紅色箭頭指示的就是刪除按鈕王污。
你已經(jīng)知道了ReactiveCocoa框架是如何給基本UIKit控件添加屬性和方法的了。目前你已經(jīng)使用了rac_textSignal楚午,它會在文本發(fā)生變化時產(chǎn)生信號昭齐。為了處理按鈕的事件,現(xiàn)在需要用到ReactiveCocoa為UIKit添加的另一個方法矾柜,rac_signalForControlEvents阱驾。
現(xiàn)在回到RWViewController.m,把下面的代碼添加到viewDidLoad的末尾:
[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
NSLog(@"button clicked");
}];
上面的代碼從按鈕的UIControlEventTouchUpInside事件創(chuàng)建了一個信號怪蔑,然后添加了一個訂閱里覆,在每次事件發(fā)生時都會輸出log。
編譯運行缆瓣,確保的確有l(wèi)og輸出喧枷。按鈕只在用戶名和密碼框輸入有效時可用,所以在點擊按鈕前需要在兩個文本框中輸入一些內(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)在按鈕有了點擊事件的信號隧甚,下一步就是把它和登錄流程連接起來车荔。那么問題就來了,打開RWDummySignInService.h戚扳,看一下接口:
typedef void (^RWSignInResponse)(BOOL);
@interface RWDummySignInService : NSObject
- (void)signInWithUsername:(NSString *)username
password:(NSString *)password
complete:(RWSignInResponse)completeBlock;
@end
這個service有3個參數(shù)忧便,用戶名、密碼和一個完成回調(diào)block帽借。這個block會在登錄成功或失敗時執(zhí)行珠增。你可以在按鈕點擊事件的subscribeNext: blcok里直接調(diào)用這個方法,但是為什么你要這么做宜雀?(切平?)
注意:本教程為了簡便使用了一個假的service,所以它不依賴任何外部API辐董。但你現(xiàn)在的確遇到了一個問題悴品,如何使用這些不是用信號表示的API呢?
創(chuàng)建信號
幸運的是简烘,把已有的異步API用信號的方式來表示相當簡單苔严。首先把RWViewController.m中的signInButtonTouched:刪掉。你會用響應(yīng)式的的方法來替換這段邏輯孤澎。
還是在RWViewController.m中届氢,添加下面的方法:
- (RACSignal *)signInSignal {
return [RACSignal createSignal:^RACDisposable *(id subscriber){
[self.signInService
signInWithUsername:self.usernameTextField.text
password:self.passwordTextField.text
complete:^(BOOL success){
[subscriber sendNext:@(success)];
[subscriber sendCompleted];
}];
return nil;
}];
}
上面的方法創(chuàng)建了一個信號,使用用戶名和密碼登錄「残瘢現(xiàn)在分解來看一下退子。
上面的代碼使用RACSignal的createSignal:方法來創(chuàng)建信號。方法的入?yún)⑹且粋€block型将,這個block描述了這個信號寂祥。當這個信號有subscriber時,block里的代碼就會執(zhí)行七兜。
block的入?yún)⑹且粋€subscriber實例丸凭,它遵循RACSubscriber協(xié)議,協(xié)議里有一些方法來產(chǎn)生事件腕铸,你可以發(fā)送任意數(shù)量的next事件惜犀,或者用error\complete事件來終止。本例中狠裹,信號發(fā)送了一個next事件來表示登錄是否成功虽界,隨后是一個complete事件。
這個block的返回值是一個RACDisposable對象涛菠,它允許你在一個訂閱被取消時執(zhí)行一些清理工作莉御。當前的信號不需要執(zhí)行清理操作刹缝,所以返回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)換成了登錄信號。subscriber輸出log噪奄。
編譯運行死姚,點擊登錄按鈕,查看Xcode的控制臺勤篮,等等都毒,輸出的這是個什么鬼?
2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign in result:
name: +createSignal:
沒錯碰缔,你已經(jīng)給subscribeNext:的block傳入了一個信號账劲,但傳入的不是登錄結(jié)果的信號。
下圖展示了到底發(fā)生了什么:
當點擊按鈕時金抡,rac_signalForControlEvents發(fā)送了一個next事件(事件的data是UIButton)瀑焦。map操作創(chuàng)建并返回了登錄信號,這意味著后續(xù)步驟都會收到一個RACSignal梗肝。這就是你在subscribeNext:這步看到的榛瓮。
上面問題的解決方法,有時候叫做信號中的信號巫击,換句話說就是一個外部信號里面還有一個內(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);
}];
這個操作把按鈕點擊事件轉(zhuǎn)換為登錄信號什黑,同時還從內(nèi)部信號發(fā)送事件到外部信號崎淳。
編譯運行,注意控制臺愕把,現(xiàn)在應(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)完成了大部分的內(nèi)容恨豁,最后就是在subscribeNext步驟里添加登錄成功后跳轉(zhuǎn)的邏輯嚣镜。把代碼更新成下面的:
[[[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é)果,相應(yīng)地更新signInFailureText是否可見橘蜜。如果登錄成功執(zhí)行導(dǎo)航跳轉(zhuǎn)菊匿。
編譯運行付呕,應(yīng)該就能再看到可愛的小貓啦!喵~
你注意到這個應(yīng)用現(xiàn)在有一些用戶體驗上的小問題了嗎跌捆?當?shù)卿泂ervice正在校驗用戶名和密碼時徽职,登錄按鈕應(yīng)該是不可點擊的。這會防止用戶多次執(zhí)行登錄操作佩厚。還有姆钉,如果登錄失敗了,用戶再次嘗試登錄時抄瓦,應(yīng)該隱藏錯誤信息潮瓶。
這個邏輯應(yīng)該怎么添加呢?改變按鈕的可用狀態(tài)并不是轉(zhuǎn)換(map)钙姊、過濾(filter)或者其他已經(jīng)學(xué)過的概念毯辅。其實這個就叫做“副作用”,換句話說就是在一個next事件發(fā)生時執(zhí)行的邏輯煞额,而該邏輯并不改變事件本身思恐。
添加附加操作(Adding side-effects)
把代碼更新成下面的:
[[[[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];
}
}];
你可以看到doNext:是直接跟在按鈕點擊事件的后面。而且doNext: block并沒有返回值膊毁。因為它是附加操作壁袄,并不改變事件本身。
上面的doNext: block把按鈕置為不可點擊媚媒,隱藏登錄失敗提示嗜逻。然后在subscribeNext: block里重新把按鈕置為可點擊,并根據(jù)登錄結(jié)果來決定是否顯示失敗提示缭召。
之前的管道圖就更新成下面這樣的:
編譯運行栈顷,確保登錄按鈕的可點擊狀態(tài)和預(yù)期的一樣。
現(xiàn)在所有的工作都已經(jīng)完成了嵌巷,這個應(yīng)用已經(jīng)是響應(yīng)式的啦萄凤。
如果你中途哪里出了問題,可以下載最終的工程(依賴庫都有)搪哪,或者在Github上找到這份代碼靡努,教程中的每一次編譯運行都有對應(yīng)的commit。
注意:在異步操作執(zhí)行的過程中禁用按鈕是一個常見的問題晓折,ReactiveCocoa也能很好的解決惑朦。RACCommand就包含這個概念,它有一個enabled信號漓概,能讓你把按鈕的enabled屬性和信號綁定起來漾月。你也許想試試這個類。
總結(jié)
希望本教程為你今后在自己的應(yīng)用中使用ReactiveCocoa打下了一個好的基礎(chǔ)胃珍。你可能需要一些練習(xí)來熟悉這些概念梁肿,但就像是語言或者編程蜓陌,一旦你夯實基礎(chǔ),用起來也就很簡單了吩蔑。ReactiveCocoa的核心就是信號钮热,而它不過就是事件流。還能再更簡單點嗎烛芬?
在使用ReactiveCocoa后霉旗,我發(fā)現(xiàn)了一個有趣的事情,那就是你可以用很多種不同的方法來解決同一個問題蛀骇。你可以用教程中的例子試試,調(diào)整一下信號读拆,改改信號的分割和聚合擅憔。
ReactiveCocoa的主旨是讓你的代碼更簡潔易懂,這值得多想想檐晕。我個人認為暑诸,如果邏輯可以用清晰的管道、流式語法來表示辟灰,那就很好理解這個應(yīng)用到底干了什么了个榕。
在本系列教程的第二部分,你將會學(xué)到諸如錯誤處理芥喇、在不同線程中執(zhí)行代碼等高級用法西采。