13

I have a loop set up that downloads a series a images which I will later use for to animate using the animationImages property of UIImageView. I would like to know when all the blocks inside my loops have finished executing so I could begin the animation, and was wondering how I may be able to tell when they are finished completing? Thanks!

for (PFObject *pictureObject in objects){

    PFFile *imageFile = [pictureObject objectForKey:@"image"];
    NSURL *imageFileURL = [[NSURL alloc] initWithString:imageFile.url];
    NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageFileURL];

    [tokenImageView setImageWithURLRequest:imageRequest placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
       [self.downloadedUIImages addObject:image]; //This is a mutableArray that will later be set to an UIImageView's animnationImages

    } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
        NSLog(@"Error %@", error);
    }];

}
//When I know all the blocks have finished downloading, I will then to animate the downloaded images. 

Edit: having issue with Error -999

I am encountering the following issue when executing the code in the provided answer: Domain=NSURLErrorDomain Code=-999 "The operation couldn’t be completed. (NSURLErrorDomain error -999.)"

A quick search reveals that Error -999 means "another request is made before the previous request is completed" ... which is certainly the case here since I am making several requests in quick succession. The recommended fix suggested here didn't work for me as it will only successfully download one UIImage (the last one requested) , with the previous ones failing. I was wondering if there is workaround here or in AFNetworking that I ought to consider? Thanks!

Edit 2: working code based on @David's solution

for (PFObject *pictureObject in objects){

    PFFile *imageFile = [pictureObject objectForKey:@"image"];
    NSURL *imageFileURL = [[NSURL alloc] initWithString:imageFile.url];
    NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageFileURL];
    AFHTTPRequestOperation *requestOperation = [[AFHTTPRequestOperation alloc] initWithRequest:imageRequest];
    requestOperation.responseSerializer = [AFImageResponseSerializer serializer];

    dispatch_group_enter(group);
    [requestOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSLog(@"Response: %@", responseObject);
        UIImage *retrivedImage = (UIImage *)responseObject;
        [self.downloadedUIImages addObject:retrivedImage];

        dispatch_group_leave(group);

    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Image error: %@", error);

        dispatch_group_leave(group);
    }];

    [requestOperation start];
    counter ++;
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"Horray everything has completed");
    NSLog(@"What is here %@", self.downloadedUIImages);
    NSLog(@"Done");

});
Community
  • 1
  • 1
daspianist
  • 5,336
  • 8
  • 50
  • 94
  • IMHO, how to get notified when all tasks have been finished isn't your real problem here. The more challenging problem is that your parallel tasks should be cancelable if this should be necessary (and it definitely will in any real application). – CouchDeveloper Apr 23 '14 at 19:30
  • Thanks for commenting @CouchDeveloper. Could you clarify what you mean by "tasks should be cancelable" if it is a real application? I am fairly new at this, so if there is literature on this topic that you think I should check out I'd also appreciate you referencing it here. Thanks! – daspianist Apr 23 '14 at 19:53
  • So, why should an asynchronous task be cancelable? Suppose, your network stalls, and downloading images may take for ever. Now, do you force the user to wait, no matter what until all requests timed out - or do you think it might be user-friendly and a good idea to provide a "Cancel" button where the user can stop these tasks at any time when she want's to? ;) Hint: NSURLConnection and also NSURLSessionTask can be cancelled. Caveat: the accepted answer isn't suitable to support this :/ – CouchDeveloper Apr 23 '14 at 20:10
  • 1
    Very nice and useful question helped me a lot in my project thank you so much – Nishad Arora May 15 '17 at 09:13

4 Answers4

34

Create a dispatch group, in the for loop enter the group, in the completion block leave the group. Then you can use dispatch_group_notify to find out when all blocks have completed:

dispatch_group_t    group = dispatch_group_create();

