47

I have a function using AFJSONRequestOperation, and I wish to return the result only after success. Could you point me in the right direction? I'm still a bit clueless with blocks and AFNetworking specifically.

-(id)someFunction{
    __block id data;

    AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
        success:^(NSURLRequest *request, NSHTTPURLResponse *response, id json){
            data = json;
            return data; // won't work
        }
        failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error){

        }];



    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation: operation];

    return data; // will return nil since the block doesn't "lock" the app.
}
Shai Mishali
  • 9,224
  • 4
  • 56
  • 83

6 Answers6

57

To block the execution of the main thread until the operation completes, you could do [operation waitUntilFinished] after it's added to the operation queue. In this case, you wouldn't need the return in the block; setting the __block variable would be enough.

That said, I'd strongly discourage forcing asynchronous operations to synchronous methods. It's tricky to get your head around sometimes, but if there's any way you could structure this to be asynchronous, that would almost certainly be the way to go.

mattt
  • 19,544
  • 7
  • 73
  • 84
  • Hey matt , thanks for the reply. Usually i do use my data asynchronously , but specifically for this i have to return some data from an API , so i don't really see another way, unless you can recommend some way of action? :) – Shai Mishali Nov 01 '11 at 17:09
  • You could always add a block parameter to the method, like `-someFunctionWithBlock:^(NSData *data) {...}`. – mattt Nov 01 '11 at 17:11
  • As i said i'm quite of a newbie with blocks, how will this help? (and more importantly , what does it mean to have a block parameter in the method?) – Shai Mishali Nov 01 '11 at 17:14
  • Makes a lot of sense ! thanks for the lesson, i truly appreciate it :) I'll try writing one method this way and if it works for my needs i will definitely make it a habit :P – Shai Mishali Nov 01 '11 at 17:21
  • Just wanted to say a big thank you, i just did what you said and i can't see how i managed without it! so simple and powerful!:) – Shai Mishali Nov 01 '11 at 18:09
  • I cannot figure out how to set a timeout. What if I do a POST request? Do I have to wait more then two minutes? This keeps me from switching to AFNetworking. For the timeout issue see also here: http://stackoverflow.com/a/8714479/601466 – borisdiakur Jan 06 '12 at 09:49
  • 7
    Unfortunately waitUntilFinished trick doesn't work for me. I've got a couple of business methods that are synchronous by nature. It's a shame that AFNetworking completely ignores such use case. – nixau Jan 30 '12 at 18:04
  • 9
    I suspect the `waitUntilFinished` trick isn't working for some because the success and failure blocks are (by default) executed using `dispatch_async` on the main queue after the operation is completed. If you aren't executing inside a runloop, e.g a unit test, then the program may exit early without giving GCD a chance to run the callbacks. – Tim Potter Dec 27 '12 at 07:03
  • 4
    I think ideally any networking SDK should allow the user to choose if they want asynchronous operations or not; it should not force a particular model, though it may suggest one. – Joseph Earl Nov 03 '14 at 14:30
12

I'm using semaphores to solve this issue. This code is implemented in my own class inherited from AFHTTPClient.

__block id result = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSURLRequest *req = [self requestWithMethod:@"GET"
                                       path:@"someURL"
                                 parameters:nil];
AFHTTPRequestOperation *reqOp = [self HTTPRequestOperationWithRequest:req
                                                              success:^(AFHTTPRequestOperation *operation, id responseObject) {
                                                                  result = responseObject;                                                                          
                                                                  dispatch_semaphore_signal(semaphore);
                                                              }
                                                              failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                                                                  dispatch_semaphore_signal(semaphore);
                                                              }];
reqOp.failureCallbackQueue = queue;
reqOp.successCallbackQueue = queue;
[self enqueueHTTPRequestOperation:reqOp];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
return result;
Kasik
  • 169
  • 1
  • 8
11

I would suggest that you don't make a synchronous method with AFNetworking (or blocks in general). A good approach is that you make another method and use the json data from the success block as an argument.

- (void)methodUsingJsonFromSuccessBlock:(id)json {
    // use the json
    NSLog(@"json from the block : %@", json); 
}

