本文翻譯自ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2
作為一個(gè)iOS開(kāi)發(fā)者草慧,你寫(xiě)的每一行代碼幾乎都是在響應(yīng)某個(gè)事件,例如按鈕的點(diǎn)擊第美,收到網(wǎng)絡(luò)消息,屬性的變化(通過(guò)KVO)或者用戶位置的變化(通過(guò)CoreLocation)。但是這些事件都用不同的方式來(lái)處理脐帝,比如action、delegate糖权、KVO堵腹、callback等。ReactiveCocoa為事件定義了一個(gè)標(biāo)準(zhǔn)接口星澳,從而可以使用一些基本工具來(lái)更容易的連接疚顷、過(guò)濾和組合。
如果你對(duì)上面說(shuō)的還比較疑惑禁偎,那還是繼續(xù)往下看吧荡含。
ReactiveCocoa結(jié)合了幾種編程風(fēng)格:
函數(shù)式編程(Functional Programming):使用高階函數(shù),例如函數(shù)用其他函數(shù)作為參數(shù)届垫。
響應(yīng)式編程(Reactive Programming):關(guān)注于數(shù)據(jù)流和變化傳播。
所以全释,你可能聽(tīng)說(shuō)過(guò)ReactiveCocoa被描述為函數(shù)響應(yīng)式編程(FRP)框架装处。
這就是這篇教程要講的內(nèi)容。編程范式是個(gè)不錯(cuò)的主題浸船,但是本篇教程的其余部分將會(huì)通過(guò)一個(gè)例子來(lái)實(shí)踐妄迁。
Reactive Playground
通過(guò)這篇教程,一個(gè)簡(jiǎn)單的范例應(yīng)用Reactive Playground 李命,你將會(huì)了解到響應(yīng)式編程登淘。下載初始工程,然后編譯運(yùn)行一下確保你已經(jīng)把一切都設(shè)置正確了封字。
Reactive Playground是一個(gè)非常簡(jiǎn)單的應(yīng)用黔州,它為用戶展示了一個(gè)登錄頁(yè)耍鬓。在用戶名框輸入user,在密碼框輸入password流妻,然后你就能看到有一只可愛(ài)小貓咪的歡迎頁(yè)了牲蜀。
呀,真是可愛(ài)啊绅这。
現(xiàn)在可以花一些時(shí)間來(lái)看一下初始工程的代碼涣达。很簡(jiǎn)單,用不了多少時(shí)間证薇。
打開(kāi)RWViewController.m看一下度苔。你多快能找到控制登錄按鈕是否可用的條件?判斷顯示/隱藏登錄失敗label的條件是什么浑度?在這個(gè)相對(duì)簡(jiǎn)單的例子里寇窑,可能只用一兩分鐘就能回答這些問(wèn)題。但是對(duì)于更復(fù)雜的例子俺泣,這些所花的時(shí)間可能就比較多了疗认。
使用ReactiveCocoa,可以使應(yīng)用的基本邏輯變得相當(dāng)簡(jiǎn)潔伏钠。是時(shí)候開(kāi)始啦横漏。
添加ReactiveCocoa框架
添加ReactiveCocoa框架最簡(jiǎn)單的方法就是用CocoaPods。如果你從沒(méi)用過(guò)CocoaPods熟掂,那還是先去看看CocoaPods簡(jiǎn)介這篇教程吧缎浇。請(qǐng)至少看完教程中初始化的步驟,這樣你才能安裝框架赴肚。
注意:如果不想用CocoaPods素跺,你仍然可以使用ReactiveCocoa,具體查看Github文檔中引入ReactiveCocoa的步驟描述誉券。
譯注:我就是不喜歡用CocoaPods的那波人指厌。所以我首先使用了Github上提供的方法,但是在第二步執(zhí)行bootstrap時(shí)提示缺少xctool踊跟,我就果斷放棄了踩验,還是乖乖用CocoaPods吧。
具體怎么使用CocoaPods安裝就不詳細(xì)講解了商玫。
開(kāi)動(dòng)
就像在介紹中提到的箕憾,RAC為應(yīng)用中發(fā)生的不同事件流提供了一個(gè)標(biāo)準(zhǔn)接口。在ReactiveCocoa術(shù)語(yǔ)中這個(gè)叫做信號(hào)(signal)拳昌,由RACSignal類表示袭异。
打開(kāi)應(yīng)用的初始view controller,RWViewController.m 炬藤,引入ReactiveCocoa的頭文件御铃。
不要替換已有的代碼碴里,將下面的代碼添加到viewDidLoad方法的最后:self.usernameTextField是您聲明的一個(gè)textFiled屬性
[self.usernameTextField.rac_textSignal subscribeNext:^(id x){
NSLog(@"%@", x);
}];
編譯運(yùn)行,在用戶名輸入框中輸幾個(gè)字畅买。注意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中的代碼都會(huì)執(zhí)行谷羞。沒(méi)有target-action帝火,沒(méi)有delegate,只有signal和block湃缎。令人激動(dòng)不是嗎犀填?
ReactiveCocoa signal(RACSignal)發(fā)送事件流給它的subscriber。目前總共有三種類型的事件:next嗓违、error九巡、completed。一個(gè)signal在因error終止或者完成前蹂季,可以發(fā)送任意數(shù)量的next事件冕广。在本教程的第一部分,我們將會(huì)關(guān)注next事件偿洁。在第二部分撒汉,將會(huì)學(xué)習(xí)error和completed事件。
RACSignal有很多方法可以來(lái)訂閱不同的事件類型涕滋。每個(gè)方法都需要至少一個(gè)block睬辐,當(dāng)事件發(fā)生時(shí)就會(huì)執(zhí)行block中的邏輯。在上面的例子中可以看到每次next事件發(fā)生時(shí)宾肺,subscribeNext:方法提供的block都會(huì)執(zhí)行溯饵。
ReactiveCocoa框架使用category來(lái)為很多基本UIKit控件添加signal。這樣你就能給控件添加訂閱了锨用,text field的rac_textSignal就是這么來(lái)的丰刊。
原理就說(shuō)這么多,是時(shí)候開(kāi)始讓ReactiveCocoa干活了增拥。
ReactiveCocoa有很多操作來(lái)控制事件流藻三。假設(shè)你只關(guān)心超過(guò)3個(gè)字符長(zhǎng)度的用戶名,那么你可以使用filter操作來(lái)實(shí)現(xiàn)這個(gè)目的跪者。把之前加在viewDidLoad中的代碼更新成下面的
[[self.usernameTextField.rac_textSignal
filter:^BOOL(id value){
NSString*text = value;
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
編譯運(yùn)行,在text field只能怪輸入幾個(gè)字熄求,你會(huì)發(fā)現(xiàn)只有當(dāng)輸入超過(guò)3個(gè)字符時(shí)才會(huì)有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)建的只是一個(gè)很簡(jiǎn)單的管道。這就是響應(yīng)式編程的本質(zhì)弟晚,根據(jù)數(shù)據(jù)流來(lái)表達(dá)應(yīng)用的功能忘衍。
用圖形來(lái)表達(dá)就是下面這樣的:
從上面的圖中可以看到逾苫,rac_textSignal是起始事件。然后數(shù)據(jù)通過(guò)一個(gè)filter枚钓,如果這個(gè)事件包含一個(gè)長(zhǎng)度超過(guò)3的字符串铅搓,那么該事件就可以通過(guò)。管道的最后一步就是subscribeNext:搀捷,block在這里打印出事件的值星掰。
filter操作的輸出也是RACSignal,這點(diǎn)先放到一邊嫩舟。你可以像下面那樣調(diào)整一下代碼來(lái)展示每一步的操作氢烘。
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的每個(gè)操作都會(huì)返回一個(gè)RACsignal,這在術(shù)語(yǔ)上叫做連貫接口(fluent interface)家厌。這個(gè)功能可以讓你直接構(gòu)建管道播玖,而不用每一步都使用本地變量。
注意:ReactiveCocoa大量使用block饭于。如果你是block新手蜀踏,你可能想看看Apple官方的block編程指南。如果你熟悉block掰吕,但是覺(jué)得block的語(yǔ)法有些奇怪和難記果覆,你可能會(huì)想看看這個(gè)有趣又實(shí)用的網(wǎng)頁(yè)f*****gblocksyntax.com。
類型轉(zhuǎn)換
如果你之前把代碼分成了多個(gè)步驟畴栖,現(xiàn)在再把它改回來(lái)吧随静。。吗讶。燎猛。。照皆。重绷。。
[[self.usernameTextField.rac_textSignal
filter:^BOOL(id value){
NSString*text = value; // implicit cast
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
在上面的代碼中膜毁,注釋部分標(biāo)記了將id隱式轉(zhuǎn)換為NSString昭卓,這看起來(lái)不是很好看。幸運(yùn)的是瘟滨,傳入block的值肯定是個(gè)NSString候醒,所以你可以直接修改參數(shù)類型,把代碼更新成下面的這樣的:
[[self.usernameTextField.rac_textSignal
filter:^BOOL(NSString*text){
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
編譯運(yùn)行杂瘸,確保沒(méi)什么問(wèn)題倒淫。
什么是事件呢?
到目前為止败玉,本篇教程已經(jīng)描述了不同的事件類型敌土,但是還沒(méi)有說(shuō)明這些事件的結(jié)構(gòu)镜硕。有意思的是(?)返干,事件可以包括任何事情兴枯。
下面來(lái)展示一下,在管道中添加另一個(gè)操作矩欠。把添加在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)行财剖,你會(huì)發(fā)現(xiàn)log輸出變成了文本的長(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改變了事件的數(shù)據(jù)晚顷。map從上一個(gè)next事件接收數(shù)據(jù)峰伙,通過(guò)執(zhí)行block把返回值傳給下一個(gè)next事件。在上面的代碼中该默,map以NSString為輸入瞳氓,取字符串的長(zhǎng)度,返回一個(gè)NSNumber栓袖。
來(lái)看下面的圖片:
能看到map操作之后的步驟收到的都是NSNumber實(shí)例匣摘。你可以使用map操作來(lái)把接收的數(shù)據(jù)轉(zhuǎn)換成想要的類型,只要它是個(gè)對(duì)象裹刮。
注意:在上面的例子中text.length返回一個(gè)NSUInteger音榜,是一個(gè)基本類型。為了將它作為事件的內(nèi)容捧弃,NSUInteger必須被封裝赠叼。幸運(yùn)的是Objective-C literal syntax提供了一種簡(jiǎn)單的方法來(lái)封裝——@ (text.length)。
現(xiàn)在差不多是時(shí)候用所學(xué)的內(nèi)容來(lái)更新一下ReactivePlayground應(yīng)用了违霞。你可以把之前的添加代碼都刪除了嘴办。。买鸽。涧郊。。眼五。
創(chuàng)建有效狀態(tài)信號(hào)
首先要做的就是創(chuàng)建一些信號(hào)妆艘,來(lái)表示用戶名和密碼輸入框中的輸入內(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]);
}];
可以看到看幼,上面的代碼對(duì)每個(gè)輸入框的rac_textSignal應(yīng)用了一個(gè)map轉(zhuǎn)換批旺。輸出是一個(gè)用NSNumber封裝的布爾值。
下一步是轉(zhuǎn)換這些信號(hào)诵姜,從而能為輸入框設(shè)置不同的背景顏色朱沃。基本上就是,你訂閱這些信號(hào)逗物,然后用接收到的值來(lái)更新輸入框的背景顏色。下面有一種方法:
[[validPasswordSignal
map:^id(NSNumber *passwordValid){
return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
}]
subscribeNext:^(UIColor *color){
self.passwordTextField.backgroundColor = color;
}];
(不要使用這段代碼瑟俭,下面有一種更好的寫(xiě)法t嶙俊)
從概念上來(lái)說(shuō),就是把之前信號(hào)的輸出應(yīng)用到輸入框的backgroundColor屬性上摆寄。但是上面的用法不是很好失暴。
幸運(yùn)的是,ReactiveCocoa提供了一個(gè)宏來(lái)更好的完成上面的事情微饥。把下面的代碼直接加到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宏允許直接把信號(hào)的輸出應(yīng)用到對(duì)象的屬性上逗扒。RAC宏有兩個(gè)參數(shù),第一個(gè)是需要設(shè)置屬性值的對(duì)象欠橘,第二個(gè)是屬性名矩肩。每次信號(hào)產(chǎn)生一個(gè)next事件,傳遞過(guò)來(lái)的值都會(huì)應(yīng)用到該屬性上肃续。
你不覺(jué)得這種方法很好嗎黍檩?
在編譯運(yùn)行之前,找到updateUIState方法始锚,把頭兩行刪掉刽酱。
self.usernameTextField.backgroundColor =
self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor];
self.passwordTextField.backgroundColor =
self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];
這樣就把不相關(guān)的代碼刪掉了。
編譯運(yùn)行瞧捌,可以發(fā)現(xiàn)當(dāng)輸入內(nèi)容無(wú)效時(shí)棵里,輸入框看起來(lái)高亮了,有效時(shí)又透明了姐呐。
現(xiàn)在的邏輯用圖形來(lái)表示就是下面這樣的殿怜。能看到有兩條簡(jiǎn)單的管道,兩個(gè)文本信號(hào)皮钠,經(jīng)過(guò)一個(gè)map轉(zhuǎn)為表示是否有效的布爾值稳捆,再經(jīng)過(guò)一個(gè)map轉(zhuǎn)為UIColor,而這個(gè)UIColor已經(jīng)和輸入框的背景顏色綁定了麦轰。
你是否好奇為什么要?jiǎng)?chuàng)建兩個(gè)分開(kāi)的validPasswordSignal和validUsernameSignal呢乔夯,而不是每個(gè)輸入框一個(gè)單獨(dú)的管道呢?(款侵?)稍安勿躁末荐,答案就在下面。
原文: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!
聚合信號(hào)
目前在應(yīng)用中新锈,登錄按鈕只有當(dāng)用戶名和密碼輸入框的輸入都有效時(shí)才工作〖自啵現(xiàn)在要把這里改成響應(yīng)式的。
現(xiàn)在的代碼中已經(jīng)有可以產(chǎn)生用戶名和密碼輸入框是否有效的信號(hào)了——validUsernameSignal和validPasswordSignal了。現(xiàn)在需要做的就是聚合這兩個(gè)信號(hào)來(lái)決定登錄按鈕是否可用块请。
把下面的代碼添加到viewDidLoad的末尾:
RACSignal *signUpActiveSignal =
[RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
reduce:^id(NSNumber*usernameValid, NSNumber *passwordValid){
return @([usernameValid boolValue]&&[passwordValid boolValue]);
}];
上面的代碼使用combineLatest:reduce:方法把validUsernameSignal和validPasswordSignal產(chǎn)生的最新的值聚合在一起娜氏,并生成一個(gè)新的信號(hào)。每次這兩個(gè)源信號(hào)的任何一個(gè)產(chǎn)生新值時(shí)墩新,reduce block都會(huì)執(zhí)行贸弥,block的返回值會(huì)發(fā)給下一個(gè)信號(hào)。
注意:RACsignal的這個(gè)方法可以聚合任意數(shù)量的信號(hào)海渊,reduce block的參數(shù)和每個(gè)源信號(hào)相關(guān)绵疲。ReactiveCocoa有一個(gè)工具類RACBlockTrampoline,它在內(nèi)部處理reduce block的可變參數(shù)臣疑。實(shí)際上在ReactiveCocoa的實(shí)現(xiàn)中有很多隱藏的技巧盔憨,值得你去看看。
現(xiàn)在已經(jīng)有了合適的信號(hào)讯沈,把下面的代碼添加到viewDidLoad的末尾郁岩。這會(huì)把信號(hào)和按鈕的enabled屬性綁定。
[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];
同樣把updateUIState儒老、usernameTextFieldChanged和passwordTextFieldChanged方法刪掉蝴乔。
最后確保把viewDidLoad中updateUIState的調(diào)用刪掉。
編譯運(yùn)行驮樊,看看登錄按鈕薇正。當(dāng)用戶名和密碼輸入有效時(shí),按鈕就是可用的囚衔,和以前一樣挖腰。
現(xiàn)在應(yīng)用的邏輯就是下面這樣的:
上圖展示了一些重要的概念,你可以使用ReactiveCocoa來(lái)完成一些重量級(jí)的任務(wù)练湿。
分割——信號(hào)可以有很多subscriber猴仑,也就是作為很多后續(xù)步驟的源。注意上圖中那個(gè)用來(lái)表示用戶名和密碼有效性的布爾信號(hào)肥哎,它被分割成多個(gè)辽俗,用于不同的地方。
聚合——多個(gè)信號(hào)可以聚合成一個(gè)新的信號(hào)篡诽,在上面的例子中崖飘,兩個(gè)布爾信號(hào)聚合成了一個(gè)。實(shí)際上你可以聚合并產(chǎn)生任何類型的信號(hào)杈女。
這些改動(dòng)的結(jié)果就是朱浴,代碼中沒(méi)有用來(lái)表示兩個(gè)輸入框有效狀態(tài)的私有屬性了吊圾。這就是用響應(yīng)式編程的一個(gè)關(guān)鍵區(qū)別,你不需要使用實(shí)例變量來(lái)追蹤瞬時(shí)狀態(tài)翰蠢。
響應(yīng)式的登錄
應(yīng)用目前使用上面圖中展示的響應(yīng)式管道來(lái)管理輸入框和按鈕的狀態(tài)项乒。但是按鈕按下的處理用的還是action,所以下一步就是把剩下的邏輯都替換成響應(yīng)式的梁沧。
在storyboard中板丽,登錄按鈕的Touch Up Inside事件和RWViewController.m中的signInButtonTouched方法是綁定的。下面會(huì)用響應(yīng)的方法替換趁尼,所以首先要做的就是斷開(kāi)當(dāng)前的storyboard action。
打開(kāi)Main.storyboard猖辫,找到登錄按鈕酥泞,按住ctrl鍵單擊,打開(kāi)outlet/action連接框啃憎,然后點(diǎn)擊x來(lái)斷開(kāi)連接芝囤。如果你找不到的話,下圖中紅色箭頭指示的就是刪除按鈕辛萍。
你已經(jīng)知道了ReactiveCocoa框架是如何給基本UIKit控件添加屬性和方法的了悯姊。目前你已經(jīng)使用了rac_textSignal,它會(huì)在文本發(fā)生變化時(shí)產(chǎn)生信號(hào)贩毕。為了處理按鈕的事件悯许,現(xiàn)在需要用到ReactiveCocoa為UIKit添加的另一個(gè)方法,rac_signalForControlEvents辉阶。
現(xiàn)在回到RWViewController.m先壕,把下面的代碼添加到viewDidLoad的末尾:
[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
NSLog(@"button clicked");
}];
上面的代碼從按鈕的UIControlEventTouchUpInside事件創(chuàng)建了一個(gè)信號(hào),然后添加了一個(gè)訂閱谆甜,在每次事件發(fā)生時(shí)都會(huì)輸出log垃僚。
編譯運(yùn)行,確保的確有l(wèi)og輸出规辱。按鈕只在用戶名和密碼框輸入有效時(shí)可用谆棺,所以在點(diǎn)擊按鈕前需要在兩個(gè)文本框中輸入一些內(nèi)容。
可以看到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)在按鈕有了點(diǎn)擊事件的信號(hào)罕袋,下一步就是把它和登錄流程連接起來(lái)改淑。那么問(wèn)題就來(lái)了,打開(kāi)RWDummySignInService.h炫贤,看一下接口:
typedef void (^RWSignInResponse)(BOOL);
@interface RWDummySignInService : NSObject
- (void)signInWithUsername:(NSString *)username
password:(NSString *)password
complete:(RWSignInResponse)completeBlock;
@end
這個(gè)service有3個(gè)參數(shù)溅固,用戶名、密碼和一個(gè)完成回調(diào)block兰珍。這個(gè)block會(huì)在登錄成功或失敗時(shí)執(zhí)行侍郭。你可以在按鈕點(diǎn)擊事件的subscribeNext: blcok里直接調(diào)用這個(gè)方法,但是為什么你要這么做?(亮元?)
注意:本教程為了簡(jiǎn)便使用了一個(gè)假的service猛计,所以它不依賴任何外部API。但你現(xiàn)在的確遇到了一個(gè)問(wèn)題爆捞,如何使用這些不是用信號(hào)表示的API呢奉瘤?
創(chuàng)建信號(hào)
創(chuàng)建信號(hào)幸運(yùn)的是,把已有的異步API用信號(hào)的方式來(lái)表示相當(dāng)簡(jiǎn)單煮甥。首先把RWViewController.m中的signInButtonTouched:刪掉盗温。你會(huì)用響應(yīng)式的的方法來(lái)替換這段邏輯。還是在RWViewController.m中成肘,添加下面的方法:- (RACSignal *)signInSignal {return [RACSignal createSignal:^RACDisposable *(idsubscriber){
[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:方法來(lái)創(chuàng)建信號(hào)砚偶。方法的入?yún)⑹且粋€(gè)block,這個(gè)block描述了這個(gè)信號(hào)洒闸。當(dāng)這個(gè)信號(hào)有subscriber時(shí)染坯,block里的代碼就會(huì)執(zhí)行。
block的入?yún)⑹且粋€(gè)subscriber實(shí)例丘逸,它遵循RACSubscriber協(xié)議单鹿,協(xié)議里有一些方法來(lái)產(chǎn)生事件,你可以發(fā)送任意數(shù)量的next事件鸣个,或者用error\complete事件來(lái)終止羞反。本例中,信號(hào)發(fā)送了一個(gè)next事件來(lái)表示登錄是否成功囤萤,隨后是一個(gè)complete事件昼窗。
這個(gè)block的返回值是一個(gè)RACDisposable對(duì)象,它允許你在一個(gè)訂閱被取消時(shí)執(zhí)行一些清理工作涛舍。當(dāng)前的信號(hào)不需要執(zhí)行清理操作澄惊,所以返回nil就可以了。
可以看到富雅,把一個(gè)異步API用信號(hào)封裝是多簡(jiǎn)單掸驱!
現(xiàn)在就來(lái)使用這個(gè)新的信號(hào)。把之前添加在viewDidLoad中的代碼更新成下面這樣的:
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
map:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(id x){
NSLog(@"Sign in result: %@", x);
}];
上面的代碼使用map方法没佑,把按鈕點(diǎn)擊信號(hào)轉(zhuǎn)換成了登錄信號(hào)毕贼。subscriber輸出log。編譯運(yùn)行蛤奢,點(diǎn)擊登錄按鈕鬼癣,查看Xcode的控制臺(tái)陶贼,等等,
輸出的這是個(gè)什么鬼?2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign in result:name: +createSignal:
沒(méi)錯(cuò),你已經(jīng)給subscribeNext:的block傳入了一個(gè)信號(hào)燕偶,但傳入的不是登錄結(jié)果的信號(hào)。
下圖展示了到底發(fā)生了什么:
當(dāng)點(diǎn)擊按鈕時(shí)拷恨,rac_signalForControlEvents發(fā)送了一個(gè)next事件(事件的data是UIButton)。map操作創(chuàng)建并返回了登錄信號(hào),這意味著后續(xù)步驟都會(huì)收到一個(gè)RACSignal。這就是你在subscribeNext:這步看到的聊替。
上面問(wèn)題的解決方法,有時(shí)候叫做信號(hào)中的信號(hào)培廓,換句話說(shuō)就是一個(gè)外部信號(hào)里面還有一個(gè)內(nèi)部信號(hào)佃牛。你可以在外部信號(hào)的subscribeNext:block里訂閱內(nèi)部信號(hào)。不過(guò)這樣嵌套太混亂啦医舆,還好ReactiveCocoa已經(jīng)解決了這個(gè)問(wèn)題。
信號(hào)中的信號(hào)
解決的方法很簡(jiǎn)單象缀,只需要把map操作改成flattenMap就可以了:
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
flattenMap:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(id x){
NSLog(@"Sign in result: %@", x);
}];
這個(gè)操作把按鈕點(diǎn)擊事件轉(zhuǎn)換為登錄信號(hào)蔬将,同時(shí)還從內(nèi)部信號(hào)發(fā)送事件到外部信號(hào)。
編譯運(yùn)行央星,注意控制臺(tái)霞怀,現(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從登錄信號(hào)中取得結(jié)果,相應(yīng)地更新signInFailureText是否可見(jiàn)颓遏。如果登錄成功執(zhí)行導(dǎo)航跳轉(zhuǎn)徐矩。
編譯運(yùn)行,應(yīng)該就能再看到可愛(ài)的小貓啦叁幢!喵~
你注意到這個(gè)應(yīng)用現(xiàn)在有一些用戶體驗(yàn)上的小問(wèn)題了嗎滤灯?當(dāng)?shù)卿泂ervice正在校驗(yàn)用戶名和密碼時(shí),登錄按鈕應(yīng)該是不可點(diǎn)擊的曼玩。這會(huì)防止用戶多次執(zhí)行登錄操作鳞骤。還有,如果登錄失敗了黍判,用戶再次嘗試登錄時(shí)豫尽,應(yīng)該隱藏錯(cuò)誤信息。
這個(gè)邏輯應(yīng)該怎么添加呢顷帖?改變按鈕的可用狀態(tài)并不是轉(zhuǎn)換(map)美旧、過(guò)濾(filter)或者其他已經(jīng)學(xué)過(guò)的概念渤滞。其實(shí)這個(gè)就叫做“副作用”,換句話說(shuō)就是在一個(gè)next事件發(fā)生時(shí)執(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:是直接跟在按鈕點(diǎn)擊事件的后面。而且doNext: block并沒(méi)有返回值录肯。因?yàn)樗歉郊硬僮髋恳福⒉桓淖兪录旧怼?br>
上面的doNext: block把按鈕置為不可點(diǎn)擊,隱藏登錄失敗提示论咏。然后在subscribeNext: block里重新把按鈕置為可點(diǎn)擊优炬,并根據(jù)登錄結(jié)果來(lái)決定是否顯示失敗提示。
之前的管道圖就更新成下面這樣的:
編譯運(yùn)行厅贪,確保登錄按鈕的可點(diǎn)擊狀態(tài)和預(yù)期的一樣蠢护。
現(xiàn)在所有的工作都已經(jīng)完成了,這個(gè)應(yīng)用已經(jīng)是響應(yīng)式的啦养涮。
如果你中途哪里出了問(wèn)題葵硕,可以下載最終的工程(依賴庫(kù)都有),或者在Github上找到這份代碼贯吓,教程中的每一次編譯運(yùn)行都有對(duì)應(yīng)的commit懈凹。
注意:在異步操作執(zhí)行的過(guò)程中禁用按鈕是一個(gè)常見(jiàn)的問(wèn)題,ReactiveCocoa也能很好的解決悄谐。RACCommand就包含這個(gè)概念介评,它有一個(gè)enabled信號(hào),能讓你把按鈕的enabled屬性和信號(hào)綁定起來(lái)爬舰。你也許想試試這個(gè)類们陆。
總結(jié)
希望本教程為你今后在自己的應(yīng)用中使用ReactiveCocoa打下了一個(gè)好的基礎(chǔ)。你可能需要一些練習(xí)來(lái)熟悉這些概念情屹,但就像是語(yǔ)言或者編程坪仇,一旦你夯實(shí)基礎(chǔ),用起來(lái)也就很簡(jiǎn)單了垃你。ReactiveCocoa的核心就是信號(hào)烟很,而它不過(guò)就是事件流。還能再更簡(jiǎn)單點(diǎn)嗎蜡镶?
在使用ReactiveCocoa后雾袱,我發(fā)現(xiàn)了一個(gè)有趣的事情,那就是你可以用很多種不同的方法來(lái)解決同一個(gè)問(wèn)題官还。你可以用教程中的例子試試芹橡,調(diào)整一下信號(hào),改改信號(hào)的分割和聚合望伦。
ReactiveCocoa的主旨是讓你的代碼更簡(jiǎn)潔易懂林说,這值得多想想煎殷。我個(gè)人認(rèn)為,如果邏輯可以用清晰的管道腿箩、流式語(yǔ)法來(lái)表示豪直,那就很好理解這個(gè)應(yīng)用到底干了什么了。
轉(zhuǎn)載自微信公眾號(hào):IT界 的一些事珠移,微信識(shí)別二維碼關(guān)注他弓乙,學(xué)習(xí)更多IT知識(shí)!