Use a Baton to Scatter/Gather Asynchronous Work

AFNetworking and NSURLConnection make it really simple to perform background network activity. Sometimes, though, you need to tie together a set of scheduled activities into a logical unit and do something when they are all completed (successfully or not). Worse, you may have multiple sets of unrelated activites operating in tandem.

I had this problem recently with an app that downloads several sets of related data and waits until all are finished before processing each set. The app uses a subclass of AFHTTPClient to create an API client class. Each API call is executed in an operation queue and the API client calls back to a completion block upon success or failure.

The set of calls to the API sort of cascade, too. The first call tells you which resources you need to download. Each resource, in turn, may specify other resources you may or may not already have that also need to be downloaded. Those, too, may have dependencies on other resources, etc., etc. Each main branch of the tree really needs to succeed or fail as a set; a partial branch may not be useful.

What I needed was something like a simple to use mutex and signal. I ended up creating a class called a Baton that can be "passed" multiple times and signalled when the last handoff is handed back. Yeah, I know, "baton" isn't the greatest name, but that's the mental image I had; a relay race with teams of multiple, simultaneous runners. At each leg of the race, the baton is passed to all of the runners for the next leg who all must complete their leg before the next one can begin.

It actually worked out pretty well, so I threw the code up on GitHub, my meager contribution to the Objective-C Hack-a-thon.

Here's how you use it (the code has been sanitized to simplify it a little):

Baton *queryBaton = [Baton batonWithName:@"Query" completion:^(BOOL cancelled) {
    if (!cancelled) {
        // do something useful
    }
}];

[queryBaton pass];
[apiClient querySomething:query block:^(NSArray *results, NSError *error) {
    if (error) {
        NSLog("%@ Query Failed: %@", error);
        [queryBaton cancel];
        return;
    }

    for (NSNumber *idOfResource in results) {
        [queryBaton pass];
        [apiClient downloadResource:idOfResource completion:^(id resource, NSError *error) {
            if (resource) {
              [queryBaton finish];
            } else {
              NSLog(@"Oops: %@", error);
              [queryBaton cancel];
            }
        }];
    }

    [queryBaton finish];
}];

The semantics are similar to retain/release; the calls to pass and finish/cancel are matched. Each call to pass increments a pass count and each call to either finish or cancel decrements it. As soon as the count reaches zero the completion block is called.

It may not be the most elegant, but it might come in handy if you have the same problem to solve.

Home