Feb 4 2014

Data-Driven iOS Development with ReactiveCocoa

Editor’s note: This post was co-authored by Howard Vining and Matt Mathias.


ReactiveCocoa (RAC) is an Objective-C framework for Functional Reactive Programming that seeks to provide more concise, flow-based code. For some useful introductions to the framework, you might watch our Tech Talk; you could otherwise check out some introductory articles here, here, and here. Throughout this post, we’ll be assuming that you’re familiar with typical ReactiveCocoa nomenclature like streams, signals, subscribers, etc. These resources are great to get an initial flavor for RAC, but if you’re hungry for more like we were, then you’re likely to have difficulty finding a post that details a more fully-featured application with RAC. To that end, we created an application that makes heavy use of RAC. Our application demonstrates how to use RAC in your application’s network and user interface layers. Along the way, we’ll emphasize what we believe RAC does well, and will also highlight its limitations. We think one of RAC’s primary benefits is its ability to seamlessly bind your application’s models and UI to the flow of data that you wish to represent.

Getting Started with ReactiveCocoa

The first place to start is to bring RAC into your project. You have two options.

  1. You can incorporate RAC as a static library into your project using these directions.
  2. CocoaPods hosts some podspecs that have been supplied by some kind third parties.

For convenience, we used CocoaPods. We’d be remiss if we didn’t mention that CocoaDocs hosts a wonderful set of documentation for RAC. Be sure to check it out.

Reactive Stack Overflow

Our app utilizes Stack Overflow’s public API to list the website’s top questions for the following platforms: Android, iOS, Ruby and Windows Phone. We use a UITabBarController for view controller containment, and give it five tabs: one for each platform above, and a final tab for overall Top/Hot Questions. Each tab in the UITabBarController has a UINavigationController that will allow users to “drill down” into each question to expose its corresponding answers. Thus, while relatively simple, our app shows how RAC may be implemented to address several familiar programming challenges.

Using RACSignal

RSOTopQuestionsTableViewController is the “main” UITableViewController of our application; it serves as the primary screen for the user and displays the current top questions on Stack Overflow. The other domain-specific tabs work in much the same manner. Accordingly, this tableview controller an excellent starting point for showing what RAC offers in our project. Let’s begin with - (void)viewDidLoad and interrogate the following code:

@weakify(self);
RACSignal *topQuestionsSignal = [[sharedStore topQuestions] deliverOn:[RACScheduler mainThreadScheduler]];

We begin with the weakify macro to avoid creating strong reference cycles with self inside later blocks. Inside relevant blocks, we follow-up each weakify with strongify. Next, we create an instance of RACSignal to serve as our topQuestionsSignal. Recall that we use RACSignal to capture and deliver present and future values in the push-driven data stream of our concern. To create our signal, we send the message topQuestions to our sharedStore object, which is the single instance of our RSOStore class that is tasked with storing our top questions. Before we continue, let’s take a trip over to RSOStore.m to examine our topQuestions method.

- (RACSignal *)topQuestions
{
    RACSignal *signal = [[RSOWebServices sharedServices] fetchQuestionsWithTag:nil];
    return [self questionsForSignal:signal];
}

As we can see, it sends a message to our web services singleton and returns a signal. Let’s go visit RSOWebServices to see what fetchQuestionsWithTag: does.

- (RACSignal *)fetchQuestionsWithTag:(NSString *)tag
{
    NSURL *fetchQuestionURL = [self createRelativeURLWithTag:tag];
    @weakify(self);
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
...

This method also returns a RACSignal. We create the signal with the createSignal: class method on RACSignal. createSignal: takes a block with a parameter—in this case, the parameter is an id that conforms to the RACSubscriber protocol—and returns a RACDisposable object that encapsulates the work related to the teardown and clean up of subscriptions. It’s important to understand that the RACSubscriber protocol involves the tasks related to sending subscriber objects the data that result from the signal to which they subscribe.


@strongify(self);
NSURLSessionDataTask *task = [self.client dataTaskWithURL:fetchQuestionURL 
                                        completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

            if(error)
            {
                [subscriber sendError:error];
            }
            else if(!data)
            {
                NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"No data was received from the server."};
                NSError *dataError = [NSError errorWithDomain:RSOErrorDomain code:RSOErrorCode userInfo:userInfo];
                [subscriber sendError:dataError];
            }
            else
            {
                NSError *jsonError;
                NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data 
                                                                     options:NSJSONReadingMutableContainers 
                                                                       error:&jsonError];

                if(jsonError)
                {
                    [subscriber sendError:jsonError];
                }
                else
                {
                    [subscriber sendNext:dict[@"items"]];
                    [subscriber sendCompleted];
                }
            }
        }];

        [task resume];

        return [RACDisposable disposableWithBlock:^{
        [task cancel];
        }];
    }];
    return signal;
}

