14

I'm stuck now some time and I need help. So in AFNetworking 2.0 we have AFHTTPRequestOperation so I could easily use NSOperationQueue and have some dependencies. So what we have now is only AFHTTPSessionManagerand NSURLSession that does not subclass NSOperation. I have class APIClient that subclasses AFHTTPSessionManager. I am using that class as singleton as sharedClient. I have overriden GET and POST so for example GET looks this:

- (NSURLSessionDataTask *)GET:(NSString *)URLString
               parameters:(NSDictionary *)parameters
                  success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                  failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure {
NSURLSessionDataTask *task = [super GET:URLString parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
    success(task, responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    failure(task, [Response createErrorWithAFNetworkingError:error]);
}];

return task;
}

Do you have any idea how to implement in that manner (if it's possible) to wrap that as NSOperation? So what I want to do - I want to be able to run in parallel two network calls, and after that have another method call that depends on second network call of first two calls. Do you have any idea what would be best approach?

Flipper
  • 1,107
  • 1
  • 11
  • 32

1 Answers1

36

I've written a quick little set of classes (https://github.com/robertmryan/AFHTTPSessionOperation/) that wrap AFHTTPSessionManager requests in asynchronous NSOperation subclass. You can then use that to enjoy maxConcurrentOperation constraints, or operation dependencies.

For example, here's an example where we issue two concurrent requests and have a completion operation dependent upon completion of both of those requests:

//  ViewController.m

#import "ViewController.h"
#import "AFNetworking.h"
#import "AFHTTPSessionOperation.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *urlString1 = @"...";
    NSString *urlString2 = @"...";

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.name = @"AFHTTPSessionManager queue";

    NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"All done");
    }];

    NSOperation *op1 = [AFHTTPSessionOperation operationWithManager:manager HTTPMethod:@"GET" URLString:urlString1 parameters:nil uploadProgress:nil downloadProgress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
        NSLog(@"finished 1");
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        NSLog(@"failed 1 - error = %@", error.localizedDescription);
    }];
    [completionOperation addDependency:op1];

    NSOperation *op2 = [AFHTTPSessionOperation operationWithManager:manager HTTPMethod:@"GET" URLString:urlString2 parameters:nil uploadProgress:nil downloadProgress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
        NSLog(@"finished 2");
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        NSLog(@"failed 2 - error = %@", error.localizedDescription);
    }];
    [completionOperation addDependency:op2];

    [queue addOperations:@[op1, op2] waitUntilFinished:false];
    [[NSOperationQueue mainQueue] addOperation:completionOperation];  // do this on whatever queue you want, but often you're updating UI or model objects, in which case you'd use the main queue
}

@end

It's worth noting that since you're only dealing with two requests, you could also use dispatch groups to accomplish the same thing:

//  ViewController.m

#import "ViewController.h"
#import "AFNetworking.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *urlString1 = @"...";
    NSString *urlString2 = @"...";

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

    dispatch_group_t group = dispatch_group_create();

    dispatch_group_enter(group);
    [manager GET:urlString1 parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"finished 1");
        dispatch_group_leave(group);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"failed 1 - error = %@", error.localizedDescription);
        dispatch_group_leave(group);
    }];

    dispatch_group_enter(group);
    [manager GET:urlString2 parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"finished 2");
        dispatch_group_leave(group);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"failed 2 - error = %@", error.localizedDescription);
        dispatch_group_leave(group);
    }];

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"All done");
    });
}

@end

