ReactiveCocoa入門教程:第二部分

原文連接:
ReactiveCocoa 是一個框架,它容許你在你的iOS程序中使用函數響應式(FRP)技術。加上第一部分的講解,你將會學會如何使用信號量(對事件發出數據流)如何替代標準的動做和事件處理邏輯。你也會學到如何轉換、分離和組合這些信號量。
在這裏,也就是第二部分裏,你將會學到更多先進的ReactiveCocoa特性,包括:
一、另外兩個事件類型:error和completed
二、Throttling(節流)
三、Threading
四、Continuations
五、更多。。。
是時候開始了。
 
Twitter Instant
這裏咱們要使用的貫穿整個教程的程序是叫作 Twitter Instant的程序,該程序能夠在你輸入的時候實時更新搜索到的結果。
該應用包括一些基本的用戶交互界面和一些平凡的代碼,瞭解以後就能夠開始了。在 第一部分裏面,你使用Cocoapods來把CocoaPods加載到你的工程裏面,這裏的工程裏面就已經包含了Podfile文件,你只須要pod install一下便可。
而後從新打開工程便可。(這個時候打開TwitterInstant.xcworkspace):
一、TwitterInstant:這是你的程序邏輯
二、Pods:裏面是包括的三方類庫
運行一下程序,你會看到以下的頁面:
花費一會時間讓你本身熟悉一下整個工程。它就是一個簡單的split viewController app.左邊的是RWSearchFormViewController,右邊的是:RWSearchResultsViewController。
本身說:原文簡單介紹了一下該工程,就不在介紹看一下就能夠了。
驗證搜索文本
你第一件要作的事情就是去驗證一下搜索文本,讓它確保大於兩個字符串。若是你看了第一篇文章,這個將會很簡單。
在RWSearchFormViewController.m中添加方法以下:
- (BOOL)isValidSearchText:(NSString *)text {
  return text.length > 2;
}

這就簡單的保證了搜索的字符串大於兩個字符。寫這個很簡單的邏輯你可能會問:爲何要分開該方法到工程文件裏面呢?html

當前的邏輯很簡單,可是若是後面這個會更復雜呢?在上面的例子中,你只須要修改一個地方。此外,上面的寫法讓你的代碼更有表現力,它告訴你爲何要檢查string的長度。咱們應該遵照好的編碼習慣,不是麼?
而後,咱們導入頭文件:
#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;
  }];

想一想這是作什麼呢?上面的代碼:react

 一、取走搜索文本框的信號量
二、把它轉換一下:用背景色來預示內容是否可用。
三、而後設置backgroundColor屬性在subscribeNext:的block裏面。
Build而後運行咱們就會發現當搜索有效的時候就會是白色,搜索字符串無效的時候就是黃色。
下面是圖解,這個簡單的反應傳輸看起來以下:
 
ran_textSignal發出包含當前文本框每次改變內容的next事件。map那個步驟轉換text value,將其轉換成了color,subscribeNext那一步將這個value提供給了textField的background。
 
固然了,你從第一個教程必定記得這些,對吧?若是你不記得的話,你也許想在這裏中止閱讀,至少讀了整個測試工程。
在添加Twitter 搜索邏輯以前 ,這裏有一些更有趣的話題。
 
Formatting of Pipelines
當你正在鑽研格式化的ReactiveCocoa代碼的時候,廣泛接受的慣例就是:每個操做在一個新行,和全部步驟垂直對齊的。
在下面的圖片,你會看到更復雜的對齊方式,從上一個教程拿來的圖片:
這樣你會更容易看到該組成管道的操做。另外,在每一個block中用最少的代碼任何超過幾行的都應該拆分出一個私有的方法。
不幸的是,Xcode真的不喜歡這種格式類型的代碼,所以你可能須要找到本身調整。
 
Memory Management
思考一下你剛纔加入到TwitterInstant的代碼。你是否想過你剛纔建立的管道式如何保留的呢?無疑地,是不是它沒有賦值爲一個變量或者屬性他就不會有本身的引用計數,註定會消亡呢?
其中一個設計目標就是ReactiveCocoa容許這種類型的編程,這裏管道能夠匿名形式。全部你寫過的響應式代碼都應該看起來比較直觀。
爲了支持這種模型,ReactiveCocoa維持和保留本身全局的信號。若是它有一個或者多個subscribers(訂閱者),信號就會活躍。若是全部的訂閱者都移除掉了,信號就會被釋放。想了解更多關於ReactiveCocoa管理進程,能夠參看Memory Management 文檔。
這就剩下了最後的問題:你如何從一個信號取消訂閱?當一個completed或者error事件以後,訂閱會自動的移除(一會就會學到)。手工的移除將會經過RACDisposable.
全部RACSignal的訂閱方法都會返回一個RACDisposable實例,它容許你經過處置方法手動的移除訂閱。下面是一個使用當前管道的快速的例子。
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];