Inside createSignal:’s block, we create our URL and kick off an instance of NSURLSessionDataTask to get our top questions. Inside the completionHandler block for NSURLSessionDataTask, we sendError: to the subscriber if there is an error downloading the data. Otherwise, if there is data, we send sendNext:—with our data—and sendCompleted to our subscriber when we have finished downloading the questions. sendError:, sendNext:, and sendCompleted: are all required methods on the RACSubscriber protocol. Note that at the end of the createSignal: block, we make sure to return our RACDisposable to clean up after ourselves. In our case, we are ending the data task with [task cancel]. Finally, we return the signal resulting from our fetchQuestionsWithTag: method. With this signal in hand, we return to our topQuestions method in RSOStore.m. We call [self questionsForSignal:signal], careful to pass in our new signal, to fill out the data model for our top questions tableview. questionsForSignal: has the following implementation:

- (RACSignal *)questionsForSignal:(RACSignal *)signal
{
    return  [signal map:^(NSArray *questionDicts) {

        NSMutableArray *questions = [[NSMutableArray alloc]init];

        for(NSDictionary *questionDictionaryItem in questionsDicts)
        {
            RSOQuestion *question = [RSOQuestion questionForDictionary:questionDictionaryItem];
            [questions addObject:question];
        }

        return [questions copy];

    }];
}

Just like the other methods we’ve discussed above, we are returning a RACSignal. Yet, we have a new concept introduced in this method’s implementation: map:. map: takes a block that is called on each unit of data in the signal. It returns a new RACSignal carrying the values returned by the block. In our case, our web service call is going to send down an array of JSON objects representing questions. Each item in the array is represented as a dictionary. We loop through these dictionaries, pull out question data from the dictionary item and place it in a mutable array. Take a look at the implementation of RSOQuestion if you’re interested in further details. At the end of this block, we return a copy of the array holding the questions for our table. At this point, we’re back in -viewDidLoad in RSOTopQuestionsTableViewController.m. Notice that we’re ensuring that the topQuestionsSignal is delivered on the main thread by passing in [RACScheduler mainThreadScheduler] to the argument for the method deliverOn:. We do so to avoid reloading our tableview from a background thread once we receive questions from our signal. Now that we have our signal, we have to use it.

[topQuestionsSignal subscribeNext:^(NSArray *questions) {
   @strongify(self);
   [self loadQuestions:questions];
   } error:^(NSError *error) {
         [self displayError:[error localizedDescription] title:@"An error occurred"];
         [progressOverlay hide:YES];
     } completed:^{
         [progressOverlay hide:YES afterDelay:1];
     }];

topQuestionsSignal is sent - subscribeNext:error:completed:. This method takes three block arguments that are each executed in specific scenarios, and returns a RACDisposable object to aid in the clean up of the signal. subscribeNext: executes its block each time the signal sends a new value down the pipeline. In this case, the value sent down the signal is the array of questions for the tableview. As such, we execute our method to reload the tableview. error: will execute its block in the scenario that our signal returns an error; for example, loss of internet connection. The completed: block gives us an opportunity to perform some work that will be executed upon completion of our signal. We take this opportunity to hide our progress HUD. Note that a subscription to a signal is removed for a subscriber when the subscriber receives an error: or completed: event.

Refreshing the Tableview

Next, we initialize a UIRefreshControl to handle downward swipes to refresh the table’s list of questions. Remember, our previous subscription on topQuestionsSignal concluded (either with new question data or with an error), but that doesn’t mean we can’t reuse it.

UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[[refreshControl rac_signalForControlEvents:UIControlEventValueChanged] subscribeNext:^(UIRefreshControl *refreshControl) {
        @strongify(self);
        [topQuestionsSignal subscribeNext:^(NSArray *questions){
            [self loadQuestions:questions];
        } error:^(NSError *error) {
            [self displayError:error.localizedDescription title:@"An error occurred"];
            [refreshControl endRefreshing];
        } completed:^{
            [refreshControl endRefreshing];
        }];
    }];
self.refreshControl = refreshControl;

RAC provides a number of useful categories that allow the developer to graft RAC functionality onto system components. In the code above, rac_signalForControlEvents: comes from the category RACSignalSupport.h on UIControl. It allows us to hook into changes to our UIRefreshControl, which will send the event UIControlEventValueChanged whenever it is activated. Thus, we pass this control event into the argument for rac_signalForControlEvents:. In order to start listening for new data, we need to send subscribeNext: to the signal resulting from rac_signalForControlEvents:. subscribeNext: takes a block as before, and we specify a UIRefreshControl as the block’s parameter. When the UIRefreshControl is activated, it is sent down on the signal to the subscriber. Here, we take this opportunity to renew our subscription to topQuestionsSignal, which makes this signal “hot” once again. Doing so kicks off another web service request to Stack Overflow to see if any new top questions are available. In this manner, we refresh the table by simply renewing a subscription to a pre-existing signal.

Filtering the Tableview

