Benedict's Soapbox

Handling Network Requests in Cocoa

Networking is a fundamental component of modern apps. Despite the ubiquity of the task I frequently seen network requests handled poorly. In this post I’ll describe the API design and app architecture I use for handling network request.

Networking and MVC

Before getting to the details of the method signature for a network request we need to discuss where in the apps architecture the network requests should be addressed. The architecture of a Cocoa app is based on the Model-View-Controller pattern. Unfortunately there are many different interpretations of MVC. To ensure we’re on the same page I’ll start by briefly describing my take on the MVC pattern:

Model: the model is the brains of the app. The model is full of moving parts. It is not a static data store that the controller can arbitrarily manipulate. The model is the boss. The model presents its self to the controller as a collection of semantically rich objects that provide the functionality for the app.

View: The view is the human facing component of the system. That generally means visual elements, for example output to the screen; inputs from touches; the pointer and the keyboard, but also other device sensors such as GPS and accelerometers.

Controller: the controller is the connection between the model and the view. The controller is not a connection between the model and all external data sources. The controller is not responsible for fetching, parsing or validating data, that’s the responsibility of the model. The controller simply takes the actions from the view and communicates them to the model and vice versa.

This approach has served me well. the strict delimitation between model and controller is powerful as it forces you to think carefully about what role an object performs. such steadfast conformance to MVC could be viewed as myopic. I, however, have found it to be very powerful constraint that has helped me create app architectures that are easy for new comes to pick up, semantically rich yet flexibly to modification. (I use a 2+1 style for classes prefix. The first 2 letters indicate the app and the 3 indicate where they sit in the MVC pattern. For example BEMPerson, BEVFancyButton, BECPersonViewController).

So which section of the app is responsible for creating network requests? The model. It’s the model that’s responsible for understanding the mundane details of the web service it interacts with. The model should encapsulate these details and provide a semantically rich interface that allows the controller to operate at the higher level of abstraction rather than the be concerned with the nitty-gritty details of networking.

An Example Method Signature

Here’s our headline act:

-(id)fetchPeopleMatchingQuery:(id)query completionHandler:(void^(id progress, BOOL didSucceed, NSArray *results, NSError *error))completionHandler;

The juicy part is completionHandler. Let’s look at each parameter in turn.

progress: This object is the key to keeping our sanity. The value of the progress progress is the same object that is return synchronously. This object performs 2 roles. Firstly it provides a means for the caller to interact with the request (cancel, monitor progress etc) while the request is running (i.e. before completionHandler is invoked). Secondly it makes it simpler for the caller to determine what to do when completionHandler is called. Here’s an example that illustrates both functions:

@implementation BECSearchViewController
[self.searchProgress cancel]; //1\. Cancel the old request
self.searchProgress = [self.addressBook fetchPeopleMatchingQuery:self.searchQuery completionHandler:^(id progress, BOOL didSucceed, NSArray *results, NSError *error){
    //2\. We're only interested in processing the current request
    BOOL isCurrentSearch = [self.searchToken isEqual:progress];
    if (!isCurrentSearch) return;  //The request is not current so is not of interest.
    //Store the results and the error to display in the view.
    self.searchResults = results;
    self.searchError = error;
    //Tidy up
    self.searchProgress = nil; //Clear the request so when we refresh the view we can provide feedback.
    [self refreshView];

But why use a protocol instead of an explicit object? An interface should only expose what is strictly necessary. If, for example, the return type had been an NSOperation subclass the caller would be able to interfere with its’ completionBlock and inadvertently cause havoc for the model. By only exposing what is strictly necessary (in this case cancel) we make such meddling impossible. A protocol also provides flexibility for the model to create disparate implementations for different network requests but maintain consistent interface between.

What should the protocol include? That ultimately depends on the semantics of the model, but as a role of thumb cancellation and progress monitoring are a good candidates. Of course an object that actually implements the protocol will be needed. If the protocol only includes cancel (which has been true for most cases I’ve used this pattern) then it’s simplest to use either an NSOperation subclass or NSProgress (which is a very useful but overlooked recent addition to Foundation). A category without an implementation can be used to state that these existing classes conforms to protocol. For example:

@protocol BECProgress 
//Instances of NSOperation can now be used as id.
//No @implemention is required because NSOperation already implements all the methods in BECProgress.
@interface NSOperation 

didSucceed: By explicitly stating whether the request succeed we avoid ambiguity. For example, is it consider a failure if result is nil or if error is non-nil? What if we were able to fetch some results from one data source but another failed thus meaning we would provide results and an error? By explicitly stating the success status we avoid all of these ambiguities. Explicitly stating success is also adhering to the DRY principle because each completion handler will need to perform this check anyway.

results: This is entirely dependant on the specifics of your model. It may be that you don’t have a result object to return and so can omit this parameter, or it may be that it makes sense to return multiple parameters.

error: It’s worth remember that NSError is more that just a jumped-up NSString. Of specific interest to this situation is NSUnderlyingErrorKey which is a standardised way by which a semantically richer description can be passed to the receiver without loosing the original error.

Two final notes:

  1. To avoid potential concurrency issues completionHandler should always be executed on the main thread. Your spidey senses should tingle whenever a controller method is called from anything other than the main thread.

  2. Even if the request can be fulfilled synchronously (e.g. from a local cache) then completionHandler should still be executed asynchronously. This is because implementations of completionHandler will have been created with the expectation that they will be executed asynchronously and synchronous execution would thus cause undesirable things to happen.

Model Instigated Requests

The approach described above is fine for user-instigated requests but falls short when we want the model to instigate the request without intervention by a controller. An example of this would be synching news items in an RSS reader app. In this example the model should know when it’s time to sync but we also want the to allow the user to manually trigger the synching. Here’s an example interface:

@interface BEMNewsItemStore : NSObject
@property(nonatomic, readonly) BOOL isSynchingNewsItems;
@property(nonatomic, readonly) NSArray *newsItems;
@property(nonatomic, readonly) NSDate *newsItemsLastSynched
@property(nonatomic, readonly) NSError *newsItemsSynchError;
-(void)updateNewItems;  //For manually synching

The properties are closely related to the parameters of completionHandler:

The properties can be observer via KVO (or using your KVO wrapper of choice). In practice only isSynchingNewsItems will likely be needed to be observed because the other properties will only ever change when synching occurs. Why KVO and not blocks, delegation or notifications?

It’s not just for network requests

I’ve described this pattern with reference to network requests but this approach is equally as useful for any asynchronous task.