Tutorial: Asynchronous HTTP Client Using NSOperationQueue

October 19th, 2012 Posted by: - posted under:Articles » Tutorials

Introduction

Recently here at ELC, we’ve been working on an app that requires a lot of server interaction, which has been a learning experience for managing threading, server load and connectivity. In order to keep the app performant and responsive while sending a large number of requests and aggregating a large set of data, our team had to intelligently manage and prioritize network interactions.

This is where NSOperationQueue helps out. This class relies heavily on Grand Central Dispatch (GCD) to execute NSOperations (in this case, HTTP requests and JSON serializations). These operations can be executed in various configurations, including concurrently and asynchronously.

In this tutorial, I’ll show you how to set up a block-based operation queue that takes advantage of NSBlockOperation, a concrete subclass of NSOperation. You can use this server client class to manage all interactions with your external server. To demonstrate this, our sample code will query Twitter for a set of search tags and return the results asynchronously. The sample code is available on GitHub. Let’s get started.

Server Client Setup

First, create a MediaServer class with an operation queue property. This server class is a singleton because we want to route all network requests through a single operation queue.

@interface MediaServer : NSObject
 
@property (strong) NSOperationQueue *operationQueue;
 
+ (id)sharedMediaServer;
 
@end

Our server singleton is instantiated as follows. Note that using dispatch_once takes advantage of GCD and is recommended by Apple for thread safety.

+ (id)sharedMediaServer;
{
    static dispatch_once_t onceToken;
    static id sharedMediaServer = nil;
 
    dispatch_once( &onceToken, ^{
        sharedMediaServer = [[[self class] alloc] init];
    });
 
    return sharedMediaServer;
}

Next, in MediaServer’s init method, initialize the operation queue and set the concurrent operation count. The maxConcurrentOperationCount property can be changed later, too.

- (id)init;
{
    if ( ( self = [super init] ) )
    {        
        _operationQueue = [[NSOperationQueue alloc] init];
        _operationQueue.maxConcurrentOperationCount = 2;
    }
 
    return self;
}

Search Tags Management

In the project files, you’ll notice SearchTagsViewController. I’ve set this up to handle adding, removing and editing Twitter search tags. You’ll find a pretty straightforward implementation using NSUserDefaults to persist your search tags. The main purpose of this view controller is to prepare a series of server requests.

Server Calls Using NSBlockOperation

Now, we’re ready to start using our operation queue. For our example, we’ll be searching for tweets containing various keywords, so we only need one fetch method in our server class.

Note: because blocks are a bit syntactically difficult to read, it can be convenient to typedef assign a block’s input and return parameters. This also makes methods much more readable when passing in blocks. For our example, we are expecting an array of tweet objects, and we’ll check for errors in the HTTP request and JSON serialization. In MediaServer.h, add:

typedef void (^FetchBlock)(NSArray *items, NSError *error);

Now, we’re ready to add our fetch tweets method. This method accepts a search string and a return block (FetchBlock). This method will create an NSBlockOperation instance using blockOperationWithBlock: and dispatch it to the Media Server’s dispatch queue. Within that block, we’ll asynchronously send an NSURLRequest, serialize the response and synchronously return the tweets using our FetchBlock. Let’s take a look at the code.

- (void)fetchTweetsForSearch:(NSString *)searchString block:(FetchBlock)block;
{
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
 
        NSMutableArray *tweetObjects = [[NSMutableArray alloc] init];
        NSError *error = nil;
        NSHTTPURLResponse *response = nil;
 
        NSString *encodedSearchString = [searchString stringWithURLEncoding];
        NSString *URLString = [NSString stringWithFormat:@"http://search.twitter.com/search.json?q=%@&rpp=%i&include_entities=true&result_type=mixed", encodedSearchString, SEARCH_RESULTS_PER_TAG];
        NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:DEFAULT_TIMEOUT];
        NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
 
        NSDictionary *JSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
        NSArray *tweets = [JSON objectForKey:@"results"];
 
        // Serialize JSON response into lightweight Tweet objects for convenience.
 
        for ( NSDictionary *tweetDictionary in tweets )
        {
            Tweet *tweet = [[Tweet alloc] initWithJSON:tweetDictionary];
            [tweetObjects addObject:tweet];
        }        
 
        NSLog(@"Search for '%@' returned %i results.", searchString, [tweetObjects count]);
 
        // Return to the main queue once the request has been processed.
 
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
 
            if ( error )
                block( nil, error );
            else
                block( tweetObjects, nil );
        }];
 
    }];
 
    // Optionally, set the operation priority. This is useful when flooding
    // the operation queue with different requests.
 
    [operation setQueuePriority:NSOperationQueuePriorityVeryHigh];
    [self.operationQueue addOperation:operation];
}

Dispatching Tweet Searches

Let’s look at how we’ll use this server method. In TweetsViewController’s viewDidLoad: method, we’ll want to loop through our search tags and fetch each set of tweets. Because each operation is dispatched to our operation queue, we don’t have to worry about swamping the server or causing timeouts due to limited network bandwidth. To do so, in viewDidLoad:, add:

MediaServer *server = [MediaServer sharedMediaServer];
 
    for (NSString *tag in self.tags) 
    {
        [server fetchTweetsForSearch:tag block:^(NSArray *items, NSError *error) {
 
            if ( items && error == nil )
            {
                [self.tweets addObjectsFromArray:items];
 
                NSArray *sortDescriptorsArray = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"createdAtDate" ascending:NO]];
                [self.tweets sortUsingDescriptors:sortDescriptorsArray];
 
                [self.tableView reloadData];
                [self.activity stopAnimating];
            }
        }];
    }

Canceling Tweet Searches

In certain cases, you might want to cancel operations in your queue, for example, when the user navigates away from a view displaying content from several server requests. In this example, when the user taps the back ‘Tags’ button, we want to prevent the remaining search requests from going through. This is as easy as:

- (void)viewDidDisappear:(BOOL)animated;
{
    MediaServer *server = [MediaServer sharedMediaServer];
    [[server operationQueue] cancelAllOperations];
}

Note that this doesn’t immediately remove operations from the queue, instead it notifies the queue to abort the operation as soon as possible. If you need more specific control of which operations to keep running, NSOperationQueue also exposes an operations property to manually cancel specific operations.

Concurrency

The only thing fetchTweetsForSearch:block: does is create an operation and submit it to the queue, so it returns almost immediately. The main benefit of this approach is that all of the work within the block operation occurs on a background queue, leaving the main thread free and ensuring that the UI remains responsive. To confirm this is working properly, you can open up the Time Profiler in Instruments (an extremely useful tool for improving UX) and check which queue the block is executed on.

Profiled NSOperation

In the profiler, you’ll see that initWithJSON:, JSONObjectWithData:options:error: and sendSynchronousRequest:returningResponse:error: are all executed on a dispatched worker thread, not the main thread. That’s exactly what we want.

Conclusion

There you have it. As a developer, you’ll glean the most benefit from this server paradigm when sending out a large number of URL requests or when your user is on a slow network. If you do encounter situations where your queue is filling up with requests, remember that you can prioritize your operations, e.g.,

[operation setQueuePriority:NSOperationQueuePriorityVeryHigh];

Another benefit of this approach is the ability to return cached data using our FetchBlock, while updating from the server in the background. Look for more on that in a later blog post.

Happy iCoding!

Source Code – GitHub