for (PFObject *pictureObject in objects){

    PFFile *imageFile = [pictureObject objectForKey:@"image"];
    NSURL *imageFileURL = [[NSURL alloc] initWithString:imageFile.url];
    NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageFileURL];

    dispatch_group_enter(group);
    [tokenImageView setImageWithURLRequest:imageRequest placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
        [self.downloadedUIImages addObject:image]; //This is a mutableArray that will later be set to an UIImageView's animnationImages
        dispatch_group_leave(group);
    } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
        NSLog(@"Error %@", error);
        dispatch_group_leave(group);
    }];

}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // do your completion stuff here
});
David Berry
  • 40,941
  • 12
  • 84
  • 95
  • As a follow up, I seem to only succeed in downloading one of the images, with the rest giving the error `Error Domain=NSURLErrorDomain Code=-999 "The operation couldn’t be completed. (NSURLErrorDomain error -999.)`. A quick search reveals that this has to do with AFNetworking.. but even with the recommended solution provided here (http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os) I still can't seem to get the rest of the images to download. – daspianist Apr 23 '14 at 20:27
  • An addition comment: it appears error `-999` means "another request is made before the previous request is completed." (http://stackoverflow.com/questions/16073519/nsurlerrordomain-error-code-999-in-ios). Would you know how I would be able to simultaneously send different requests to be executed on different threads? – daspianist Apr 23 '14 at 20:31
  • 2
    I'd guess that that problem is caused somewhere within setImageWithURLRequest. Mostly likely because you're repeatedly setting the url for the same image view, which probably cancels the previous request. Instead of using tokenImageView to handle the download, handle it more directly using AFNetworking or similar kit. You can certainly have multiple requests processing at the same time. -999 indicates request cancelled, not duplicate request. – David Berry Apr 23 '14 at 20:36
  • Great - this is helpful. Will try a different method to download the UIImage. Thanks to both of you. – daspianist Apr 23 '14 at 20:39
  • Thank you - this is a great answer! – Michael Royzen Nov 28 '16 at 00:45
  • Thank you! This worked perfectly, I wasted so many hours with counters and what not. The example in your post helped me fix it within a couple of minutes. – Curious101 Dec 21 '16 at 21:24
2

Count how many you've completed. The challenging part is making it thread safe. I recommend creating an atomic counter class for that.

Generic solution!

+ (void)runBlocksInParallel:(NSArray *)blocks completion:(CompletionBlock)completion {
    AtomicCounter *completionCounter = [[AtomicCounter alloc] initWithValue:blocks.count];
    for (AsyncBlock block in blocks) {
        block(^{
            if ([completionCounter decrementAndGet] == 0) {
                if (completion) completion();
            }
        });
    }
    if (blocks.count == 0) {
        if (completion) completion();
    }
}

NSMutableArray *asyncBlocks = [NSMutableArray array];
for (PFObject *pictureObject in objects){
    [asyncBlocks addObject:^(CompletionBlock completion) {
        PFFile *imageFile = [pictureObject objectForKey:@"image"];
        NSURL *imageFileURL = [[NSURL alloc] initWithString:imageFile.url];
        NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageFileURL];

        [tokenImageView setImageWithURLRequest:imageRequest placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
           [self.downloadedUIImages addObject:image]; //This is a mutableArray that will later be set to an UIImageView's animnationImages
        } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
            NSLog(@"Error %@", error);
        } completion:completion];
    }];
}
[BlockRunner runBlocksInParallel:[asyncBlocks copy] completion:^{
    //Do your final completion here!
}];
CrimsonChris
  • 4,651
  • 2
  • 19
  • 30
  • You can make it thread-safe through using a dedicated dispatch queue (possibly the main queue) for accessing the counter and the property `downloadedUIImages`. – CouchDeveloper Apr 23 '14 at 20:30
  • @CouchDeveloper My implementation of `AtomicCounter` uses `NSLock` to make it thread safe. I find it simpler than messing with the dispatch queues. – CrimsonChris Apr 23 '14 at 20:39
0

Set up a property and initialize it to the number of cycles - objects.count. In the completion of the block, lower the number down. When you reach zero, you are done.

Michal
  • 15,429
  • 10
  • 73
  • 104
  • What if the last two blocks complete at the same time on different threads? – CrimsonChris Apr 23 '14 at 19:32
  • 1
    @CrimsonChris: `setImageWithURLRequest:...` seems to be from from AFNetworking, which (if I understand it correctly) calls the completion blocks on the main thread by default. – Martin R Apr 23 '14 at 19:47
  • 1
    Perhaps the anonymous downvoter comes back some time and changes his decision ... – Martin R Apr 23 '14 at 20:08
-3
for (PFObject *pictureObject in objects){

    PFFile *imageFile = [pictureObject objectForKey:@"image"];
    NSURL *imageFileURL = [[NSURL alloc] initWithString:imageFile.url];
    NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageFileURL];

    [tokenImageView setImageWithURLRequest:imageRequest placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
       [self.downloadedUIImages addObject:image]; //This is a mutableArray that will later be set to an UIImageView's animnationImages
       if([[objects lastObject] isEqual:pictureObject]) {
           [self animateImages];
       }
    } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
        NSLog(@"Error %@", error);
        if([[objects lastObject] isEqual:pictureObject]) {
           [self animateImages];
       }
    }];

}

- (void)animateImages {
    //do animation here.
}
Burhanuddin Sunelwala
  • 5,318
  • 3
  • 25
  • 51