ReactiveCocoa入門教程:第二部分

jopen 8年前發布 | 14K 次閱讀 iOS開發 移動開發 ReactiveCocoa

翻譯自: http://www.raywenderlich.com/62796/reactivecocoa-tutorial-pt2

原文鏈接:

ReactiveCocoa 是一個框架,它允許你在你的iOS程序中使用函數響應式(FRP)技術。加上第一部分的講解,你將會學會如何使用信號量(對事件發出數據流)如何替代標準的動作和事件處理邏輯。你也會學到如何轉換、分離和組合這些信號量。

在這里,也就是第二部分里,你將會學到更多先進的ReactiveCocoa特性,包括:

1、另外兩個事件類型:error和completed

2、Throttling(節流)

3、Threading

4、Continuations

5、更多。。。

是時候開始了。

推ter Instant

這里我們要使用的貫穿整個教程的程序是叫做 推ter Instant 的程序,該程序可以在你輸入的時候實時更新搜索到的結果。

該應用包括一些基本的用戶交互界面和一些平凡的代碼,了解之后就可以開始了。在 第一部分 里面,你使用Cocoapods來把CocoaPods加載到你的工程里面,這里的工程里面就已經包含了Podfile文件,你只需要pod install一下即可。

然后重新打開工程即可。(這個時候打開推terInstant.xcworkspace):

1、推terInstant:這是你的程序邏輯

2、Pods:里面是包括的三方類庫

運行一下程序,你會看到如下的頁面:

花費一會時間讓你自己熟悉一下整個工程。它就是一個簡單的split viewController app.左邊的是RWSearchFormViewController,右邊的是:RWSearchResultsViewController。

自己說:原文簡單介紹了一下該工程,就不在介紹看一下就可以了。

驗證搜索文本

你第一件要做的事情就是去驗證一下搜索文本,讓它確保大于兩個字符串。如果你看了第一篇文章,這個將會很簡單。

在RWSearchFormViewController.m中添加方法如下:

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

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

當前的邏輯很簡單,但是如果后面這個會更復雜呢?在上面的例子中,你只需要修改一個地方。此外,上面的寫法讓你的代碼更有表現力,它告訴你為什么要檢查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;
  }];

想想這是做什么呢?上面的代碼:

1、取走搜索文本框的信號量

2、把它轉換一下:用背景色來預示內容是否可用。

3、然后設置backgroundColor屬性在subscribeNext:的block里面。

Build然后運行我們就會發現當搜索有效的時候就會是白色,搜索字符串無效的時候就是黃色。

下面是圖解,這個簡單的反應傳輸看起來如下:

ran_textSignal發出包含當前文本框每次改變內容的next事件。map那個步驟轉換text value,將其轉換成了color,subscribeNext那一步將這個value提供給了textField的background。

當然了,你從第一個教程一定記得這些,對吧?如果你不記得的話,你也許想在這里停止閱讀,至少讀了整個測試工程。

在添加推ter 搜索邏輯之前 ,這里有一些更有趣的話題。

Formatting of Pipelines

當你正在鉆研格式化的ReactiveCocoa代碼的時候,普遍接受的慣例就是:每一個操作在一個新行,和所有步驟垂直對齊的。

在下面的圖片,你會看到更復雜的對齊方式,從上一個教程拿來的圖片:

這樣你會更容易看到該組成管道的操作。另外,在每個block中用最少的代碼任何超過幾行的都應該拆分出一個私有的方法。

不幸的是,Xcode真的不喜歡這種格式類型的代碼,因此你可能需要找到自己調整。

Memory Management

思考一下你剛才加入到推terInstant的代碼。你是否想過你剛才創建的管道式如何保留的呢?無疑地,是否是它沒有賦值為一個變量或者屬性他就不會有自己的引用計數,注定會消亡呢?

其中一個設計目標就是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];

你不會經常做這些,但是你必須知道可能性的存在。

Note:作為這些的一個推論,如果你創建了一個管道,但是你不給他訂閱,這個管道將不會執行,這些包括任何側面的影響,例如doNext:blocks。

Avoiding Retain Cycles

當ReactiveCocoa在場景背后做了好多聰明的事情—這就意味著你不必要擔心太多關于信號量的內存管理——這里有一個很重要的內存喜愛那個管的問你你需要考慮。

如果你看到下面的響應式代碼你僅僅加入:

