iOS開發,一篇寫得很是好的入門級ReactiveCocoa教程(一)

ReactiveCocoa入門教程——第一部分html

ReactiveCocoa iOS 翻譯    2015-01-21 18:33:37    9657    6    15react

本文翻譯自RayWenderlich  ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2ios


做爲一個iOS開發者,你寫的每一行代碼幾乎都是在響應某個事件,例如按鈕的點擊,收到網絡消息,屬性的變化(經過KVO)或者用戶位置的變化(經過CoreLocation)。可是這些事件都用不一樣的方式來處理,好比action、delegate、KVO、callback等。ReactiveCocoa爲事件定義了一個標準接口,從而可使用一些基本工具來更容易的鏈接、過濾和組合。git


若是你對上面說的還比較疑惑,那仍是繼續往下看吧。github

 

ReactiveCocoa結合了幾種編程風格:objective-c

函數式編程(Functional Programming):使用高階函數,例如函數用其餘函數做爲參數。編程

響應式編程(Reactive Programming):關注於數據流和變化傳播。bootstrap

 

因此,你可能據說過ReactiveCocoa被描述爲函數響應式編程(FRP)框架。網絡

 

這就是這篇教程要講的內容。編程範式是個不錯的主題,可是本篇教程的其他部分將會經過一個例子來實踐。 app

Reactive Playground

經過這篇教程,一個簡單的範例應用Reactive Playground ,你將會了解到響應式編程。下載初始工程,而後編譯運行一下確保你已經把一切都設置正確了。

 

Reactive Playground是一個很是簡單的應用,它爲用戶展現了一個登陸頁。在用戶名框輸入user,在密碼框輸入password,而後你就能看到有一隻可愛小貓咪的歡迎頁了。

 

呀,真是可愛啊。


如今能夠花一些時間來看一下初始工程的代碼。很簡單,用不了多少時間。


打開RWViewController.m看一下。你多快能找到控制登陸按鈕是否可用的條件?判斷顯示/隱藏登陸失敗label的條件是什麼?在這個相對簡單的例子裏,可能只用一兩分鐘就能回答這些問題。可是對於更復雜的例子,這些所花的時間可能就比較多了。

 

使用ReactiveCocoa,可使應用的基本邏輯變得至關簡潔。是時候開始啦。

添加ReactiveCocoa框架

添加ReactiveCocoa框架最簡單的方法就是用CocoaPods。若是你從沒用過CocoaPods,那仍是先去看看CocoaPods簡介這篇教程吧。請至少看完教程中初始化的步驟,這樣你才能安裝框架。

 

注意:若是不想用CocoaPods,你仍然可使用ReactiveCocoa,具體查看Github文檔中引入ReactiveCocoa的步驟描述。

譯註:我就是不喜歡用CocoaPods的那波人。因此我首先使用了Github上提供的方法,可是在第二步執行bootstrap時提示缺乏xctool,我就果斷放棄了,仍是乖乖用CocoaPods吧。


具體怎麼使用CocoaPods安裝就不詳細講解了。 

開動

就像在介紹中提到的,RAC爲應用中發生的不一樣事件流提供了一個標準接口。在ReactiveCocoa術語中這個叫作信號(signal),由RACSignal類表示。

 

打開應用的初始view controller,RWViewController.m ,引入ReactiveCocoa的頭文件。

1#import <ReactiveCocoa/ReactiveCocoa.h> 


不要替換已有的代碼,將下面的代碼添加到viewDidLoad方法的最後:

1[self.usernameTextField.rac_textSignal subscribeNext:^(id x){

2  NSLog(@"%@", x);

3}];

 

編譯運行,在用戶名輸入框中輸幾個字。注意console的輸出應該和下面的相似。

12013-12-24 14:48:50.359 RWReactivePlayground[9193:a0b] i

22013-12-24 14:48:50.436 RWReactivePlayground[9193:a0b] is

32013-12-24 14:48:50.541 RWReactivePlayground[9193:a0b] is 

42013-12-24 14:48:50.695 RWReactivePlayground[9193:a0b] is t

52013-12-24 14:48:50.831 RWReactivePlayground[9193:a0b] is th

