pbuchheit
pbuchheit

Reputation: 1607

Incrementing a Variable from an Asynchronous Block in Objective-C

I have run into a bit of a conundrum with a service I am working on in objective-c. The purpose of the service is to parse through a list of core-data entities and download a corresponding image file for each object. The original design of the service was choking my web-server with too many simultaneous download requests. To get around that, I moved the code responsible for executing the download request into a recursive method. The completion handler for each download request will call the method again, thus ensuring that each download will wait for the previous one to complete before dispatching.

Where things get tricky is the code responsible for actually updating my core-data model and the progress indicator view. In the completion handler for the download, before the method recurses, I make an asynchronous call the a block that is responsible for updating the core data and then updating the view to show the progress. That block needs to have a variable to track how many times the block has been executed. In the original code, I could simply have a method-level variable with block scope that would get incremented inside the block. Since the method is recursive now, that strategy no longer works. The method level variable would simply get reset on each recursion. I can't simply pass the variable to the next level either thanks to the async nature of the block calls.

I'm at a total loss here. Can anyone suggest an approach for dealing with this?

Update: As matt pointed out below, the core issue here is how to control the timing of the requests. After doing some more research, I found out why my original code was not working. As it turns out, the timeout interval starts running as soon as the first task is initiated, and once the time is up, any additional requests would fail. If you know exactly how much time all your requests will take, it is possible to simply increase the timeout on your requests. The better approach however is to use an NSOperationQueue to control when the requests are dispatched. For a great example of how to do this see: https://code-examples.net/en/q/19c5248 If you take this approach, keep in mind that you will have to call the completeOperation() method of each operation you create on the completion handler of the downloadTask.

Some sample code:

-(void) downloadSkuImages:(NSArray *) imagesToDownload onComplete:(void (^)(BOOL update,NSError *error))onComplete
{
    [self runSerializedRequests:imagesToDownload progress:weakProgress downloaded:0 index:0 onComplete:onComplete ];
}

-(void)runSerializedRequests:(NSArray *) skuImages progress:(NSProgress *) progress downloaded:(int) totalDownloaded index:(NSUInteger) index onComplete:(void (^)(BOOL update,NSError *error))onComplete 
{
     int __block downloaded = totalDownloaded;

     TotalDownloadProgressBlock totalDownloadProgressBlock =  ^BOOL (SkuImageID *skuImageId, NSString  *imageFilePath, NSError *error) {
          if(error==nil) {
                  downloaded++;
                  weakProgress.completedUnitCount = downloaded;
                  //save change to core-data here
                  }
          else {
                        downloaded++;
                        weakProgress.completedUnitCount = downloaded;
                        [weakSelf setSyncOperationDetail:[NSString stringWithFormat:@"Problem downloading sku image %@",error.localizedDescription]];
                      }

          if(weakProgress.totalUnitCount==weakProgress.completedUnitCount) {
                              [weakSelf setSyncOperationIndicator:SYNC_INDICATOR_WORKING];
                              [weakSelf setSyncOperationDetail:@"All product images up to date"];
                              [weakSelf setSyncOperationStatus:SYNC_STATUS_SUCCESS];
                              weakProgress.totalUnitCount = 1;
                              weakProgress.completedUnitCount = 1;
                              onComplete(false,nil);
                              return true;
                          }
          return false;
     };

    NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:nil
    completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {

                NSLog(@"finished download %u of %lu", index +1, (unsigned long)skuImages.count);
                if(error != nil)
                {                    
                    NSLog(@"Download failed for URL: %@ with error: %@",skuImage.url, error.localizedDescription);
                }
                else
                {
                    NSLog(@"Download succeeded for URL: %@", skuImage.url);
                }
                dispatch_async(dispatch_get_main_queue(), ^(void){

                    totalDownloadProgressBlock(skuImageId, imageFilePath, error);

                });

                [self runSerializedRequests:manager skuImages:skuImages progress:progress downloaded:downloaded index:index+1 onComplete:onComplete ];
            }];

            NSLog(@"Starting download %u of %lu", index +1, (unsigned long)skuImages.count);
            [downloadTask resume];
}

Upvotes: 2

Views: 240

Answers (1)

matt
matt

Reputation: 534977

The original design of the service was choking my web-server with too many simultaneous download requests. To get around that, I moved the code responsible for executing the download request into a recursive method.

But that was never the right way to solve the problem. Use a single persistent custom NSURLSession with your own configuration, and set the configuration's httpMaximumConnectionsPerHost.

Upvotes: 2

Related Questions