你不會常常作這些,可是你必須知道可能性的存在。ios

Note:做爲這些的一個推論,若是你建立了一個管道,可是你不給他訂閱,這個管道將不會執行,這些包括任何側面的影響,例如doNext:blocks。

 

Avoiding Retain Cyclesgit

當ReactiveCocoa在場景背後作了好多聰明的事情—這就意味着你沒必要要擔憂太多關於信號量的內存管理——這裏有一個很重要的內存喜好那個管的問你你須要考慮。github

 若是你看到下面的響應式代碼你僅僅加入:
[[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來得到一個textField的引用,Blocks在封閉返回內捕獲而且持有了值。所以在self和這個信號量之間形成了強引用,形成了循環引用。這取決於對象的生命週期,若是他的生命週期是應用程序的生命週期,那這樣是不要緊的,可是在更復雜的應用中就不行了。
爲了不這種潛在的循環引用,蘋果官方文檔: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;
  }];
在上面的代碼中,bself就是self標記爲__weak(使用它能夠make一個弱引用)的引用,如今能夠看到使用textField的時候使用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傳遞的強引用變量。json

Note:若是你對@weakify和@strongify感興趣,能夠進入RACEXTSCope.h中查看其實現。
最後一個提醒,當在Blocks使用實例變量的時候要當心,這樣也會致使block捕獲一個self的強引用。你能夠打開編譯警告去告訴你你的代碼有這個問題。
好了,你從理論中倖存出來了,恭喜。如今你變得更加明智,準備移步到有趣的環節:添加一些真實的函數到你的工程裏面。
 
 
Requesting Access to Twitter
 爲了在TwitterInstant 應用中去搜索Tweets,你將會用到社交框架(Social Framework)。爲了訪問Twitter你須要使用Accounts Framework。
在你添加代碼以前,你須要到模擬器中輸入你的帳號:
 
設置好帳號以後,而後你只須要在RWSearchFormViewController.m中導入如下文件便可:
#import <Accounts/Accounts.h>
#import <Social/Social.h>

而後在引入的頭文件下面寫以下的代碼:api

typedef NS_ENUM(NSInteger, RWTwitterInstantError) {
    RWTwitterInstantErrorAccessDenied,
    RWTwitterInstantErrorNoTwitterAccounts,
    RWTwitterInstantErrorInvalidResponse
};
 
static NSString * const RWTwitterInstantDomain = @"TwitterInstant";
你將會使用這些簡單地鑑定錯誤。而後在interface和end之間聲明兩個屬性: 
@property (strong, nonatomic) ACAccountStore *accountStore;
@property (strong, nonatomic) ACAccountType *twitterAccountType;

ACAccountsStore類提供訪問你當前設備有的social帳號,ACAccountType類表明指定類型的帳戶。app

而後在viewDidLoad裏面加入如下代碼:
self.accountStore = [[ACAccountStore alloc] init];
self.twitterAccountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

這些代碼建立了帳戶存儲和Twitter帳號標示。在.m中添加以下方法:框架

- (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<RACSubscriber> 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;
  }];
}

這個方法的做用是:

一、定義了若是用戶拒絕訪問的錯誤
二、根據第一個入門教程,類方法createSignal返回了一個RACSignal的實例。
三、經過帳戶存儲請求訪問Twitter。在這一點上,用戶將看到一個提示,要求他們給予這個程序訪問Twitter帳戶的彈框。
四、當用戶贊成或者拒絕訪問,信號事件就會觸發。若是用戶贊成訪問,next事件將會緊隨而來,而後是completed發送,若是用戶拒絕訪問,error事件會觸發。
若是你回想其第一個入門教程,一個信號能夠以三種不一樣的事件發出:
一、next
二、completed
三、error
超過了signal的生命週期,它將不會發出任何信號事件。
最後,爲了充分利用信號,在viewDidLoad後面添加以下代碼;
[[self requestAccessToTwitterSignal]
  subscribeNext:^(id x) {
    NSLog(@"Access granted");
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

 

 
若是你運行程序,將會看到一個彈出框:
提示是否容許訪問權限,若是ok,則打印出來Access granted ,不然將會走error。
Accounts Framework會記住你的決定,所以若是想再次測試,你須要針對模擬機進行:Reset Contents and Settings。
 
Chaining Signals
一旦用戶容許訪問Twitter帳戶,爲了執行twitter,程序將會不斷監聽搜索內容textField的變化.
程序須要等待信號,它請求訪問Twitter去發出completed事件,而後訂閱textField的信號。不一樣信號連續的鏈是一個共有的問題,可是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事件發出,而後訂閱者經過本身的block參數返回,這有效地將控制從一個信號傳遞給下一個。

Note:上面已經寫過了@weakly(self);因此這裏就不用再寫了。

then方法傳遞error事件。所以最後的subscribeNext:error: block還接收初始的訪問請求錯誤。

當你運行的時候,而後容許訪問,你應該能夠在控制檯看到打印出來的你輸入的東西。
 
而後,添加filter操做到管道去移除無效的搜索字符串。在這個實例中,他們是不到三個字符的string:
[[[[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);
  }];
運行就能夠在控制檯看到只有三個以上的才能輸出。 
 
圖解一下上邊的管道:
 
 
程序管道從requestAccessToTwitterSignal信號開始,而後轉換到tac_textSignal。同事next事件經過filter,最後到達訂閱block.你也能夠看到任何經過第一步的error事件。
如今你有一個發出搜索text的信號,它能夠用來搜索Twitter了。頗有趣吧。
 
Searching Twitter
 Social Framework是一個訪問Twitter 搜索API的選項。然而,它並沒有法響應搜索,下一步就是給信號包括API請求方法。在當前的控制器中,添加以下方法:
- (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;
}

下一步就是建立一個基於request的信號量。添加以下方法:這建立了一個請求:搜索Twitter(V.1.1REST API)。這個是調用Twitter的api。

- (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<RACSubscriber> 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;
  }];
}
而後在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];
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

運行:

便可在控制檯裏面打印出來篩選的數據。
 
Threading
 
我很確信你這會亟待把JSON數據放到UI裏面,可是在放到UI裏面以前你須要作最後一件事:找到他是什麼,你須要作一些探索!
 
添加一個端點到subscribeNext:error:那個步,而後咱們會看到Xcode左側的Thread,咱們發現若是想加載圖片的話必須在主線程裏面,可是他不在主線程中,因此咱們就能夠作以下操做:
[[[[[[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);
  }];

這樣就會在主線程中運行。也就是更新了管道:添加了deliverOn:操做。

而後再次運行咱們就會發現他是在主線程上執行了。這樣你就能夠更新UI了。
 
Updating the UI
這裏用到了另外一個庫: LinqToObjectiveC。安裝方式就不說了和ReactiveCocoa同樣
咱們在RWSearchFormViewController中導入:
#import "RWTweet.h"
#import "NSArray+LinqExtensions.h"
而後在輸出json數據的地方修改以下:
[[[[[[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);
  }];

運行:

就能夠看到右側的詳情頁面加載到數據了。剛引入的類庫其實就是將json數據轉換成了model.加載數據的效果以下:
 
 
Asynchronous Loading of Images
 
如今內容都加載出來了,就差圖片了。在RWSearchResultsViewController.m中添加以下方法:
-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl {
 
  RACScheduler *scheduler = [RACScheduler
                         schedulerWithPriority:RACSchedulerPriorityBackground];
 
  return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
    UIImage *image = [UIImage imageWithData:data];
    [subscriber sendNext:image];
    [subscriber sendCompleted];
    return nil;
  }] subscribeOn:scheduler];
 
}

這會你一ing該就會很熟悉這種模式了。而後在tableview:cellForRowAtIndex:方法裏面添加:

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

再次運行就能夠出來效果了:

 
Throttling(限流)
你可能注意到這個問題:每次輸入一個字符串都會當即執行而後致使刷新太快 ,致使每秒會顯示幾回搜索結果。這不是理想的狀態。
一個好的解決方式就是若是搜索內容不變以後的時間間隔後在搜索好比500毫秒。
而ReactiveCocoa是這個工做變的如此簡單。
打開RWSearchFormViewController.m而後更新管道,調整以下:
[[[[[[[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);
  }];
 

你會發現這樣就能夠了。throttle操做只是發送一個操做,這個操做在時間到以後繼續進行。

 
Wrap Up
如今咱們知道ReactiveCocoa是多麼的優雅。
 
附: 最終代碼 
相關文章
相關標籤/搜索