62013-12-24 14:48:50.878 RWReactivePlayground[9193:a0b] is thi

72013-12-24 14:48:50.901 RWReactivePlayground[9193:a0b] is this

82013-12-24 14:48:51.009 RWReactivePlayground[9193:a0b] is this 

92013-12-24 14:48:51.142 RWReactivePlayground[9193:a0b] is this m

102013-12-24 14:48:51.236 RWReactivePlayground[9193:a0b] is this ma

112013-12-24 14:48:51.335 RWReactivePlayground[9193:a0b] is this mag

122013-12-24 14:48:51.439 RWReactivePlayground[9193:a0b] is this magi

132013-12-24 14:48:51.535 RWReactivePlayground[9193:a0b] is this magic

142013-12-24 14:48:51.774 RWReactivePlayground[9193:a0b] is this magic?


能夠看到每次改變文本框中的文字,block中的代碼都會執行。沒有target-action,沒有delegate,只有signal和block。使人激動不是嗎?

 

ReactiveCocoa signal(RACSignal)發送事件流給它的subscriber。目前總共有三種類型的事件:nexterrorcompleted。一個signal在因error終止或者完成前,能夠發送任意數量的next事件。在本教程的第一部分,咱們將會關注next事件。在第二部分,將會學習error和completed事件。

 

RACSignal有不少方法能夠來訂閱不一樣的事件類型。每一個方法都須要至少一個block,當事件發生時就會執行block中的邏輯。在上面的例子中能夠看到每次next事件發生時,subscribeNext:方法提供的block都會執行。

 

ReactiveCocoa框架使用category來爲不少基本UIKit控件添加signal。這樣你就能給控件添加訂閱了,text field的rac_textSignal就是這麼來的。

 

原理就說這麼多,是時候開始讓ReactiveCocoa幹活了。

 

ReactiveCocoa有不少操做來控制事件流。假設你只關心超過3個字符長度的用戶名,那麼你可使用filter操做來實現這個目的。把以前加在viewDidLoad中的代碼更新成下面的:

1[[self.usernameTextField.rac_textSignal

2filter:^BOOL(id value){

3   NSString*text = value;

4   return text.length > 3;

5}]

6subscribeNext:^(id x){

7   NSLog(@"%@", x);

8  }];

 

編譯運行,在text field只能怪輸入幾個字,你會發現只有當輸入超過3個字符時纔會有log。

12013-12-26 08:17:51.335 RWReactivePlayground[9654:a0b] is t

22013-12-26 08:17:51.478 RWReactivePlayground[9654:a0b] is th

32013-12-26 08:17:51.526 RWReactivePlayground[9654:a0b] is thi

42013-12-26 08:17:51.548 RWReactivePlayground[9654:a0b] is this

52013-12-26 08:17:51.676 RWReactivePlayground[9654:a0b] is this 

62013-12-26 08:17:51.798 RWReactivePlayground[9654:a0b] is this m

72013-12-26 08:17:51.926 RWReactivePlayground[9654:a0b] is this ma

82013-12-26 08:17:51.987 RWReactivePlayground[9654:a0b] is this mag

92013-12-26 08:17:52.141 RWReactivePlayground[9654:a0b] is this magi

102013-12-26 08:17:52.229 RWReactivePlayground[9654:a0b] is this magic

112013-12-26 08:17:52.486 RWReactivePlayground[9654:a0b] is this magic?

 

剛纔所建立的只是一個很簡單的管道。這就是響應式編程的本質,根據數據流來表達應用的功能。

 

用圖形來表達就是下面這樣的:

 

從上面的圖中能夠看到,rac_textSignal是起始事件。而後數據經過一個filter,若是這個事件包含一個長度超過3的字符串,那麼該事件就能夠經過。管道的最後一步就是subscribeNext:,block在這裏打印出事件的值。

 

filter操做的輸出也是RACSignal,這點先放到一邊。你能夠像下面那樣調整一下代碼來展現每一步的操做。

1RACSignal *usernameSourceSignal =

2    self.usernameTextField.rac_textSignal;

