ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2

Get to grips with ReactiveCocoa in this 2-part tutorial series. Put the paradigms to one-side, and understand the practical value with work-through examples By Colin Eberhardt.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

Chaining Signals

Once the user has (hopefully!) granted access to their Twitter accounts, the application needs to continuously monitor the changes to the search text field, in order to query twitter.

The application needs to wait for the signal that requests access to Twitter to emit its completed event, and then subscribe to the text field’s signal. The sequential chaining of different signals is a common problem, but one that ReactiveCocoa handles very gracefully.

Replace your current pipeline at the end of viewDidLoad with the following:

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

The then method waits until a completed event is emitted, then subscribes to the signal returned by its block parameter. This effectively passes control from one signal to the next.

Note: You’ve already weakified self for the pipeline that sits just above this one, so there is no need to precede this pipeline with a @weakify(self).

The then method passes error events through. Therefore the final subscribeNext:error: block still receives errors emitted by the initial access-requesting step.

When you build and run, then grant access, you should see the text you input into the search field logged in the console:

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!

Next, add a filter operation to the pipeline to remove any invalid search strings. In this instance, they are strings comprised of less than three characters:

[[[[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);
  }];

Build and run again to observe the filtering in action:

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!

Illustrating the current application pipeline graphically, it looks like this:

PipelineWithThen

The application pipeline starts with the requestAccessToTwitterSignal then switches to the rac_textSignal. Meanwhile, next events pass through a filter and finally onto the subscription block. You can also see any error events emitted by the first step are consumed by the same subscribeNext:error: block.

Now that you have a signal that emits the search text, it is time to use this to search Twitter! Are you having fun yet? You should be because now you’re really getting somewhere.

Searching Twitter

The Social Framework is an option to access the Twitter Search API. However, as you might expect, the Social Framework is not reactive! The next step is to wrap the required API method calls in a signal. You should be getting the hang of this process by now!

Within RWSearchFormViewController.m, add the following method:

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

This creates a request that searches Twitter via the v1.1 REST API. The above code uses the q search parameter to search for tweets that contain the given search string. You can read more about this search API, and other parameters that you can pass, in the Twitter API docs.

The next step is to create a signal based on this request. Within the same file, add the following method:

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

Taking each step in turn:

  1. Initially, you need to define a couple of different errors, one to indicate the user hasn’t added any Twitter accounts to their device, and the other to indicate an error when performing the query itself.
  2. As before, a signal is created.
  3. Create a request for the given search string using the method you added in the previous step.
  4. Query the account store to find the first available Twitter account. If no accounts are given, an error is emitted.
  5. The request executes.
  6. In the event of a successful response (HTTP response code 200), the returned JSON data is parsed and emitted along as a next event, followed by a completed event.
  7. In the event of an unsuccessful response, an error event is emitted.

Now to put this new signal to use!

In the first part of this tutorial you learnt how to use flattenMap to map each next event to a new signal that is then subscribed to. It’s time to put this to use once again. At the end of viewDidLoad update your application pipeline by adding a flattenMap step at the end:

[[[[[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);
  }];

Build and run, then type some text into the search text field. Once the text is at least three characters or more in length, you should see the results of the Twitter search in the console window.

The following shows just a snippet of the kind of data you’ll see:

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 = "<null>";
            coordinates = "<null>";
            "created_at" = "Sun Jan 05 07:42:07 +0000 2014";
            entities =             {
                hashtags = ...

The signalForSearchText: method also emits error events which the subscribeNext:error: block consumes. You could take my word for this, but you’d probably like to test it out!

Within the simulator open up the Settings app and select your Twitter account, then delete it by tapping the Delete Account button:

RemoveTwitterAccount

If you re-run the application, it is still granted access to the user’s Twitter accounts, but there are no accounts available. As a result the signalForSearchText method will emit an error, which will be logged:

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.)"

The Code=1 indicates this is the RWTwitterInstantErrorNoTwitterAccounts error. In a production application, you would want to switch on the error code and do something more meaningful than just log the result.

This illustrates an important point about error events; as soon as a signal emits an error, it falls straight-through to the error-handling block. It is an exceptional flow.

Note: Have a go at exercising the other exceptional flow when the Twitter request returns an error. Here’s a quick hint, try changing the request parameters to something invalid!

Colin Eberhardt

Contributors

Colin Eberhardt

Author

Over 300 content creators. Join our team.