Ev.
Ev.

Reputation: 7589

Wait for AFNetworking completion block before continuing (i.e. Synchronous)

I have a use case for AFNetworking to behave synchronously (details below). How can I achieve this?

Here is my code snippet, which I've simplified as much as possible.

I would like to return the success response, but I only ever get nil (because the function returns before the block is called).

- (id)sendForUrl:(NSURL *)url {
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    __block id response;
    [manager GET:url.absoluteString parameters:nil success: ^(AFHTTPRequestOperation *operation, id responseObject) {
        response = responseObject;
        NSLog(@"JSON: %@", responseObject);
    } failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Error: %@", error);
    }];

    return response;
}

Details So the reason I need this to behave synchronously is because I am building a pod that will bootstrap an application at start up. The boot strapping hits a services and saves a bunch of values locally. These values are then used for the current session and not changed. If the values change, the user will get a strange experience, so its important I avoid this.

If the service is down, that's okay. We'll use default values or looks for some saved values from a previous session, but whatever happens, we don't want the experience for the user to change in the session.

(This is an engine for A/B testing and experimentation - if that helps you 'get' the use case).

Upvotes: 1

Views: 4095

Answers (2)

Rob
Rob

Reputation: 438297

Ignoring the fact that we generally do not want to make synchronous network requests, the traditional solution is to use semaphores:

- (id)sendForUrl:(NSURL *)url {
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    __block id response;

    [manager GET:url.absoluteString parameters:nil success: ^(AFHTTPRequestOperation *operation, id responseObject) {
        response = responseObject;
        NSLog(@"JSON: %@", responseObject);
        dispatch_semaphore_signal(semaphore);
    } failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Error: %@", error);
        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return response;
}

There are two issues here:

  1. I know you said you didn't want "to see some complex dispatch style things", but with due deference to others, a semaphore is better than a while loop that is spinning, polling until some value changes.

  2. Note, assuming you're calling this method from the main thread, you must set the completionQueue to be some other, background queue. By default, the AFHTTPRequestOperationManager will use the main queue for those completion blocks. And if you block that thread waiting until the completion blocks to run on that same thread, you'll deadlock (i.e. your app will freeze).


For the sake of completeness, I'll point out that invariably, when someone asks "how to I make some asynchronous method behave synchronously", the correct answer is, "you don't". Instead, you generally adopt asynchronous patterns, for example changing the method to take a block parameter:

- (void)sendForUrl:(NSURL *)url completionHandler:(void (^)(id responseObject, NSError *error))completionHandler {
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    [manager GET:url.absoluteString parameters:nil success: ^(AFHTTPRequestOperation *operation, id responseObject) {
        if (completionHandler) {
            completionHandler(responseObject, nil);
        }
    } failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
        if (completionHandler) {
            completionHandler(nil, error);
        }
    }];
}

You'd then call it like so, providing a completion handler block:

[object sendForURL:url completionHandler:^(id responseObject, NSError *error) {
    // use responseObject and/or error here
}];

// don't use them here

Upvotes: 12

danh
danh

Reputation: 62686

It sounds like there's a part of your app that can't run until a starting request is done. But there's also a part that can run (like the part that starts the request). Give that part a UI that tells the user that we're busy getting ready. No blocking the UI.

But if you must, and if AFNetworking doesn't provide a blocking version of a request (kudos to them for that), then you could always block the old fashioned way...

- (void)pleaseDontUseThisIdea {

    __block BOOL thePopeIsCatholic = YES;
    [manager GET: ....^{
        // ...
        thePopeIsCatholic = NO;
    }];

    while (thePopeIsCatholic) {}
}

Upvotes: 2

Related Questions