4RACSignal *filteredUsername =[usernameSourceSignal

5  filter:^BOOL(id value){

6    NSString*text = value;

7    return text.length > 3;

8  }];

10[filteredUsername subscribeNext:^(id x){

11  NSLog(@"%@", x);

12}];

 

RACSignal的每一個操做都會返回一個RACsignal,這在術語上叫作連貫接口fluent interface)。這個功能可讓你直接構建管道,而不用每一步都使用本地變量。

 

注意:ReactiveCocoa大量使用block。若是你是block新手,你可能想看看Apple官方的block編程指南。若是你熟悉block,可是以爲block的語法有些奇怪和難記,你可能會想看看這個有趣又實用的網頁f*****gblocksyntax.com

類型轉換

若是你以前把代碼分紅了多個步驟,如今再把它改回來吧。。。。。。。。

1[[self.usernameTextField.rac_textSignal

2  filter:^BOOL(id value){

3    NSString*text = value; // implicit cast

4    return text.length > 3;

5  }]

6  subscribeNext:^(id x){

7    NSLog(@"%@", x);

8  }];

 

在上面的代碼中,註釋部分標記了將id隱式轉換爲NSString,這看起來不是很好看。幸運的是,傳入block的值確定是個NSString,因此你能夠直接修改參數類型,把代碼更新成下面的這樣的:

1[[self.usernameTextField.rac_textSignal

2  filter:^BOOL(NSString*text){

3    return text.length > 3;

4  }]

5  subscribeNext:^(id x){

6    NSLog(@"%@", x);

7  }];


編譯運行,確保沒什麼問題。

什麼是事件呢?

到目前爲止,本篇教程已經描述了不一樣的事件類型,可是尚未說明這些事件的結構。有意思的是(?),事件能夠包括任何事情。

 

下面來展現一下,在管道中添加另外一個操做。把添加在viewDidLoad中的代碼更新成下面的:

1[[[self.usernameTextField.rac_textSignal

2  map:^id(NSString*text){

3    return @(text.length);

4  }]

5  filter:^BOOL(NSNumber*length){

6    return[length integerValue] > 3;

7  }]

8  subscribeNext:^(id x){

9    NSLog(@"%@", x);

10  }];

 

編譯運行,你會發現log輸出變成了文本的長度而不是內容。

12013-12-26 12:06:54.566 RWReactivePlayground[10079:a0b] 4

22013-12-26 12:06:54.725 RWReactivePlayground[10079:a0b] 5

32013-12-26 12:06:54.853 RWReactivePlayground[10079:a0b] 6

42013-12-26 12:06:55.061 RWReactivePlayground[10079:a0b] 7

52013-12-26 12:06:55.197 RWReactivePlayground[10079:a0b] 8

62013-12-26 12:06:55.300 RWReactivePlayground[10079:a0b] 9

72013-12-26 12:06:55.462 RWReactivePlayground[10079:a0b] 10

82013-12-26 12:06:55.558 RWReactivePlayground[10079:a0b] 11

92013-12-26 12:06:55.646 RWReactivePlayground[10079:a0b] 12

 

新加的map操做經過block改變了事件的數據。map從上一個next事件接收數據,經過執行block把返回值傳給下一個next事件。在上面的代碼中,map以NSString爲輸入,取字符串的長度,返回一個NSNumber

 

來看下面的圖片:

 


能看到map操做以後的步驟收到的都是NSNumber實例。你可使用map操做來把接收的數據轉換成想要的類型,只要它是個對象。

 

注意:在上面的例子中text.length返回一個NSUInteger,是一個基本類型。爲了將它做爲事件的內容,NSUInteger必須被封裝。幸運的是Objective-C literal syntax提供了一種簡單的方法來封裝——@ (text.length)

 

如今差很少是時候用所學的內容來更新一下ReactivePlayground應用了。你能夠把以前的添加代碼都刪除了。。。。。。

建立有效狀態信號

首先要作的就是建立一些信號,來表示用戶名和密碼輸入框中的輸入內容是否有效。把下面的代碼添加到RWViewController.mviewDidLoad的最後面:

1RACSignal *validUsernameSignal =