With dispatch groups, you just need to be careful that every path within both the success and failure blocks call dispatch_group_leave.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • That is exactly what I was looking for, great :) – Flipper Jan 12 '16 at 23:09
  • @Flipper - BTW, I've updated answer with GCD alternative if you don't want to introduce those external classes. – Rob Jan 12 '16 at 23:13
  • @Rob - You state: "as you are only dealing with 2 requests....." at one point do you suggest that using dispatch groups would not be appropriate in this use case? Thx – grayson May 19 '16 at 19:33
  • @grayson - It's a matter of opinion, but I really prefer `NSOperation`-based solutions for networking, as it handles this dependency stuff really well, allows you to control the degree of concurrency without risking timeout issues, is really graceful in handling cancellations, etc. For simple situations, dispatch groups are fine, but I often lean towards `NSOperation` in most cases. – Rob May 19 '16 at 19:45
  • @Rob, thanks for the feedback. I apolgise for asking and this may require a full question in itself, but when you say it "is really graceful in handling cancellations" could you explain that. I am currently calling 7 endpoints (same api, just different data) using AFHTTPSessionManager (as a singleton), calling each subsequent one in the returning block because I don't want to call the next endpoint if the first one fails. Can I do this better with NSOperation (using AFNetworking 2.x)? – grayson May 19 '16 at 20:26
  • 1
    First, I would never go back to 2.x. I'd just wrap v3 tasks in `NSOperation` subclass if I needed operations. Second, if you're really going to run these sequentially (and suffer the performance penalty that entails), then neither operations nor dispatch groups are needed: Just initiate each upon the completion of the prior one. – Rob May 19 '16 at 20:52
  • @Rob : you have suggests in last comment `Just initiate each upon the completion of the prior one`, By that do you mean something like `recursion`? Like calling next request from completion block of previous ? – Ketan Parmar Jun 08 '16 at 11:38
  • 1
    @Lion Yeah, it's type of recursion (though because stuff is happening asynchronously, some of the uglier issues of cramming a lot of stuff onto a stack are not relevant here). But note, only do this type of sequential requests if you absolutely have to (i.e. the information necessary to build one request required information supplied by the prior request) because you pay huge penalty performance for sequential requests. It's much better to design solution that allows you to issue requests concurrently. – Rob Jun 08 '16 at 12:10
  • Rob, I'm getting this error. ---------------------------------------------------------Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AFHTTPSessionManager dataTaskWithHTTPMethod:URLString:parameters:uploadProgress:downloadProgress:success:failure:]: unrecognized selector sent to instance 0x7b497620' – JIANG Mar 06 '17 at 21:06
  • 1
    @Aviva - Are you using the latest version of AFNetworking or an older version? Earlier versions didn't have those progress parameters. If you're using the latest version of AFNetworking, it should work fine. I just tested the above code with the latest version of AFNetworking without incident. – Rob Mar 06 '17 at 22:10
  • Thank you Rob! I upgraded it to AFNetworking 3.1, now i'm getting a cancelled error. Please help! – JIANG Mar 13 '17 at 16:35
  • AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init]; NSOperation *operation = [AFHTTPSessionOperation operationWithManager:manager HTTPMethod:@"POST" URLString:urlString parameters:parameters uploadProgress:nil downloadProgress: nil success:^(NSURLSessionDataTask *task, id responseObject) { NSLog(@"Response --->>>: %@", responseObject ); } failure:^(NSURLSessionDataTask *task, NSError *error) { NSLog(@"Error --->>>: %@", error.localizedDescription); }]; – JIANG Mar 13 '17 at 16:37
  • 1
    Can you share the full text of the error? And don't just look at `error.localizedDescription`, but let's see the whole `NSError`. Let's make sure the error isn't something unrelated (like, perhaps trying to connect to http resource (versus https) without appropriate Info.plist settings to allow unsecured network connections). – Rob Mar 13 '17 at 16:39
  • Thank you Rob! I was able to track down to ATS related error. The error code is 999. My web server I was calling is TLS 1.2 only. – JIANG Mar 13 '17 at 17:57
  • @Rob, can you help me with this time out problem? I'm using your class and not sure what I did wrong. http://stackoverflow.com/questions/42821463/timed-out-while-using-afhttpsessionoperation – JIANG Mar 16 '17 at 13:48
  • @Rob, you fixed my last problem, however I run into something new, my session are not longer there. I'm a little lost and not sure how to do that. http://stackoverflow.com/questions/43149842/how-to-set-session-with-afhttpsessionoperation – JIANG Mar 31 '17 at 21:33
  • @Rob: How can I implement the same scenario with SOAP request. – iPhone Oct 03 '19 at 10:31
  • @iPhone - It seems like it would be basically the same sort of approach except you’ve got all the overhead of composing well-formed SOAP requests, parsing the XML responses, etc., just adding another layer on top of this. If I were starting a project nowadays, though, I’m not sure I’d pick AFNetworking/Objective-C; I would lean towards Alamofire/Swift. – Rob Oct 03 '19 at 17:13