[[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傳遞的強引用變量。

Note:如果你對@weakify和@strongify感興趣,可以進入RACEXTSCope.h中查看其實現。

最后一個提醒,當在Blocks使用實例變量的時候要小心,這樣也會導致block捕獲一個self的強引用。你可以打開編譯警告去告訴你你的代碼有這個問題。

好了,你從理論中幸存出來了,恭喜。現在你變得更加明智,準備移步到有趣的環節:添加一些真實的函數到你的工程里面。

Requesting Access to 推ter

為了在推terInstant 應用中去搜索Tweets,你將會用到社交框架(Social Framework)。為了訪問推ter你需要使用Accounts Framework。

在你添加代碼之前,你需要到模擬器中輸入你的賬號:

設置好賬號之后,然后你只需要在RWSearchFormViewController.m中導入以下文件即可:

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

然后在引入的頭文件下面寫如下的代碼:

typedef NS_ENUM(NSInteger, RW推terInstantError) {
    RW推terInstantErrorAccessDenied,
    RW推terInstantErrorNo推terAccounts,
    RW推terInstantErrorInvalidResponse
};

static NSString * const RW推terInstantDomain = @"推terInstant";
你將會使用這些簡單地鑒定錯誤。然后在interface和end之間聲明兩個屬性:
@property (strong, nonatomic) ACAccountStore *accountStore;
@property (strong, nonatomic) ACAccountType *推terAccountType;

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

然后在viewDidLoad里面加入以下代碼:

self.accountStore = [[ACAccountStore alloc] init];
self.推terAccountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifier推ter];

這些代碼創建了賬戶存儲和推ter賬號標示。在.m中添加如下方法:

- (RACSignal *)requestAccessTo推terSignal {
  // 1 - define an error
  NSError *accessError = [NSError errorWithDomain:RW推terInstantDomain
                                             code:RW推terInstantErrorAccessDenied
                                         userInfo:nil];
  // 2 - create the signal
  @weakify(self)
  return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    // 3 - request access to 推ter
    @strongify(self)
    [self.accountStore
       requestAccessToAccountsWithType:self.推terAccountType
         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、定義了如果用戶拒絕訪問的錯誤

2、根據第一個入門教程,類方法createSignal返回了一個RACSignal的實例。

3、通過賬戶存儲請求訪問推ter。在這一點上,用戶將看到一個提示,要求他們給予這個程序訪問推ter賬戶的彈框。

4、當用戶同意或者拒絕訪問,信號事件就會觸發。如果用戶同意訪問,next事件將會緊隨而來,然后是completed發送,如果用戶拒絕訪問,error事件會觸發。

如果你回想其第一個入門教程,一個信號可以以三種不同的事件發出:

1、next

2、completed

3、error

超過了signal的生命周期,它將不會發出任何信號事件。

最后,為了充分利用信號,在viewDidLoad后面添加如下代碼;

[[self requestAccessTo推terSignal]
  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

一旦用戶允許訪問推ter賬戶,為了執行推ter,程序將會不斷監聽搜索內容textField的變化.

程序需要等待信號,它請求訪問推ter去發出completed事件,然后訂閱textField的信號。不同信號連續的鏈是一個共有的問題,但是ReactiveCocoa處理起來非常優雅。

用下面的代碼替換當前在viewDidLoad后面的管道:

[[[self requestAccessTo推terSignal]
  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 requestAccessTo推terSignal]
  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);
  }];

運行就可以在控制臺看到只有三個以上的才能輸出。

圖解一下上邊的管道:

程序管道從requestAccessTo推terSignal信號開始,然后轉換到tac_textSignal。同事next事件通過filter,最后到達訂閱block.你也可以看到任何通過第一步的error事件。

現在你有一個發出搜索text的信號,它可以用來搜索推ter了。很有趣吧。

Searching 推ter

Social Framework是一個訪問推ter 搜索API的選項。然而,它并無法響應搜索,下一步就是給信號包括API請求方法。在當前的控制器中,添加如下方法:

- (SLRequest *)requestfor推terSearchWithText:(NSString *)text {
  NSURL *url = [NSURL URLWithString:@"https://api.推ter.com/1.1/search/tweets.json"];
  NSDictionary *params = @{@"q" : text};

  SLRequest *request =  [SLRequest requestForServiceType:SLServiceType推ter
                                           requestMethod:SLRequestMethodGET
                                                     URL:url
                                              parameters:params];
  return request;
}

下一步就是創建一個基于request的信號量。添加如下方法: 這創建了一個請求:搜索推ter(V.1.1REST API)。這個是調用推ter的api。

- (RACSignal *)signalForSearchWithText:(NSString *)text {

  // 1 - define the errors
  NSError *noAccountsError = [NSError errorWithDomain:RW推terInstantDomain
                                                 code:RW推terInstantErrorNo推terAccounts
                                             userInfo:nil];

  NSError *invalidResponseError = [NSError errorWithDomain:RW推terInstantDomain
                                                      code:RW推terInstantErrorInvalidResponse
                                                  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 requestfor推terSearchWithText:text];

    // 4 - supply a 推ter account
    NSArray *推terAccounts = [self.accountStore
      accountsWithAccountType:self.推terAccountType];
    if (推terAccounts.count == 0) {
      [subscriber sendError:noAccountsError];
    } else {
      [request setAccount:[推terAccounts 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 requestAccessTo推terSignal]
  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 requestAccessTo推terSignal]
  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 requestAccessTo推terSignal]
  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.推terAvatarView.image = nil;

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

再次運行就可以出來效果了:

Throttling(限流)

你可能注意到這個問題:每次輸入一個字符串都會立即執行然后導致刷新太快 ,導致每秒會顯示幾次搜索結果。這不是理想的狀態。

一個好的解決方式就是如果搜索內容不變之后的時間間隔后在搜索比如500毫秒。

而ReactiveCocoa是這個工作變的如此簡單。

打開RWSearchFormViewController.m然后更新管道,調整如下:

[[[[[[[self requestAccessTo推terSignal]
  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是多么的優雅。

附: 最終代碼

來自: http://www.cnblogs.com/zhanggui/p/5138831.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!