2 [self.usernameTextField.rac_textSignal

3 map:^id(NSString *text) {

4 return @([self isValidUsername:text]);

5 }]; 

6RACSignal *validPasswordSignal =

7 [self.passwordTextField.rac_textSignal 

8 map:^id(NSString *text) { 

9 return @([self isValidPassword:text]);

10 }];


能夠看到,上面的代碼對每一個輸入框的rac_textSignal應用了一個map轉換。輸出是一個用NSNumber封裝的布爾值。


下一步是轉換這些信號,從而能爲輸入框設置不一樣的背景顏色。基本上就是,你訂閱這些信號,而後用接收到的值來更新輸入框的背景顏色。下面有一種方法:

1[[validPasswordSignal

2  map:^id(NSNumber *passwordValid){

3    return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];

4  }]

5  subscribeNext:^(UIColor *color){

6    self.passwordTextField.backgroundColor = color;

7  }];

(不要使用這段代碼,下面有一種更好的寫法!)


從概念上來講,就是把以前信號的輸出應用到輸入框的backgroundColor屬性上。可是上面的用法不是很好。


幸運的是,ReactiveCocoa提供了一個宏來更好的完成上面的事情。把下面的代碼直接加到viewDidLoad中兩個信號的代碼後面:

1RAC(self.passwordTextField, backgroundColor) =

2  [validPasswordSignal

3    map:^id(NSNumber *passwordValid){

4      return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];

5    }];

7RAC(self.usernameTextField, backgroundColor) =

8  [validUsernameSignal

9    map:^id(NSNumber *passwordValid){

10     return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];

11   }];


RAC宏容許直接把信號的輸出應用到對象的屬性上。RAC宏有兩個參數,第一個是須要設置屬性值的對象,第二個是屬性名。每次信號產生一個next事件,傳遞過來的值都會應用到該屬性上。


你不以爲這種方法很好嗎?


在編譯運行以前,找到updateUIState方法,把頭兩行刪掉。

1self.usernameTextField.backgroundColor = 

2    self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor]; 

3self.passwordTextField.backgroundColor = 

4    self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];

這樣就把不相關的代碼刪掉了。


編譯運行,能夠發現當輸入內容無效時,輸入框看起來高亮了,有效時又透明瞭。


如今的邏輯用圖形來表示就是下面這樣的。能看到有兩條簡單的管道,兩個文本信號,通過一個map轉爲表示是否有效的布爾值,再通過一個map轉爲UIColor,而這個UIColor已經和輸入框的背景顏色綁定了。

 


你是否好奇爲何要建立兩個分開的validPasswordSignalvalidUsernameSignal呢,而不是每一個輸入框一個單獨的管道呢?(?)稍安勿躁,答案就在下面。

原文: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!

聚合信號

目前在應用中,登陸按鈕只有當用戶名和密碼輸入框的輸入都有效時才工做。如今要把這裏改爲響應式的。


如今的代碼中已經有能夠產生用戶名和密碼輸入框是否有效的信號了——validUsernameSignalvalidPasswordSignal了。如今須要作的就是聚合這兩個信號來決定登陸按鈕是否可用。


把下面的代碼添加到viewDidLoad的末尾:

1RACSignal *signUpActiveSignal =

2  [RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]

3                    reduce:^id(NSNumber*usernameValid, NSNumber *passwordValid){

4                      return @([usernameValid boolValue]&&[passwordValid boolValue]);

5                    }];


上面的代碼使用combineLatest:reduce:方法把validUsernameSignalvalidPasswordSignal產生的最新的值聚合在一塊兒,並生成一個新的信號。每次這兩個源信號的任何一個產生新值時,reduce block都會執行,block的返回值會發給下一個信號。


注意:RACsignal的這個方法能夠聚合任意數量的信號,reduce block的參數和每一個源信號相關。ReactiveCocoa有一個工具類RACBlockTrampoline,它在內部處理reduce block的可變參數。實際上在ReactiveCocoa的實現中有不少隱藏的技巧,值得你去看看。


如今已經有了合適的信號,把下面的代碼添加到viewDidLoad的末尾。這會把信號和按鈕的enabled屬性綁定。