The last bit of functionality that we’d like to highlight is the search box at the top of our tableview. We use this search box to filter the table’s content with the query string entered by the user in this text field. The search box utilizes the category RACSignalSupport.h that RAC provides on UITextField. This category yields an instance method called - (RACSignal *)rac_textSignal that creates and returns a signal for the receiving UITextField. The following code characterizes our approach:

RACSignal *searchBoxSignal = [[[self.searchBox rac_textSignal] throttle:kSearchQueryThrottle] skip:1];

RAC(self,filteredTopQuestions) = 
[RACSignal combineLatest:@[searchBoxSignal, topQuestionsSignal]
                  reduce:^id(NSString *filterString, NSArray *questions) {
                          @strongify(self);
                          if ([filterString length] > 0)
                          {
                              NSPredicate *predicate = [NSPredicate predicateWithFormat:@"text contains %@", filterString]; 
                              self.filteredTopQuestions = [questions filteredArrayUsingPredicate:predicate]; 
                              return self.filteredTopQuestions; 
                          } 
                          else 
                          { 
                              return questions; 
                          } 
}];

The signal returned by rac_textSignal starts “hot” with the current text, and so we choose to skip: the first value. Accordingly, new values entered into the text field are sent down the signal only after the user begins adding text. Since we don’t want to filter the table too quickly, we elect to throttle: the signal by kSearchQueryThrottle (which is a constant we set equal to 0.6 seconds). The argument for throttle: takes an NSTimeInterval and only sends nexts to the subscriber after the given time interval. Ensuring to skip the on-subscription event and throttling future events allows us to reload our table appropriately. RAC() is a macro that will create a one-way binding between the signal produced by comebineLatest:reduce:and the property filteredTopQuestions on self. combineLatest:reduce: is a class method on RACSignal that takes an array of signals to combine as the argument for combineLatest. The reduce block, in our case, will return an NSArray corresponding to the array of filteredTopQuestions and takes two parameters that correspond to the returns types carried by the data stream represented by the signals in the combineLatest argument above. Inside the reduce block, we check to see if the filterString’s length is greater that 0 to ensure that a filterString exists. If so, we filter our top questions array according to filterString. If not, we return the full top questions array generated by our previous subscription to our existing topQuestionsSignal. In sum, our use of combineLatest:reduce: has the effect of combining two signals into one with the purpose of filtering our top questions array based upon the specified criteria.

That’s a wrap

We’ve accomplished a lot: created some signals, passed them through methods and classes, done some work on them along the way, used the results to fill out our data model and transformed our UI. Overall, this tour of RSOTopQuestionsTableViewController.m demonstrates the general approach each view controller takes in representing its content. Go ahead and play with the app! The above demonstration highlights several very powerful features of RAC:

  1. combineLatest:reduce allows the developer to combine signals into one. Doing so means that specific properties, to take our example, can be set according to complex logic determined by a single scope.
  2. Additionally, combineLatest:reduce: allows the developer to combine signals with completely different logic. For example, you can throttle: one signal and skip: the first three values for another. This combination can make your code at once more concise and flexible.
  3. Furthermore, throttle: offers a succinct way of adding time delay to an action or set of actions. This is done without the conventional, manual management of timers and threads.
  4. Perhaps more importantly, new values in the data streams captured by signals will be sent down automatically as they appear. This feature is a consequence of the side-effects of the signals being triggered by their subscriptions.
  5. sendNext: adds more control, as it provides the flexibility to manually push data down signals. To that point, RACSubjectis a signal that allows for manual management and provides even more flexibility (and responsibility!). See here and here for some implementation details and other considerations.

Some Final Thoughts

One of RAC’s exciting prospects is to alleviate the burden of all of those laborious checks for application state. Mobile applications are especially data hungry, and RAC provides developers a set of tools that can make matching our models and UI to the data they will consume much more convenient. Its block-based structure is also a huge win. Such convenience comes with some cost and risk. First, the syntax of RAC is at least a small departure from the code you may be used to typing. Second, RAC requires some fluency in a variety of new concepts in order to understand how to use the framework to solve problems. Third, and perhaps most controversially, while RAC aims to introduce functional programming into Objective-C, it cannot be entirely successful. For example, RAC cannot bind a data collection to a UI element (e.g., a tableview). As such, you will still need to use the tableview’s datasource methods to populate the table with data. For better or worse, your application’s code will not be completely functional. Cocoa just isn’t built that way; however, it is worth mentioning that Objective-C’s block-based present and future are taking and will take us further along the functional path. It is up to the developer (and the team) to ensure that the project’s code is coherent, effective and efficient. All in all, we think that RAC provides a wonderful suite of tools that, when appropriately implemented, can make some elements of your code easier to read, more maintainable and even more pleasurable to write.

1 Comment

  1. aceontech.com

    Thank you for this post! We definitely need more introductory articles like this one to get people familiar with ReactiveCocoa.

Leave a Comment

Join the discussion. Do not worry, your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>