ReactiveCocoa入門教程——第二部分

ReactiveCocoa是一個框架,它能讓你在iOS應用中使用函數響應式編程(FRP)技術。在本系列教程的第一部分中,你學到了如何將標準的動做與事件處理邏輯替換爲發送事件流的信號。你還學到了如何轉換、分割和聚合這些信號。html

 

在本系列教程的第二部分,你將會學到一些ReactiveCocoa的高級功能,包括:react

  • 另外兩個事件類型:error 和 completedios

  • 節流git

  • 線程github

  • 延伸編程

  • 其餘json

 

是時候深刻研究一下了。設計模式

Twitter Instant

在本教程中你將要開發的應用叫Twitter Instant(基於Google Instant的概念),這個應用能搜索Twitter上的內容,並根據輸入實時更新搜索結果。api

 

這個應用的初始工程包括一些基本的UI和必須的代碼。和第一部分同樣,你須要使用CocoaPods來獲取ReactiveCocoa框架,並集成到項目中。初始工程已經包含必須的Podfile,因此打開終端,執行下面的命令:數組

pod install

 

若是執行正確的話,你能看到和下面相似的輸出:

Analyzing dependencies Downloading dependencies Using ReactiveCocoa (2.1.8) Generating Pods project Integrating client project

 

這會生成一個Xcode workspcae,TwitterInstant.xcworkspace 。在Xcode中打開它,確認其中包含兩個項目:

  • TwitterInstant :應用的邏輯就在這裏。

  • Pods :這裏是外部依賴。目前只包含ReactiveCocoa。

 

構建運行,就能看到下面的界面:

 

花一些時間來熟悉應用的代碼。這個是一個很簡單的應用,基於split view controller。左欄是RWSearchFormViewController,它經過storyboard在上面添加了一些UI控件,經過outlet鏈接了search text field。右欄是RWSearchResultsViewController,目前只是UITableViewController的子類。

 

打開RWSearchFormViewController.m,能看到在viewDidLoad方法中,首先定位到results view controller,而後把它分配給resultsViewController私有屬性。應用的主要邏輯都會集中在RWSearchFormViewController,這個屬性能把搜索結果提供給RWSearchResultsViewController

 

驗證搜索文本的有效性

首先要作的就是驗證搜索文本,來確保文本長度大於2個字符。若是你完成了本系列教程的第一部分,那這個應該很熟悉。

 

RWSearchFormViewController.m中的viewDidLoad 下面添加下面的方法:

- (BOOL)isValidSearchText:(NSString *)text {    return text.length > 2;}

 

這個方法就只是確保要搜索的字符串長度大於2個字符。這個邏輯很簡單,你可能會問「爲何要在工程文件中寫這麼一個單獨的方法呢?」。

 

目前驗證輸入有效性的邏輯的確很簡單,但若是未來邏輯須要變得更復雜呢?若是是像上面的例子中那樣,那你就只須要修改一個地方。並且這樣寫能讓你代碼的可讀性更高,代碼自己就說明了你爲何要檢查字符串的長度。

 

在RWSearchFormViewController.m的最上面,引入ReactiveCocoa:

#import <ReactiveCocoa.h>


把下面的代碼加到viewDidLoad的最下面 :

[[self.searchText.rac_textSignal     map:^id(NSString *text) {        return [self isValidSearchText:text] ?             [UIColor whiteColor] : [UIColor yellowColor];    }]     subscribeNext:^(UIColor *color) {         self.searchText.backgroundColor = color; }];

 

上面的代碼作了什麼呢?

  • 獲取search text field 的text signal

  • 將其轉換爲顏色來標示輸入是否有效

  • 而後在subscribeNext:block裏將顏色應用到search text field的backgroundColor屬性

 

構建運行,觀察在輸入文本太短時,text field的背景會變成黃色來標示輸入無效。

 

用圖形來表示的話,流程和下面的相似:

 

當text field中的文字每次發生變化時,rac_textSignal都會發送一個next 事件,事件包含當前text field中的文字。map這一步將文本值轉換成了顏色值,因此subscribeNext:這一步會拿到這個顏色值,並應用在text field的背景色上。

 

你應該還記得本系列教程第一部分裏這些內容吧?若是忘了,建議你先停在這裏,回去看一下第一部分。

 