1[signUpActiveSignal subscribeNext:^(NSNumber*signupActive){

2   self.signInButton.enabled =[signupActive boolValue];

3 }];


在運行以前,把之前的舊實現刪掉。把下面這兩個屬性刪掉。

1@property (nonatomic) BOOL passwordIsValid;

2@property (nonatomic) BOOL usernameIsValid;


把viewDidLoad中的這些也刪掉:

1// handle text changes for both text fields

2[self.usernameTextField addTarget:self

3                           action:@selector(usernameTextFieldChanged)

4                 forControlEvents:UIControlEventEditingChanged];

5[self.passwordTextField addTarget:self

6                           action:@selector(passwordTextFieldChanged)

7                forControlEvents:UIControlEventEditingChanged];


一樣把updateUIStateusernameTextFieldChangedpasswordTextFieldChanged方法刪掉。

最後確保把viewDidLoadupdateUIState的調用刪掉。


編譯運行,看看登陸按鈕。當用戶名和密碼輸入有效時,按鈕就是可用的,和之前同樣。


如今應用的邏輯就是下面這樣的:

 


上圖展現了一些重要的概念,你可使用ReactiveCocoa來完成一些重量級的任務。

•分割——信號能夠有不少subscriber,也就是做爲不少後續步驟的源。注意上圖中那個用來表示用戶名和密碼有效性的布爾信號,它被分割成多個,用於不一樣的地方。

•聚合——多個信號能夠聚合成一個新的信號,在上面的例子中,兩個布爾信號聚合成了一個。實際上你能夠聚合併產生任何類型的信號。


這些改動的結果就是,代碼中沒有用來表示兩個輸入框有效狀態的私有屬性了。這就是用響應式編程的一個關鍵區別,你不須要使用實例變量來追蹤瞬時狀態。

響應式的登陸

應用目前使用上面圖中展現的響應式管道來管理輸入框和按鈕的狀態。可是按鈕按下的處理用的仍是action,因此下一步就是把剩下的邏輯都替換成響應式的。


在storyboard中,登陸按鈕的Touch Up Inside事件和RWViewController.m中的signInButtonTouched方法是綁定的。下面會用響應的方法替換,因此首先要作的就是斷開當前的storyboard action。


打開Main.storyboard,找到登陸按鈕,按住ctrl鍵單擊,打開outlet/action鏈接框,而後點擊x來斷開鏈接。若是你找不到的話,下圖中紅色箭頭指示的就是刪除按鈕。

 


你已經知道了ReactiveCocoa框架是如何給基本UIKit控件添加屬性和方法的了。目前你已經使用了rac_textSignal,它會在文本發生變化時產生信號。爲了處理按鈕的事件,如今須要用到ReactiveCocoa爲UIKit添加的另外一個方法,rac_signalForControlEvents


如今回到RWViewController.m,把下面的代碼添加到viewDidLoad的末尾:

1[[self.signInButton

2   rac_signalForControlEvents:UIControlEventTouchUpInside]

3   subscribeNext:^(id x) {

4     NSLog(@"button clicked");

5   }];

 

上面的代碼從按鈕的UIControlEventTouchUpInside事件建立了一個信號,而後添加了一個訂閱,在每次事件發生時都會輸出log。


編譯運行,確保的確有log輸出。按鈕只在用戶名和密碼框輸入有效時可用,因此在點擊按鈕前須要在兩個文本框中輸入一些內容。


能夠看到Xcode控制檯的輸出和下面的相似:

12013-12-28 08:05:10.816 RWReactivePlayground[18203:a0b] button clicked

22013-12-28 08:05:11.675 RWReactivePlayground[18203:a0b] button clicked

32013-12-28 08:05:12.605 RWReactivePlayground[18203:a0b] button clicked

42013-12-28 08:05:12.766 RWReactivePlayground[18203:a0b] button clicked

52013-12-28 08:05:12.917 RWReactivePlayground[18203:a0b] button clicked


如今按鈕有了點擊事件的信號,下一步就是把它和登陸流程鏈接起來。那麼問題就來了,打開RWDummySignInService.h,看一下接口:

1typedef void (^RWSignInResponse)(BOOL);

