Better asynchronous control flow with Objective-C blocks

10,302

Solution 1

I haven't used it yet, but it sounds like Reactive Cocoa was designed to do just what you describe.

Solution 2

I created a light-weight solution for this. It's called Sequencer and it's up on github.

It makes chaining API calls (or any other async code) easy and straightforward.

Here's an example of using AFNetworking with it:

Sequencer *sequencer = [[Sequencer alloc] init];

[sequencer enqueueStep:^(id result, SequencerCompletion completion) {
    NSURL *url = [NSURL URLWithString:@"https://alpha-api.app.net/stream/0/posts/stream/global"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
        completion(JSON);
    } failure:nil];
    [operation start];
}];

[sequencer enqueueStep:^(NSDictionary *feed, SequencerCompletion completion) {
    NSArray *data = [feed objectForKey:@"data"];
    NSDictionary *lastFeedItem = [data lastObject];
    NSString *cononicalURL = [lastFeedItem objectForKey:@"canonical_url"];

    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:cononicalURL]];
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        completion(responseObject);
    } failure:nil];
    [operation start];
}];

[sequencer enqueueStep:^(NSData *htmlData, SequencerCompletion completion) {
    NSString *html = [[NSString alloc] initWithData:htmlData encoding:NSUTF8StringEncoding];
    NSLog(@"HTML Page: %@", html);
    completion(nil);
}];

[sequencer run];

Solution 3

It was not uncommon when using AFNetworking in Gowalla to have calls chained together in success blocks.

My advice would be to factor the network requests and serializations as best you can into class methods in your model. Then, for requests that need to make sub-requets, you can call those methods in the success block.

Also, in case you aren't using it already, AFHTTPClient greatly simplifies these kinds of complex network interactions.

Solution 4

PromiseKit could be useful. It seems to be one of the more popular promise implementations, and others have written categories to integrate it with libraries like AFNetworking, see PromiseKit-AFNetworking.

Solution 5

There is an Objective-C implementation of CommonJS-style promises here on Github:

https://github.com/mproberts/objc-promise

Example (taken from the Readme.md)

Deferred *russell = [Deferred deferred];
Promise *promise = [russell promise];

[promise then:^(NSString *hairType){
    NSLog(@"The present King of France is %@!", hairType);
}];

[russell resolve:@"bald"];

// The present King of France is bald!

I haven't yet tried out this library, but it looks 'promising' despite this slightly underwhelming example. (sorry, I couldn't resist).

Share:
10,302
bromanko
Author by

bromanko

Hi! I’m Brian Romanko, cofounder of BigBig Bomb. I’ve been building software for over 13 years at firms like frog and several fast-paced startups. I hold a BS in Information Technology from RIT and an MBA from the University of Texas. Throughout my career I've consulted for clients such as HP, Microsoft, Cisco and Dell. Check out our latest app, StackTrace, a reading/browsing experience for Stack Overflow

Updated on June 26, 2022

Comments

  • bromanko
    bromanko about 2 years

    I'm using AFNetworking for asynchronous calls to a web service. Some of these calls must be chained together, where the results of call A are used by call B which are used by call C, etc.

    AFNetworking handles results of async calls with success/failure blocks set at the time the operation is created:

    NSURL *url = [NSURL URLWithString:@"http://api.twitter.com/1/statuses/public_timeline.json"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
        NSLog(@"Public Timeline: %@", JSON);
    } failure:nil];
    [operation start];
    

    This results in nested async call blocks which quickly becomes unreadable. It's even more complicated when tasks are not dependent on one another and instead must execute in parallel and execution depends on the results of all operations.

    It seems that a better approach would be to leverage a promises framework to clean up the control flow.

    I've come across MAFuture but can't figure out how best to integrate it with AFNetworking. Since the async calls could have multiple results (success/failure) and don't have a return value it doesn't seem like an ideal fit.

    Any pointers or ideas would be appreciated.

  • Chris Devereux
    Chris Devereux about 12 years
    I have used it, and Jon is right. it's great for exactly this sort of thing.
  • bromanko
    bromanko about 12 years
    Thanks @mattt. That's basically what I'm doing now. The nested blocks just have a sense of code smell. It's the same smell I get with deeply nested conditional logic. Perhaps I'm longing for some of the cleanliness that node.js and other Javascript frameworks offer to make for more readable functional programming.
  • bromanko
    bromanko about 12 years
    Interesting. I'd come across Reactive Cocoa but didn't consider it for this scenario. Since the AF operations are all KVO compliant I could add handlers to either the operation queue or the individual operations. I'll mess with that.
  • mattt
    mattt about 12 years
    The deep nesting is not an inherent result of this approach--by effectively factoring callbacks into their own methods, it should look a lot more like chaining in a functional language. Having to go deeper than two nested calls is definitely a smell, though, and it probably means that you should consider creating a new API call to get you what you need all at once (if that's in your power at all)
  • Ben Clayton
    Ben Clayton over 11 years
    That's a nice neat, simple solution. Thanks for sharing.
  • mpemburn
    mpemburn over 11 years
    Looks like it could be very useful but it's not ARC compliant and I don't have the wherewithal to make it so {sigh}.
  • Richard H Fung
    Richard H Fung almost 11 years
    I like the ReactiveCocoa approach. My blog article explains how to use ReactiveCocoa for this purpose.
  • fabb
    fabb over 10 years
    Looks like in the case of an error in step 1 or 2, the rest of the steps won't be executed.
  • eremzeit
    eremzeit over 10 years
    This commit appears to have made it ARC compliant: github.com/mproberts/objc-promise/commit/…
  • Benjohn
    Benjohn over 9 years
    @fabb I believe that's the desired outcome here – it's certainly the effect I want to achieve.
  • Benjohn
    Benjohn over 9 years
    @fabb In the example code above, nil is passed in as the failure block, so errors are silently ignored (but error do cause the sequencer to halt and be disposed of, because nothing calls the next step). In my code, I have an error handler block available to the function that is running the Sequencer. I pass this error handler instead of the nil. If there is an error, I don't want the remaining steps to run. I want the error block to be called immediately and the sequencer will just get thrown away at the point it reached.
  • fabb
    fabb over 9 years
    Ok, so Sequencer is a "half" implementation of a Promise then?
  • Benjohn
    Benjohn over 9 years
    @fabb :-) I'm not sure what a Promise is, to be honest. The Sequencer here is really just (quite literally) a queue of tasks that you want to run. Its main conveniences are that it "thinks about" what the next task is for you (by popping it from the front of the queue), and provides a simple interface to pass a parameter forward to the next step.
  • Benjohn
    Benjohn over 9 years
    I like this solution a lot. In my code, as discussed with @fabb, I've got a single error handler block I can pass to the asynchronous calls where needed (or I can wrap it when the calls have a different error block interface). A "tip" for passing more data forward to future steps. Sometimes you have data you need to pass forward several steps. A clean and easy way to achieve this is to declare a __block variable before a step (at function scope), assign to it in a step, and use the variable in a future step.
  • fabb
    fabb over 9 years
    @Benjohn I highly recommend you learning about promises. There are 2 very good iOS libraries I know that also have excellent documentation and introduction to Promises: github.com/couchdeveloper/RXPromise and promisekit.org
  • Benjohn
    Benjohn over 9 years
  • Taku
    Taku about 8 years
    It seems like Sequencer only allows waterfall-like controls and not customizable for doing things in parallel.