bromanko
bromanko

Reputation: 948

Better asynchronous control flow with Objective-C blocks

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.

Upvotes: 37

Views: 10403

Answers (6)

无夜之星辰
无夜之星辰

Reputation: 6158

You can combine NSBlockOperation with semaphore to achieve it:

- (void)loadDataByOrderSuccess:(void (^)(void))success failure:(void (^)(void))failure {
    // first,load data1
    NSBlockOperation * operation1 = [NSBlockOperation blockOperationWithBlock:^{
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        [self loadData1Success:^{
            dispatch_semaphore_signal(sema);
        } failure:^{
            !failure ?: failure();
        }];
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    }];
    // then,load data2
    NSBlockOperation * operation2 = [NSBlockOperation blockOperationWithBlock:^{
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        [self loadData2Success:^{
            dispatch_semaphore_signal(sema);
        } failure:^{
            !failure ?: failure();
        }];
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    }];
    // finally,load data3
    NSBlockOperation * operation3 = [NSBlockOperation blockOperationWithBlock:^{
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        [self loadData3Success:^{
            dispatch_semaphore_signal(sema);
        } failure:^{
            !failure ?: failure();
        }];
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        !success ?: success();
    }];
    [operation2 addDependency:operation1];
    [operation3 addDependency:operation2];
    NSOperationQueue * queue = [[NSOperationQueue alloc] init];
    [queue addOperations:@[operation1, operation2, operation3] waitUntilFinished:NO];
}

Upvotes: 0

jeffmax
jeffmax

Reputation: 469

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.

Upvotes: 6

Ben Clayton
Ben Clayton

Reputation: 82219

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).

Upvotes: 4

Tal Bereznitskey
Tal Bereznitskey

Reputation: 2051

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];

Upvotes: 19

mattt
mattt

Reputation: 19544

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.

Upvotes: 10

Jon Reid
Jon Reid

Reputation: 20980

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

Upvotes: 10

Related Questions