在添加Twitter搜索邏輯以前,還有一些有意思的話題要說說。

 

格式化代碼

當你在探索如何格式化ReactiveCocoa的代碼時,慣例是每一個操做新起一行,垂直對齊每一個步驟。

 

在下圖中你能看到比較複雜的代碼是如何對齊的,這是第一部分教程中的代碼。

 

這樣對齊能讓你很容易的看到每一步的操做。同時你還應該減小每一個block中的代碼量,若是block中的代碼超過幾行時,就應該新寫一個私有方法。

 

很不幸的是,Xcode不是很喜歡這種風格的格式化,因此你會發現Xcode的自動縮進邏輯老是和你過不去。 

 

內存管理

看一下你添加到TwitterInstant中的代碼,你是否好奇建立的這些管道是如何持有的呢?顯然,它並無分配給某個變量或是屬性,因此它也不會有引用計數的增長,那它是怎麼銷燬的呢?

 

ReactiveCocoa設計的一個目標就是支持匿名生成管道這種編程風格。到目前爲止,在你所寫的全部響應式代碼中,這應該是很直觀的。

 

爲了支持這種模型,ReactiveCocoa本身持有全局的全部信號。若是一個signal有一個或多個訂閱者,那這個signal就是活躍的。若是全部的訂閱者都被移除了,那這個信號就能被銷燬了。更多關於ReactiveCocoa如何管理這一過程,參見文檔Memory Management

 

上面說的就引出了最後一個問題:如何取消訂閱一個signal?在一個completed或者error事件以後,訂閱會自動移除(立刻就會講到)。你還能夠經過RACDisposable 手動移除訂閱。

 

RACSignal的訂閱方法都會返回一個RACDisposable實例,它能讓你經過dispose方法手動移除訂閱。下面是一個例子:

RACSignal *backgroundColorSignal =    [self.searchText.rac_textSignal         map:^id(NSString *text) {             return [self isValidSearchText:text] ?                 [UIColor whiteColor] : [UIColor yellowColor];     }];     RACDisposable *subscription =     [backgroundColorSignal         subscribeNext:^(UIColor *color) {            self.searchText.backgroundColor = color;     }];     // at some point in the future ... [subscription dispose];

 

你會發現這個方法並不經常使用到,可是仍是有必要知道能夠這樣作。

 

注意:根據上面所說的,若是你建立了一個管道,可是沒有訂閱它,這個管道就不會執行,包括任何如doNext: block的附加操做。

 

避免循環引用

ReactiveCocoa已經在幕後作了不少事情,這也就意味着你並不須要太多關注signal的內存管理。可是還有一個很重要的內存相關問題你須要注意。

 

看一下你剛纔添加的代碼:

[[self.searchText.rac_textSignal     map:^id(NSString *text) {         return [self isValidSearchText:text] ?             [UIColor whiteColor] : [UIColor yellowColor];     }]     subscribeNext:^(UIColor *color) {         self.searchText.backgroundColor = color;     }];

 

subscribeNext:block中使用了self來獲取text field的引用。block會捕獲並持有其做用域內的值。所以,若是self和這個信號之間存在一個強引用的話,就會形成循環引用。循環引用是否會形成問題,取決於self對象的生命週期。若是self的生命週期是整個應用運行時,好比說本例,那也就無傷大雅。可是在更復雜一些的應用中,就不是這麼回事了。

 

爲了不潛在的循環引用,Apple的文檔Working With Blocks中建議獲取一個self的弱引用。用本例來講就是下面這樣的:

__weak RWSearchFormViewController *bself = self; // Capture the weak reference  [[self.searchText.rac_textSignal     map:^id(NSString *text) {         return [self isValidSearchText:text] ?             [UIColor whiteColor] : [UIColor yellowColor];     }]     subscribeNext:^(UIColor *color) {         bself.searchText.backgroundColor = color;     }];

 

在上面的代碼中,__weak修飾符使bself成爲了self的一個弱引用。注意如今subscribeNext:block中使用bself變量。不過這種寫法看起來不是那麼優雅。

 

ReactiveCocoa框架包含了一個語法糖來替換上面的代碼。在文件頂部添加下面的代碼:

#import "RACEXTScope.h"

 

而後把代碼替換成下面的:

@weakify(self) [[self.searchText.rac_textSignal     map:^id(NSString *text) {         return [self isValidSearchText:text] ?             [UIColor whiteColor] : [UIColor yellowColor];     }]     subscribeNext:^(UIColor *color) {         @strongify(self)         self.searchText.backgroundColor = color;     }];

 

上面的@weakify 和 @strongify 語句是在Extended Objective-C庫中定義的宏,也被包括在ReactiveCocoa中。@weakify宏讓你建立一個弱引用的影子對象(若是你須要多個弱引用,你能夠傳入多個變量),@strongify讓你建立一個對以前傳入@weakify對象的強引用。

 

注意:若是你有興趣瞭解@weakify 和 @strongify 實際上作了什麼,在Xcode中,選擇Product -> Perform Action -> Preprocess 「RWSearchForViewController」。這會對view controller 進行預處理,展開全部的宏,以便你能看到最終的輸出。

 

最後須要注意的一點,在block中使用實例變量時請當心謹慎。這也會致使block捕獲一個self的強引用。你能夠打開一個編譯警告,當發生這個問題時能提醒你。在項目的build settings中搜索「retain」,找到下面顯示的這個選項:

 

好了,你已經經過理論的考驗,祝賀你。如今你應該可以開始有意思的部分了:爲你的應用添加一些真正的功能!

 

注意:大家中一些眼尖的讀者,那些關注了上一篇教程的讀者,無疑已經注意到能夠在目前的管道中移除subscribeNext:block,轉而使用RAC宏。若是你發現了這個,修改代碼,而後獎勵本身一個小星星吧~

 

請求訪問Twitter

你將要使用Social Framework來讓TwitterInstant應用能搜索Twitter的內容,使用Accounts Framework來獲取Twitter的訪問權限。關於Social Framework的更詳細內容,參見iOS 6 by Tutorials中的相關章節。

 

在你添加代碼以前,你須要在模擬器或者iPad真機上輸入Twitter的登陸信息。打開設置應用,選擇Twitter選項,而後在屏幕右邊的頁面中輸入登陸信息。

 

初始工程已經添加了須要的框架,因此你只需引入頭文件。在RWSearchFormViewController.m中,添加下面的引用。

#import <Accounts/Accounts.h>#import <Social/Social.h> 

 

就在引用的下面,添加下面的枚舉和常量:

typedef NS_ENUM(NSInteger, RWTwitterInstantError) {    RWTwitterInstantErrorAccessDenied,     RWTwitterInstantErrorNoTwitterAccounts,     RWTwitterInstantErrorInvalidResponse };  static NSString * const RWTwitterInstantDomain = @"TwitterInstant";

 

一下子你就要用到它們來標示錯誤。

 

仍是在這個文件中,在已有屬性聲明的下面,添加下面的代碼:

@property (strong, nonatomic) ACAccountStore *accountStore; @property (strong, nonatomic) ACAccountType *twitterAccountType;

 

ACAccountsStore類能讓你訪問你的設備能鏈接到的多個社交媒體帳號,ACAccountType類則表明帳戶的類型。

 

仍是在這個文件中,把下面的代碼添加到viewDidLoad的最下面:

self.accountStore = [[ACAccountStore alloc] init]; self.twitterAccountType = [self.accountStore     accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

 

上面的代碼建立了一個account store和Twitter帳戶標識符。

 

當應用獲取訪問社交媒體帳號的權限時,用戶會看見一個彈框。這是一個異步操做,所以把這封裝進一個signal是很好的選擇。

 

仍是在這個文件中,添加下面的代碼:

- (RACSignal *)requestAccessToTwitterSignal {    // 1 - define an error     NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain                                                code:RWTwitterInstantErrorAccessDenied                                            userInfo:nil];                                           // 2 - create the signal     @weakify(self)     return [RACSignal createSignal:^RACDisposable *(id subscriber) {         // 3 - request access to twitter         @strongify(self)         [self.accountStore requestAccessToAccountsWithType:self.twitterAccountType                options:nil             completion:^(BOOL granted, NSError *error) {            // 4 - handle the response             if (!granted) {                [subscriber sendError:accessError];             } else {                 [subscriber sendNext:nil];                 [subscriber sendCompleted];             }         }];     return nil;     }]; }

 

這個方法作了下面幾件事:

  1. 定義了一個error,當用戶拒絕訪問時發送。

  2. 和第一部分同樣,類方法createSignal返回一個RACSignal實例。

  3. 經過account store請求訪問Twitter。此時用戶會看到一個彈框來詢問是否容許訪問Twitter帳戶。

  4. 在用戶容許或拒絕訪問以後,會發送signal事件。若是用戶容許訪問,會發送一個next事件,緊跟着再發送一個completed事件。若是用戶拒絕訪問,會發送一個error事件。

 

回憶一下教程的第一部分,signal能發送3種不一樣類型的事件:

  • Next

  • Completed

  • Error

 

在signal的生命週期中,它可能不發送事件,發送一個或多個next事件,在這以後還能發送一個completed事件或一個error事件。

 

最後,爲了使用這個signal,把下面的代碼添加到viewDidLoad的最下面:

[[self requestAccessToTwitterSignal]     subscribeNext:^(id x) {         NSLog(@"Access granted");     } error:^(NSError *error) {         NSLog(@"An error occurred: %@", error);     }];

 

構建運行,應該能看到下面這樣的提示:

 

若是你點擊OK,控制檯裏就會顯示subscribeNext:block中的log信息了。若是你點擊Don't Allow,那麼錯誤block就會執行,而且打印相應的log信息。

 

Acounts Framework會記住你的選擇。所以爲了測試這兩個選項,你須要經過 iOS Simulator -> Reset Contents and Settings 來重置模擬器。這個有點麻煩,由於你還須要再次輸入Twitter的登陸信息。

 

連接signal

一旦用戶容許訪問Twitter帳號(但願如此),應用就應該一直監測search text filed的變化,以便搜索Twitter的內容。

 

應用應該等待獲取訪問Twitter權限的signal發送completed事件,而後再訂閱text field的signal。按順序連接不一樣的signal是一個常見的問題,可是ReactiveCocoa處理的很好。

 

viewDidLoad中當前管道的代碼替換成下面的:

[[[self requestAccessToTwitterSignal]     then:^RACSignal *{         @strongify(self)         return self.searchText.rac_textSignal;     }]     subscribeNext:^(id x) {         NSLog(@"%@", x);     } error:^(NSError *error) {         NSLog(@"An error occurred: %@", error);     }]; 

 

then方法會等待completed事件的發送,而後再訂閱由then block返回的signal。這樣就高效地把控制權從一個signal傳遞給下一個。

 

注意:你在以前的代碼中已經把self轉成弱引用了,因此就不用在這個管道以前再寫@weakify(self)了。

 

then方法會跳過error事件,所以最終的subscribeNext:error:  block仍是會收到獲取訪問權限那一步發送的error事件。

 

構建運行,而後容許訪問,你應該能看到search text field的輸入會在控制檯裏輸出。

2014-01-04 08:16:11.444 TwitterInstant[39118:a0b] m 2014-01-04 08:16:12.276 TwitterInstant[39118:a0b] ma 2014-01-04 08:16:12.413 TwitterInstant[39118:a0b] mag 2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi 2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic 2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!

 

接下來,在管道中添加一個filter操做來過濾掉無效的輸入。在本例裏就是長度不夠3個字符的字符串:

[[[[self requestAccessToTwitterSignal]     then:^RACSignal *{         @strongify(self)         return self.searchText.rac_textSignal;     }]     filter:^BOOL(NSString *text) {         @strongify(self)         return [self isValidSearchText:text];     }]     subscribeNext:^(id x) {         NSLog(@"%@", x);     } error:^(NSError *error) {         NSLog(@"An error occurred: %@", error);     }];

 

 再次構建運行,觀察過濾器的工做:

2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi 2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic 2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!

 

如今用圖形來表示管道,就和下圖相似:

 

管道從requestAccessToTwitterSignal 開始,而後轉換爲rac_textSignal。同時,next事件經過一個filter,最終到達訂閱者的block。你還能看到第一步發送的error事件也是由subscribeNext:error:block來處理的。

 

如今你已經有了一個發送搜索文本的signal了,是時候來搜索Twitter的內容了。你如今以爲還好嗎?我以爲應該還不錯哦~

 

搜索Twitter的內容

你可使用Social Framework來獲取Twitter搜索API,但的確如你所料,Social Framework不是響應式的。那麼下一步就是把所需的API調用封裝進signal中。你如今應該熟悉這個過程了。

 

RWSearchFormViewController.m中,添加下面的方法:

- (SLRequest *)requestforTwitterSearchWithText:(NSString *)text { NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"]; NSDictionary *params = @{@"q" : text}; SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter                                         requestMethod:SLRequestMethodGET                                                   URL:url                                            parameters:params]; return request; }

 

 方法建立了一個請求,請求經過v1.1 REST API來搜索Twitter。上面的代碼使用q這個搜索參數來搜索Twitter中包含有給定字符串的微博。你能夠在Twitter API 文檔中來閱讀更多關於搜索API和其餘傳入參數的信息。

 

 下一步是基於這個請求建立signal。在同一個文件中,添加下面的方法:

- (RACSignal *)signalForSearchWithText:(NSString *)text {     // 1 - define the errors     NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain                                                    code:RWTwitterInstantErrorNoTwitterAccounts                                                userInfo:nil];     NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain                                                         code:RWTwitterInstantErrorInvalidResponse                                                         userInfo:nil];                                                             // 2 - create the signal block     @weakify(self)     return [RACSignal createSignal:^RACDisposable *(id subscriber) {         @strongify(self);                 // 3 - create the request         SLRequest *request = [self requestforTwitterSearchWithText:text];                 // 4 - supply a twitter account         NSArray *twitterAccounts = [self.accountStore accountsWithAccountType:self.twitterAccountType];         if (twitterAccounts.count == 0) {             [subscriber sendError:noAccountsError];         } else {             [request setAccount:[twitterAccounts lastObject]];                     // 5 - perform the request         [request performRequestWithHandler: ^(NSData *responseData,                 NSHTTPURLResponse *urlResponse, NSError *error) {             if (urlResponse.statusCode == 200) {                             // 6 - on success, parse the response                 NSDictionary *timelineData = [NSJSONSerialization JSONObjectWithData:responseData                                                 options:NSJSONReadingAllowFragments                                                   error:nil];                 [subscriber sendNext:timelineData];                 [subscriber sendCompleted];             } else {                 // 7 - send an error on failure                 [subscriber sendError:invalidResponseError];             }         }];     }     return nil;     }];}


分別講一下每一個步驟:

  1. 首先須要定義2個不一樣的錯誤,一個表示用戶尚未添加任何Twitter帳號,另外一個表示在請求過程當中發生了錯誤。

  2. 和以前的同樣,建立一個signal。

  3. 用你以前寫的方法,給須要搜索的文本建立一個請求。

  4. 查詢account store來找到可用的Twitter帳號。若是沒有帳號的話,發送一個error事件。

  5. 執行請求。

  6. 在請求成功的事件裏(http響應碼200),發送一個next事件,返回解析好的JSON數據,而後再發送一個completed事件。

  7. 在請求失敗的事件裏,發送一個error事件。

 

如今來使用這個新的signal!

 

在本教程的第一部分,你學過了如何使用flattenMap來把每一個next事件映射到一個新的signal。如今又要用到了。在viewDidLoad的末尾更新你的管道,添加flattenMap這一步:

[[[[[self requestAccessToTwitterSignal]     then:^RACSignal *{         @strongify(self)         return self.searchText.rac_textSignal;     }]     filter:^BOOL(NSString *text) {         @strongify(self)         return [self isValidSearchText:text];     }]     flattenMap:^RACStream *(NSString *text) {         @strongify(self)         return [self signalForSearchWithText:text];     }]     subscribeNext:^(id x) {         NSLog(@"%@", x);     } error:^(NSError *error) {         NSLog(@"An error occurred: %@", error);     }];

 

 構建運行,在search text field中輸入一些文字。當文本長度超過3個字符時,你應該就能在控制檯看到搜索Twitter的結果了。

 

下面是一段你將會看到的數據:

2014-01-05 07:42:27.697 TwitterInstant[40308:5403] { "search_metadata" = {     "completed_in" = "0.019";     count = 15;     "max_id" = 419735546840117248;     "max_id_str" = 419735546840117248;     "next_results" = "?max_id=419734921599787007&q=asd&include_entities=1";     query = asd;     "refresh_url" = "?since_id=419735546840117248&q=asd&include_entities=1";     "since_id" = 0;     "since_id_str" = 0; }; statuses = (     {         contributors = "";         coordinates = "";         "created_at" = "Sun Jan 05 07:42:07 +0000 2014";         entities = {             hashtags = ...

 

signalForSearchText:方法還會發送error事件到subscribeNext:error: block裏。你最好本身嘗試一下。

 

在模擬中打開設置應用,選擇你的Twitter帳戶,而後按「Delete Account」刪除它。

 

再從新運行應用,如今仍是容許訪問用戶的Twitter帳號,可是沒有可用的帳號。signalForSearchText:會發送一個error,輸出以下:

2014-01-05 07:52:11.705 TwitterInstant[41374:1403] An error occurred: Error Domain=TwitterInstant Code=1 "The operation couldn’t be completed. (TwitterInstant error 1.)"

 

Code=1表示是RWTwitterInstantErrorNoTwitterAccounts錯誤。在實際的應用中,你可能須要判斷錯誤碼來作一些更有用的事情,而不僅是打印到控制檯。

 

這代表了error事件很重要的一點,當signal發送error後,會直接到達處理error的block。這是一個例外流程。

 

注意:當請求Twitter返回錯誤時也是一個例外流程,嘗試一下,比較簡單的方法就是把請求參數改爲無效的。

 

線程

我相信你已經想把搜索Twitter返回的JSON值和UI鏈接起來了,可是在這以前還有最後一個須要作的事情。如今須要稍微作一些探索,來看一下這究竟是什麼!

 

subscribeNext:error:中以下圖所示的地方加一個斷點:

 

從新運行應用。若是須要的話,再次輸入Twitter登陸信息。在search field中輸入一些內容。當在斷點中止時,你應該能看到和下圖相似的東西:

 

注意斷點停在的代碼並無在主線程,也就是截圖中的Thread 1中執行。請記住你只能在主線程中更新UI。所以你須要切換線程來在UI中展現微博的列表。

 

這展現了ReactiveCocoa框架很重要的一點。上面顯示的操做會在signal最開始發送事件的線程中執行。嘗試在管道的其餘步驟添加斷點,你可能會驚奇的發現它們也是在不一樣線程上執行的。

 

因此接下來你要怎麼更新UI呢?一般的作法是使用操做隊列(參見教程如何使用 NSOperations 和 NSOperationQueues)。可是ReactiveCocoa有更簡單的解決辦法。

 

像下面的代碼同樣,在flattenMap:以後添加一個deliverOn:操做:

[[[[[[self requestAccessToTwitterSignal]     then:^RACSignal *{         @strongify(self)         return self.searchText.rac_textSignal;     }]     filter:^BOOL(NSString *text) {         @strongify(self)         return [self isValidSearchText:text];     }]     flattenMap:^RACStream *(NSString *text) {         @strongify(self)         return [self signalForSearchWithText:text];     }]     deliverOn:[RACScheduler mainThreadScheduler]]     subscribeNext:^(id x) {         NSLog(@"%@", x);     } error:^(NSError *error) {         NSLog(@"An error occurred: %@", error);     }]; 

 

如今從新運行,輸入一些內容,停在斷點。你應該能看到subscribeNext:error:block中的代碼如今實在主線程執行了:

 

這是真的嗎?一個簡單的操做,就把事件流切換到不一樣的線程了?真的是太棒了!

 

如今你就能安全地更新UI啦!

 

注意:若是你看一下RACScheduler類,就能發現還有不少選項,好比不一樣的線程優先級,或者在管道中添加延遲。

如今要展現那些微博了。

 

更新UI

若是你打開RWSearchResultsViewController.h 就會發現已經有一個displayTweets:方法了,它會讓右邊的view controller根據提供的微博數組來展現內容。實現很是簡單,就是一個標準的UITableView數據源。displayTweets:方法須要的惟一一個參數就是包含RWTweet實例的數組。RWTweet模型已經包含在初始工程裏了。

 

subscibeNext:error:裏收到的數據目前是在signalForSearchWithText:裏由返回的JSON值轉換獲得的一個NSDictionary。因此你怎麼肯定字典裏的內容呢?

 

看一下Twitter的API文檔,那裏有返回值的樣例。NSDictionary和這個結構對應,因此你能找到一個叫「statuses」的鍵,它對應的值是一個包含微博的NSArray,每一個條文也是NSDictionary實例。

 

RWTweet已經有一個類方法tweetWithStatus:,方法從NSDictionary中取得須要的數據。因此你須要的作的就是寫一個for循環,遍歷數組,爲每條微博建立一個RWTweet實例。

 

但咱們此次不這麼作。還有更好的方法。

 

這篇文章是關於ReactiveCocoa和函數式編程。若是用函數式API來實現把數據從一個格式轉換爲另外一個會優雅不少。你將會用到LinqToObjectiveC來完成這個任務。

 

關閉TwitterInstant workspace,而後在文本編輯中打開以前建立的Podfile。加入新的依賴:

platform :ios, '7.0'  pod 'ReactiveCocoa', '2.1.8' pod 'LinqToObjectiveC', '2.0.0'

 

在這個文件中打開終端,輸入下面的命令:

pod update

 

能看到輸出和下面的相似:

Analyzing dependencies Downloading dependencies Installing LinqToObjectiveC (2.0.0) Using ReactiveCocoa (2.1.8) Generating Pods project Integrating client project 


再次打開workspace,檢查新的pod是否和下圖同樣顯示出來:

 

打開RWSearchFormViewController.m,添加下列引用:

#import "RWTweet.h" #import "NSArray+LinqExtensions.h"

 

NSArray+LinqExtensions.h頭文件是LinqToObjectiveC裏的,它爲NSArray添加了許多方法,能讓你用流式API來轉換、排序、分組和過濾其中的數據。如今就來用一下

 

viewDidLoad中的代碼更新成下面這樣的:

[[[[[[self requestAccessToTwitterSignal]     then:^RACSignal *{         @strongify(self)         return self.searchText.rac_textSignal;     }]     filter:^BOOL(NSString *text) {         @strongify(self)         return [self isValidSearchText:text];     }]     flattenMap:^RACStream *(NSString *text) {         @strongify(self)         return [self signalForSearchWithText:text];     }]     deliverOn:[RACScheduler mainThreadScheduler]]     subscribeNext:^(NSDictionary *jsonSearchResult) {         NSArray *statuses = jsonSearchResult[@"statuses"];         NSArray *tweets = [statuses linq_select:^id(id tweet) {             return [RWTweet tweetWithStatus:tweet];         }];         [self.resultsViewController displayTweets:tweets];     } error:^(NSError *error) {         NSLog(@"An error occurred: %@", error);     }];

 

在上面的代碼中,subscribeNext:block首先獲取包含微博的數組。而後linq_select方法對數組中的每一個元素執行提供的block,來把NSDictionary的數組轉換成RWTweet的數組。

 

轉換完成就把微博發送給result view controller。

 

構建運行,終於能看到微博展現在UI中了:

 

注意:ReactiveCocoa 和 LinqToObjectiveC 靈感的來源類似。 ReactiveCocoa以微軟的 Reactive Extensions 庫爲模型,而 LinqToObjectiveC 以 Language Integrated Query APIs或者說 LINQ爲模型,特別是 Linq to Objects.

 

異步加載圖片

你可能注意到了每條微博的左側有一段空隙,這是用來顯示Twitter用戶頭像的。

 

RWTweet類有一個屬性profileImageUrl來存放頭像的URL。爲了讓table view能流暢地滾動,你須要讓用URL獲取圖像的代碼不在主線程中執行。你可使用Grand Central Dispatch或者NSOperationQueue來實現。可是爲何不用ReactiveCocoa呢?

 

打開RWSearchResultsViewController.m,添加下面的方法:

-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl {     RACScheduler *scheduler = [RACScheduler         schedulerWithPriority:RACSchedulerPriorityBackground];             return [[RACSignal createSignal:^RACDisposable *(id subscriber) {         NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];         UIImage *image = [UIImage imageWithData:data];         [subscriber sendNext:image];         [subscriber sendCompleted];         return nil;     }] subscribeOn:scheduler]; }

 

你如今應該對這個模式已經很熟悉了。

 

上面的方法首先獲取一個後臺scheduler,來讓signal不在主線程執行。而後,建立一個signal來下載圖片數據,當有訂閱者時建立一個UIImage。最後是subscribeOn:來確保signal在指定的scheduler上執行。

 

太神奇了!

 

如今仍是在這個文件中,在tableView:cellForRowAtIndex:方法的return語句以前添加下面的代碼:

cell.twitterAvatarView.image = nil;  [[[self signalForLoadingImage:tweet.profileImageUrl]     deliverOn:[RACScheduler mainThreadScheduler]]     subscribeNext:^(UIImage *image) {         cell.twitterAvatarView.image = image;     }];

 

由於cell是重用的,可能有髒數據,因此上面的代碼首先重置圖片。而後建立signal來獲取圖片數據。你以前也遇到過deliverOn:這一步,它會把next事件發送到主線程,這樣subscribeNext:block就能安全執行了。

 

這麼簡單真是好。

 

構建運行,如今頭像就能正確地顯示出來了:

 

譯註:做者在原文評論中針對cell重用的問題更新了代碼:

[[[[self signalForLoadingImage:tweet.profileImageUrl]     takeUntil:cell.rac_prepareForReuseSignal]     deliverOn:[RACScheduler mainThreadScheduler]]     subscribeNext:^(UIImage *image) {         cell.twitterAvatarView.image = image;     }];

 

節流

你可能注意到了,每次輸入一個字,搜索Twitter都會立刻執行。若是你輸入很快(或者只是一直按着刪除鍵),這可能會形成應用在一秒內執行好幾回搜索。這很不理想,緣由以下:首先,屢次調用Twitter搜索API,但大部分返回結果都沒有用。其次,不停地更新界面會讓用戶分心。

 

更好的解決方法是,當搜索文本在短期內,好比說500毫秒,再也不變化時,再執行搜索。

 

你可能也猜到了,用ReactiveCocoa來處理這個問題很是簡單!

 

打開RWSearchFormViewController.m,在viewDidLoad中,在filter以後添加一個throttle步驟:

[[[[[[[self requestAccessToTwitterSignal]     then:^RACSignal *{         @strongify(self)         return self.searchText.rac_textSignal;     }]     filter:^BOOL(NSString *text) {         @strongify(self)         return [self isValidSearchText:text];     }]     throttle:0.5]     flattenMap:^RACStream *(NSString *text) {         @strongify(self)         return [self signalForSearchWithText:text];     }]     deliverOn:[RACScheduler mainThreadScheduler]]     subscribeNext:^(NSDictionary *jsonSearchResult) {         NSArray *statuses = jsonSearchResult[@"statuses"];         NSArray *tweets = [statuses linq_select:^id(id tweet) {             return [RWTweet tweetWithStatus:tweet];         }];         [self.resultsViewController displayTweets:tweets];     } error:^(NSError *error) {         NSLog(@"An error occurred: %@", error);     }];

 

只有當,前一個next事件在指定的時間段內沒有被接收到後,throttle操做纔會發送next事件。就是這麼簡單。

 

構建運行,確認一下當中止輸入超過500毫秒後,纔會開始搜索。感受比以前好一些吧?你的用戶也會這麼想的。

 

到如今你的Twitter Instant應用已經完成了。放鬆一下,旋轉,跳躍,閉上眼吧~

 

若是你卡在教程中的某個地方了,能夠下載最終的工程(再打開以前別忘記運行pod install)。或者在Github上獲取這份代碼,每一步的構建運行都有一個commit。

 

譯註:最終工程裏的代碼和文章中的有一些區別。主要是在requestAccessToTwitterSignal方法。

總結

在你準備喝杯咖啡放鬆一下以前,仍是有必要來總結一下應用最終的管道圖:

 

數據流仍是挺複雜的,如今這全都用響應式的管道清晰地表現了出來。若是不用響應式的話,你能想象到這個應用會變得多複雜嗎?數據流會變得多混亂嗎?聽起來就很麻煩,還好你不用這麼作了。

 

如今你應該知道ReactiveCocoa有多棒了吧!

 

最後一點,ReactiveCocoa讓使用Model View ViewModel,或者說MVVM設計模式成爲可能。MVVM能讓應用邏輯和視圖邏輯更好地分離。若是你想了解更多的話,就來看下一篇教程吧。

相關文章
相關標籤/搜索