- (void)someFunction {
    AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
        success:^(NSURLRequest *request, NSHTTPURLResponse *response, id json){
            // use the json not as return data, but pass it along to another method as an argument
            [self methodUsingJsonFromSuccessBlock:json];
        }
        failure:nil];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation: operation];
}
Ikhsan Assaat
  • 900
  • 1
  • 9
  • 23
  • Does `json` need to be retained somewhere so the instance isn't deallocated? I'm assuming the AFNetworking code is autoreleasing it. – raidfive Jan 16 '12 at 23:19
  • Under ARC, while the block is executing, it would be retained by the block. – paulmelnikow Apr 04 '13 at 18:28
  • or a more modern way, use `NSNotification`. – Raptor Apr 25 '14 at 07:04
  • Useless! Do you know that if your app send requests in a concrete order then in most cases it doesn't mean that app will process the responses in the same order? The only ways I found out are synchronous requests and promisekit (and similar libraries – Gargo Mar 10 '16 at 06:02
6

It's worth noting that some features of AFNetworking's AFClient can still be used in a synchronous manner, meaning that you can still use niceties such as Authorisation headers and multipart uploads.

For example:

NSURLRequest *request = [self.client requestWithMethod: @"GET"
                                                  path: @"endpoint"
                                            parameters: @{}];
NSHTTPURLResponse *response = nil;
NSError *error = nil;

NSData *responseData = [NSURLConnection sendSynchronousRequest: request
                                             returningResponse: &response
                                                         error: &error];

Remember to check response.statusCode in this case, as this method doesn't consider HTTP failure codes as errors.

joerick
  • 16,078
  • 4
  • 53
  • 57
  • 1
    With self.client representing an instance of AFHTTPClient – Greg M. Krsak Sep 23 '13 at 22:14
  • This was perfect for my needs, thank you. I wanted a method I could call from the debugger running on our client that would provide "curl" like queries against our REST backend, without needing to reimplement the OAUTH yack shaving that the client already manages. It's probably also suitable for tests and other non interactive tasks. – Benjohn Mar 19 '15 at 16:33
4

Add this below the code you normally work with:

[operation start];
[operation waitUntilFinished];
// do what you want
// return what you want

Example:

+ (NSString*) runGetRequest:(NSString*)frontPath andMethod:(NSString*)method andKeys:(NSArray*)keys andValues:(NSArray*)values
{
    NSString * pathway = [frontPath stringByAppendingString:method];
    AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:pathway]];
    NSMutableDictionary * params = [[NSMutableDictionary alloc] initWithObjects:values forKeys:keys];
    NSMutableURLRequest *request = [httpClient requestWithMethod:@"GET"
                                                            path:pathway
                                                      parameters:params];
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
    [httpClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) 
{
            // Success happened here so do what ever you need in a async manner
} 
failure:^(AFHTTPRequestOperation *operation, NSError *error) 
{
            //error occurred here in a async manner
}];
        [operation start];
        [operation waitUntilFinished];

         // put synchronous code here

        return [operation responseString];
}
1

To expand/update @Kasik's answer. You can create a category on AFNetworking like so using semaphores:

@implementation AFHTTPSessionManager (AFNetworking)

- (id)sendSynchronousRequestWithBaseURLAsString:(NSString * _Nonnull)baseURL pathToData:(NSString * _Nonnull)path parameters:(NSDictionary * _Nullable)params {
    __block id result = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    AFHTTPSessionManager *session = [[AFHTTPSessionManager alloc]initWithBaseURL:[NSURL URLWithString:baseURL]];
    [session GET:path parameters:params progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
        result = responseObject;
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    return result;
 }

@end

If you are calling the sync block inside a completion block of another AFNetwork request, make sure you change the completionQueue property. If you don't change it, the synchronous block will call the main queue upon completion while already on the main queue and will crash your application.

+ (void)someRequest:(void (^)(id response))completion {
    AFHTTPSessionManager *session = [[AFHTTPSessionManager alloc]initWithBaseURL:[NSURL URLWithString:@""] sessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    dispatch_queue_t queue = dispatch_queue_create("name", 0);
    session.completionQueue = queue;
    [session GET:@"path/to/resource" parameters:nil progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
     NSDictionary *data = [session sendSynchronousRequestWithBaseURLAsString:@"" pathToData:@"" parameters:nil ];
      dispatch_async(dispatch_get_main_queue(), ^{
          completion (myDict);
      });
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        completion (error);
    });
}];
Mark Bourke
  • 9,806
  • 7
  • 26
  • 30