Reactivecocoa 學(xué)習(xí)

作為一個iOS開發(fā)者,你寫的每一行代碼幾乎都是在相應(yīng)某個事件睹酌,例如按鈕的點擊憋沿,收到網(wǎng)絡(luò)消息辐啄,屬性的變化(通過KVO)或者用戶位置的變化(通過CoreLocation)壶辜。但是這些事件都用不同的方式來處理洗出,比如action翩活、delegate菠镇、KVO利耍、callback等隘梨。ReactiveCocoa為事件定義了一個標(biāo)準(zhǔn)接口轴猎,從而可以使用一些基本工具來更容易的連接捻脖、過濾和組合可婶。

如果你對上面說的還比較疑惑矛渴,那還是繼續(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,然后你就能看到有一只可愛小貓咪的歡迎頁了外恕。


呀鳞疲,真是可愛啊建丧。

現(xiàn)在可以花一些時間來看一下初始工程的代碼翎朱。很簡單拴曲,用不了多少時間。

打開RWViewController.m看一下叁熔。你多快能找到控制登錄按鈕是否可用的條件荣回?判斷顯示/隱藏登錄失敗label的條件是什么心软?在這個相對簡單的例子里删铃,可能只用一兩分鐘就能回答這些問題猎唁。但是對于更復(fù)雜的例子胖秒,這些所花的時間可能就比較多了阎肝。

使用ReactiveCocoa,可以使應(yīng)用的基本邏輯變得相當(dāng)簡潔沛硅。是時候開始啦摇肌。

添加ReactiveCocoa框架

添加ReactiveCocoa框架最簡單的方法就是用CocoaPods围小。如果你從沒用過CocoaPods肯适,那還是先去看看CocoaPods簡介這篇教程吧框舔。請至少看完教程中初始化的步驟樱溉,這樣你才能安裝框架饺窿。

注意:如果不想用CocoaPods肚医,你仍然可以使用ReactiveCocoa肠套,具體查看Github文檔中引入ReactiveCocoa的步驟描述你稚。

譯注:我就是不喜歡用CocoaPods的那波人。所以我首先使用了Github上提供的方法宇弛,但是在第二步執(zhí)行bootstrap時提示缺少xctool枪芒,我就果斷放棄了舅踪,還是乖乖用CocoaPods吧抽碌。

具體怎么使用CocoaPods安裝就不詳細(xì)講解了左权。

開始

就像在介紹中提到的涮总,RAC為應(yīng)用中發(fā)生的不同事件流提供了一個標(biāo)準(zhǔn)接口。在ReactiveCocoa術(shù)語中這個叫做信號(signal)裳扯,由RACSignal類表示。

打開應(yīng)用的初始view controller冤吨,RWViewController.m 漩蟆,引入ReactiveCocoa的頭文件怠李。


#import

不要替換已有的代碼,將下面的代碼添加到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京革,當(dāng)事件發(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)只有當(dāng)輸入超過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ù)流來表達(dá)應(yīng)用的功能的诵。

用圖形來表達(dá)就是下面這樣的:


從上面的圖中可以看到,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);

}];

在上面的代碼中,注釋部分標(biāo)記了將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)當(dāng)輸入內(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)用中动遭,登錄按鈕只有當(dā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)用刪掉。

編譯運行盔性,看看登錄按鈕霞丧。當(dāng)用戶名和密碼輸入有效時,按鈕就是可用的冕香,和以前一樣蛹尝。

現(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)的方法替換艾疟,所以首先要做的就是斷開當(dā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用信號的方式來表示相當(dāng)簡單胡嘿。首先把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描述了這個信號面氓。當(dāng)這個信號有subscriber時,block里的代碼就會執(zhí)行。

block的入?yún)⑹且粋€subscriber實例舌界,它遵循RACSubscriber協(xié)議掘譬,協(xié)議里有一些方法來產(chǎn)生事件,你可以發(fā)送任意數(shù)量的next事件呻拌,或者用error\complete事件來終止葱轩。本例中,信號發(fā)送了一個next事件來表示登錄是否成功藐握,隨后是一個complete事件靴拱。

這個block的返回值是一個RACDisposable對象,它允許你在一個訂閱被取消時執(zhí)行一些清理工作趾娃。當(dāng)前的信號不需要執(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ā)生了什么:


當(dāng)點擊按鈕時,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)在有一些用戶體驗上的小問題了嗎多矮?當(dāng)?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的主旨是讓你的代碼更簡潔易懂膘螟,這值得多想想。我個人認(rèn)為碾局,如果邏輯可以用清晰的管道荆残、流式語法來表示,那就很好理解這個應(yīng)用到底干了什么了净当。

在本系列教程的第二部分内斯,你將會學(xué)到諸如錯誤處理、在不同線程中執(zhí)行代碼等高級用法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末像啼,一起剝皮案震驚了整個濱河市俘闯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忽冻,老刑警劉巖真朗,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異僧诚,居然都是意外死亡遮婶,警方通過查閱死者的電腦和手機(jī)蝗碎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旗扑,“玉大人蹦骑,你說我怎么就攤上這事〖缁恚” “怎么了脊串?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵辫呻,是天一觀的道長清钥。 經(jīng)常有香客問我,道長放闺,這世上最難降的妖魔是什么祟昭? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮怖侦,結(jié)果婚禮上篡悟,老公的妹妹穿的比我還像新娘。我一直安慰自己匾寝,他們只是感情好搬葬,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著艳悔,像睡著了一般急凰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上猜年,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天抡锈,我揣著相機(jī)與錄音,去河邊找鬼乔外。 笑死床三,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的杨幼。 我是一名探鬼主播撇簿,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼差购!你這毒婦竟也來了四瘫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤歹撒,失蹤者是張志新(化名)和其女友劉穎莲组,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體暖夭,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡锹杈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年撵孤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竭望。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡邪码,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咬清,到底是詐尸還是另有隱情闭专,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布旧烧,位于F島的核電站影钉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏掘剪。R本人自食惡果不足惜平委,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望夺谁。 院中可真熱鬧廉赔,春花似錦、人聲如沸匾鸥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽勿负。三九已至馏艾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間笆环,已是汗流浹背攒至。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留躁劣,地道東北人迫吐。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像账忘,于是被迫代替她去往敵國和親志膀。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容