3@interface RWDummySignInService : NSObject

5- (void)signInWithUsername:(NSString *)username

6                  password:(NSString *)password

7                  complete:(RWSignInResponse)completeBlock;

8                  

9@end


這個service有3個參數,用戶名、密碼和一個完成回調block。這個block會在登陸成功或失敗時執行。你能夠在按鈕點擊事件的subscribeNext: blcok裏直接調用這個方法,可是爲何你要這麼作?(?)


注意:本教程爲了簡便使用了一個假的service,因此它不依賴任何外部API。但你如今的確遇到了一個問題,如何使用這些不是用信號表示的API呢?

建立信號

幸運的是,把已有的異步API用信號的方式來表示至關簡單。首先把RWViewController.m中的signInButtonTouched:刪掉。你會用響應式的的方法來替換這段邏輯。


仍是在RWViewController.m中,添加下面的方法:

1- (RACSignal *)signInSignal {

2return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber){

3   [self.signInService 

4     signInWithUsername:self.usernameTextField.text

5               password:self.passwordTextField.text

6               complete:^(BOOL success){

7                    [subscriber sendNext:@(success)];

8                    [subscriber sendCompleted];

9     }];

10   return nil;

11}];

12}


上面的方法建立了一個信號,使用用戶名和密碼登陸。如今分解來看一下。


上面的代碼使用RACSignalcreateSignal:方法來建立信號。方法的入參是一個block,這個block描述了這個信號。當這個信號有subscriber時,block裏的代碼就會執行。


block的入參是一個subscriber實例,它遵循RACSubscriber協議,協議裏有一些方法來產生事件,你能夠發送任意數量的next事件,或者用error\complete事件來終止。本例中,信號發送了一個next事件來表示登陸是否成功,隨後是一個complete事件。


這個block的返回值是一個RACDisposable對象,它容許你在一個訂閱被取消時執行一些清理工做。當前的信號不須要執行清理操做,因此返回nil就能夠了。


能夠看到,把一個異步API用信號封裝是多簡單!


如今就來使用這個新的信號。把以前添加在viewDidLoad中的代碼更新成下面這樣的:

1[[[self.signInButton

2   rac_signalForControlEvents:UIControlEventTouchUpInside]

3   map:^id(id x){

4     return[self signInSignal];

5   }]

6   subscribeNext:^(id x){

7     NSLog(@"Sign in result: %@", x);

8   }];


上面的代碼使用map方法,把按鈕點擊信號轉換成了登陸信號。subscriber輸出log。


編譯運行,點擊登陸按鈕,查看Xcode的控制檯,等等,輸出的這是個什麼鬼?

12014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign in result:

2                                   <RACDynamicSignal: 0xa068a00> name: +createSignal:



沒錯,你已經給subscribeNext:的block傳入了一個信號,但傳入的不是登陸結果的信號。


下圖展現了到底發生了什麼:

 

當點擊按鈕時,rac_signalForControlEvents發送了一個next事件(事件的data是UIButton)。map操做建立並返回了登陸信號,這意味着後續步驟都會收到一個RACSignal。這就是你在subscribeNext:這步看到的。


上面問題的解決方法,有時候叫作信號中的信號,換句話說就是一個外部信號裏面還有一個內部信號。你能夠在外部信號的subscribeNext:block裏訂閱內部信號。不過這樣嵌套太混亂啦,還好ReactiveCocoa已經解決了這個問題。

信號中的信號

解決的方法很簡單,只須要把map操做改爲flattenMap就能夠了:

1[[[self.signInButton

2   rac_signalForControlEvents:UIControlEventTouchUpInside]

3   flattenMap:^id(id x){

4     return[self signInSignal];

5   }]

6   subscribeNext:^(id x){

7     NSLog(@"Sign in result: %@", x);

8   }];


這個操做把按鈕點擊事件轉換爲登陸信號,同時還從內部信號發送事件到外部信號。


編譯運行,注意控制檯,如今應該輸出登陸是否成功了。

12013-12-28 18:20:08.156 RWReactivePlayground[22993:a0b] Sign in result: 0

