1

I have a UITableViewController, call it TableViewControllerA, that's the delegate of another object, APICallerB, that I've created to communicate with an API. Through an NSURLSessionDataTask, APICallerB is setting one of its properties, which will then be set equal to one of TableViewControllerA's properties.

Here's tableViewControllerA's viewDidLoad method:

- (void)viewDidLoad {

    [super viewDidLoad];

    // init instance of APICallerB
    APICallerB *acb = [[APICallerB alloc] init];

    // Set TableViewControllerA as delegate
    tvcA.delegate = self;


    [acb makeAPICallWithArgument:self.argument];


    self.property1 = acb.property2;
}

My question is: What's the best way to to wait for [acb makeAPICallWithARgument:self.argument] to complete, so that self.property1 = acb.property2 will work? I'm assuming GCD could be used (`dispatch_sync'?) but, being new to iOS/Objective-C, I'm not sure where to use it. Or would it be better move one or both of those items elsewhere?

Here's the method from APICallerB:

- (void)makeAPICallWithArgument:(NSString *)arg
{


    NSString *requestString = [NSString stringWithFormat:@"http://%@:%@@apiurl.com/json/Request?arg=%@", API_USERNAME, API_KEY, arg];
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    config.HTTPAdditionalHeaders = @{@"Accept" : @"application/json"};
    NSURLSession *urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];

    NSURL *url = [NSURL URLWithString:requestString];
    NSURLRequest *req = [NSURLRequest requestWithURL:url];

    NSURLSessionDataTask *dataTask = [urlSession dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

        NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        NSArray *ar = [jsonObject[@"Result"] objectForKey:@"results"];
        self.property2 = ar;
        }];

  [dataTask resume];
}
Ja5onHoffman
  • 642
  • 8
  • 17

1 Answers1

2

You are calling asynchronous method so you should employ asynchronous pattern. For example, the completion block implementation might look like:

- (void)makeAPICallWithArgument:(NSString *)arg completionHandler:(void (^)(NSArray *results, NSError *error))completionHandler
{
    NSString *requestString = [NSString stringWithFormat:@"http://%@:%@@apiurl.com/json/Request?arg=%@", API_USERNAME, API_KEY, arg];
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    config.HTTPAdditionalHeaders = @{@"Accept" : @"application/json"};
    NSURLSession *urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];

    NSURL *url = [NSURL URLWithString:requestString];
    NSURLRequest *req = [NSURLRequest requestWithURL:url];

    NSURLSessionDataTask *dataTask = [urlSession dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (completionHandler) {
            if (error) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionHandler(nil, error);
                });
            } else {
                NSError *parseError;
                NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionHandler(jsonObject[@"Result"][@"results"], parseError);
                });
            }
        }
    }];

    [dataTask resume];
}

And you'd call it like:

[acb makeAPICallWithArgument:self.argument completionHandler:^(NSArray *results, NSError *error){
    // you'd probably first check to make sure you didn't have an error

    self.property1 = results;
}];

A couple of minor observations:

  1. I probably wouldn't create a new session every time. Save this session for future requests if you expect to do more than one call.

  2. Your makeAPICallWithArgument is updating a property, which you are trying to retrieve later. I'd retire that property and just pass the values back as parameters to the completion block.

  3. I've added a little error handling in this.

  4. Your [jsonObject[@"Result"] objectForKey:@"results"] doesn't look like it could be right. Do you really have JSON that returns a dictionary with a key of "Result" and inside that another dictionary with a key of "results". If so, fine, but that looks suspicious. And even if that was your JSON format, I'd simplify this to say jsonObject[@"Result"][@"results"].

  5. Elsewhere it was suggested that you might consider semaphores. That's almost always a bad idea. That's used to make an asynchronous method behave synchronously. But you never want to block the main queue.

    Using completion block pattern eliminates the need for semaphores. Apple provides an asynchronous API for a reason, so we should adopt asynchronous patterns when using it.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Great explanation. I'll implement this. – Ja5onHoffman Oct 18 '14 at 13:55
  • I feel this is essentially a non-answer, given the number of identical questions that have been asked. The query is "how do I wait for a DataTask to complete", not "how should I restructure my code to use async I/O". I think the former question is valid, and has not been answered (here, or elsewhere on the other identical questions). Particularly since `sendSynchronousRequest` is deprecated as of iOS 9. What is the answer for people who _want_ blocking I/O? – aroth Apr 01 '16 at 03:08
  • 1
    I hear you, but the simple truth is that 99.9% of the time, doing synchronous calls is just the wrong thing to do. It looks simpler (esp, for this unfamiliar with async patterns), but generally a _really_ bad idea. Apple retired synchronous calls for a reason. I feel like someone came up to me and says "gee, I'm feeling a bit depressed; how do I turn off the safety on this gun." We can show them how to do it, but it's almost invariably a great disservice to do so. But, this has been asked and answered many times, e.g.: http://stackoverflow.com/a/21205992/1271826 – Rob Apr 01 '16 at 03:59
  • Thanks, I find that answer to be much more useful. You'll probably hate this idea, but I rolled the approach suggested there into a [category on NSURLSession](https://github.com/adam-roth/sendSynchronous) to make it easier to consume (and also harder to accidentally block the main thread). – aroth Apr 01 '16 at 07:06
  • As you predicted, I do disagree with your contention that asynchronous API is no better than backgrounded synchronous one (e.g. new asynchronous API offers cancelable interface, less subject to abuse, consistent with emerging best practices, theoretically more efficient, etc.). And, IMHO, your rant about Apple's capricious API change is unfair: The synchronous request pattern deserved to be taken out back and be shot because it practically invited poor programming patterns. Apple shouldn't refrain from improving their API in new frameworks just because you happened to use old patterns. – Rob Apr 01 '16 at 08:13