Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to know when a for loop with NSURLSessionDataTasks is complete

I'm calling a method that will enumerate through an array, create an NSURL, and call an NSURLSessionDataTask that returns JSON. The loop typically runs about 10 times but can vary depending on the day.

I need to wait for the for loop and all NSURLSessionDataTasks to complete before I can start processing the data.

I'm having a hard time figuring out when all the work is complete. Could anyone recommend any ways or logic to know when the entire method is complete (for loop and data tasks)?

-(void)findStationsByRoute{
for (NSString *stopID in self.allRoutes) {
    NSString *urlString =[NSString stringWithFormat:@"http://truetime.csta.com/developer/api/v1/stopsbyroute?route=%@", stopID];
    NSURL *url = [NSURL URLWithString:urlString];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request
                                                 completionHandler:^(NSData *data,
                                                                     NSURLResponse *response,
                                                                     NSError *error) {
                                                     NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
                                                     if(httpResponse.statusCode == 200){
                                                         NSError *jsonError = [[NSError alloc]init];
                                                         NSDictionary *stopLocationDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError];
                                                         NSArray *stopDirectionArray = [stopLocationDictionary objectForKey:@"direction"];
                                                         for (NSDictionary * _stopDictionary in stopDirectionArray) {
                                                             NSArray *stop =   [_stopDictionary objectForKey:@"stop"];
                                                             [self.arrayOfStops addObject:stop];

                                                         }
                                                     }

                                                 }];

    [task resume];
}

}

like image 977
slidmac07 Avatar asked Feb 15 '23 07:02

slidmac07


1 Answers

There are a number of options. The fundamental issue is that these individual data tasks run asynchronously, so you need some way to keep track of these asynchronous tasks and establish some dependency on their completion.

There are several possible approaches:

  1. The typical solution is to employ a dispatch group. Enter the group before you start the request with dispatch_group_enter, leave the group with dispatch_group_leave inside the completion handler, which is called asynchronously, and then, at the end of the loop, supply a dispatch_group_notify block that will be called asynchronously when all of the "enter" calls are offset by corresponding "leave" calls:

    - (void)findStationsByRoute {
        dispatch_group_t group = dispatch_group_create();
    
        for (NSString *stopID in self.allRoutes) {
            NSString     *urlString = [NSString stringWithFormat:@"http://truetime.csta.com/developer/api/v1/stopsbyroute?route=%@", stopID];
            NSURL        *url       = [NSURL URLWithString:urlString];
            NSURLRequest *request   = [NSURLRequest requestWithURL:url];
    
            dispatch_group_enter(group);   // enter group before making request
    
            NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
                if(httpResponse.statusCode == 200){
                    NSError *jsonError;   // Note, do not initialize this with [[NSError alloc]init];
                    NSDictionary *stopLocationDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
                    NSArray *stopDirectionArray = [stopLocationDictionary objectForKey:@"direction"];
                    for (NSDictionary *stopDictionary in stopDirectionArray) {
                        NSArray *stop = [stopDictionary objectForKey:@"stop"];
                        [self.arrayOfStops addObject:stop];
                    }
                }
    
                dispatch_group_leave(group);  // leave group from within the completion handler
            }];
    
            [task resume];
        }
    
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            // do something when they're all done
        });
    }
    
  2. A more sophisticated way to handle this is to wrap the NSSessionDataTask in a NSOperation subclass and you can then use dependencies between your data task operations and your final completion operation. You'll want to ensure your individual data task operations are "concurrent" ones (i.e. do not issue isFinished notification until the asynchronous data task is done). The benefit of this approach is that you can set maxConcurrentOperationCount to constrain how many requests will be started at any given time. Generally you want to constrain it to 3-4 requests at a time.

    Note, this can also address timeout issues from which the dispatch group approach can suffer from. Dispatch groups don't constrain how many requests are submitted at any given time, whereas this is easily accomplished with NSOperation.

    For more information, see the discussion about "concurrent operations" in the Operation Queue section of the Concurrency Programming Guide.

    For an example of wrapping NSURLSessionTask requests in asynchronous NSOperation subclass, see a simple implementation the latter half NSURLSession with NSBlockOperation and queues. This question was addressing a different topic, but I include a NSOperation subclass example at the end.

  3. If instead of data tasks you used upload/download tasks, you could then use a [NSURLSessionConfiguration backgroundSessionConfiguration] and URLSessionDidFinishEventsForBackgroundURLSession: of your NSURLSessionDelegate would then get called when all of the tasks are done and the app is brought back into the foreground. (A little annoyingly, though, this is only called if your app was not active when the downloads finished: I wish there was a rendition of this delegate method that was called even if the app was in the foreground when the downloads finished.)

    While you asked about data tasks (which cannot be used with background sessions), using background session with upload/download tasks enjoys a significant advantage of background operation. If your process really takes 10 minutes (which seems extraordinary), refactoring this for background session might offer significant advantages.

  4. I hate to even mention this, but for the sake a completeness, I should acknowledge that you could theoretically just by maintain an mutable array or dictionary of pending data tasks, and upon the completion of every data task, remove an item from that list, and, if it concludes it is the last task, then manually initiate the completion process.

like image 177
Rob Avatar answered Mar 16 '23 00:03

Rob