22013-12-28 18:25:50.927 RWReactivePlayground[22993:a0b] Sign in result: 1


還不錯。


如今已經完成了大部分的內容,最後就是在subscribeNext步驟裏添加登陸成功後跳轉的邏輯。把代碼更新成下面的:

1[[[self.signInButton

2rac_signalForControlEvents:UIControlEventTouchUpInside]

3flattenMap:^id(id x){

4   return[self signInSignal];

5}]

6subscribeNext:^(NSNumber*signedIn){

7   BOOL success =[signedIn boolValue];

8   self.signInFailureText.hidden = success;

9   if(success){

10     [self performSegueWithIdentifier:@"signInSuccess" sender:self];

11   }

12  }];


subscribeNext: block從登陸信號中取得結果,相應地更新signInFailureText是否可見。若是登陸成功執行導航跳轉。


編譯運行,應該就能再看到可愛的小貓啦!喵~

 


你注意到這個應用如今有一些用戶體驗上的小問題了嗎?當登陸service正在校驗用戶名和密碼時,登陸按鈕應該是不可點擊的。這會防止用戶屢次執行登陸操做。還有,若是登陸失敗了,用戶再次嘗試登陸時,應該隱藏錯誤信息。


這個邏輯應該怎麼添加呢?改變按鈕的可用狀態並非轉換(map)、過濾(filter)或者其餘已經學過的概念。其實這個就叫作「反作用」,換句話說就是在一個next事件發生時執行的邏輯,而該邏輯並不改變事件自己。

添加附加操做(Adding side-effects)

把代碼更新成下面的:

1[[[[self.signInButton

2   rac_signalForControlEvents:UIControlEventTouchUpInside]

3   doNext:^(id x){

4     self.signInButton.enabled =NO;

5     self.signInFailureText.hidden =YES;

6   }]

7   flattenMap:^id(id x){

8     return[self signInSignal];

9   }]

10   subscribeNext:^(NSNumber*signedIn){

11     self.signInButton.enabled =YES;

12     BOOL success =[signedIn boolValue];

13     self.signInFailureText.hidden = success;

14     if(success){

15       [self performSegueWithIdentifier:@"signInSuccess" sender:self];

16     }

17   }];


你能夠看到doNext:是直接跟在按鈕點擊事件的後面。並且doNext: block並無返回值。由於它是附加操做,並不改變事件自己。


上面的doNext: block把按鈕置爲不可點擊,隱藏登陸失敗提示。而後在subscribeNext: block裏從新把按鈕置爲可點擊,並根據登陸結果來決定是否顯示失敗提示。


以前的管道圖就更新成下面這樣的:

 


編譯運行,確保登陸按鈕的可點擊狀態和預期的同樣。


如今全部的工做都已經完成了,這個應用已是響應式的啦。


若是你中途哪裏出了問題,能夠下載最終的工程(依賴庫都有),或者在Github上找到這份代碼,教程中的每一次編譯運行都有對應的commit。


注意:在異步操做執行的過程當中禁用按鈕是一個常見的問題,ReactiveCocoa也能很好的解決。RACCommand就包含這個概念,它有一個enabled信號,能讓你把按鈕的enabled屬性和信號綁定起來。你也許想試試這個類。

總結

但願本教程爲你從此在本身的應用中使用ReactiveCocoa打下了一個好的基礎。你可能須要一些練習來熟悉這些概念,但就像是語言或者編程,一旦你夯實基礎,用起來也就很簡單了。ReactiveCocoa的核心就是信號,而它不過就是事件流。還能再更簡單點嗎?


在使用ReactiveCocoa後,我發現了一個有趣的事情,那就是你能夠用不少種不一樣的方法來解決同一個問題。你能夠用教程中的例子試試,調整一下信號,改改信號的分割和聚合。


ReactiveCocoa的主旨是讓你的代碼更簡潔易懂,這值得多想一想。我我的認爲,若是邏輯能夠用清晰的管道、流式語法來表示,那就很好理解這個應用到底幹了什麼了。


在本系列教程的第二部分,你將會學到諸如錯誤處理、在不一樣線程中執行代碼等高級用法。

相關文章
相關標籤/搜索