2

I have an array of PFFiles I retrieve from parse. The PFFiles must be converted to images and I am trying to convert them in a loop, however;

The array of converted images must be in the same order of the array containing the PFFiles.

The problem is, is the loop runs and causes all of the blocks to trigger, then they are all loading at the same time thus the imageArray will result in a different order to the original array of PFFiles, because if one object finishes loading before the previous object, it will be added to the array before the previous one, making it go out of order.

Instead, I would like a loop where it loops through every object in the array, however it doesn't loop onto the next object until the getDataInBackgroundBlock has finished loading and the current object has been added to the new array.

-(void)organizePhotos {
    for (PFFile *picture in pictureArray) {
        //This block loads, after the next index in the loop is called, causing the array to be out of order.
        [picture getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
            [imageArray addObject:[UIImage imageWithData:data]];
            [self savePhotos];
        }];
    }
}

Above is my code. Is there anyway I can make the loop wait until the getDatanBackgroundWithBlock finishes loading?

Pang
  • 9,564
  • 146
  • 81
  • 122
Josh
  • 745
  • 1
  • 7
  • 22
  • Apart from what NickEntin has suggested, you could use dependent operations if you want to use clean Objective-C solution. – dispatchMain Aug 23 '15 at 07:42

5 Answers5

2

Ideally, you should use a synchronous method; however the behavior you are asking for can be achieved using Grand Central Dispatch.

First, create a dispatch_group_t using dispatch_group_create(). Let's call it asyncGroup.

Then, before calling the async method, call dispatch_group_enter(asyncGroup). This increments the counter of the number of calls in the group.

Then, at the end of the async block, call dispatch_group_leave(asyncGroup) to decrement the counter of the number of calls in the group.

Finally, after calling the async method, call dispatch_group_wait(asyncGroup, timeout) to pause thread execution until the group counter reaches zero. Since you increment, make the call, and then wait, the loop will only continue when the async block has been run. Just ensure the timeout is longer than the operation will take.

NickEntin
  • 431
  • 4
  • 16
2

You can make the code synchronous:

-(void)organizePhotos {
    for (PFFile *picture in pictureArray) {
         [imageArray addObject:[UIImage imageWithData:[picture getData]]];
        [self savePhotos];
    }
}

and run it in the background:

[self performSelectorInBackground:@selector(organizePhotos) withObject:nil];
Islam
  • 3,654
  • 3
  • 30
  • 40
2

The problem is, is the loop runs and causes all of the blocks to trigger, then they are all loading at the same time

That is not a problem, it is good - the reason why the call is asynchronous is it can take an arbitrarily long time to complete. If you've got multiple downloads to do then doing then concurrently can be a big win.

thus the imageArray will result in a different order to the original array of PFFiles, because if one object finishes loading before the previous object, it will be added to the array before the previous one, making it go out of order.

This is the problem and can be addressed in a number of ways.

As you are using arrays, here is a simple array based solution: first create yourself an array of the right size and fill it with nulls to indicate the image hasn't yet arrived:

(all code typed directly into answer, treat as pseudo-code and expect some errors)

NSUInteger numberOfImages = pictureArray.length;

NSMutableArray *downloadedImages = [NSMutableArray arrayWithCapacity:numberOfImages];
// You cannot set a specific element if it is past the end of an array
// so pre-fill the array with nulls
NSUInteger count = numberOfImages;
while (count-- > 0)
   [downloadedImages addObject:[NSNull null]];

Now you have your pre-filled array just modify your existing loop to write the downloaded image into the correct index:

for (NSUInteger ix = 0; ix < numberOfImages; ix++)
{
    PFFile *picture = pictureArray[ix];
    [picture getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
        dispatch_async(dispatch_get_main_queue(),
                       ^{ [imageArray replaceObjectAtIndex:ix
                                                withObject:[UIImage imageWithData:data]
                        });
        [self savePhotos];
    }];
}

The use of dispatch_async here is to ensure there are not concurrent updates to the array.

If you wish to know when all images have been downloaded you can check for that within the dispatch_async block, e.g. it can increment a counter safely as it is running on the main thread and call a method/issue a notification/invoke a block when all the images have downloaded.

You are also possibly making things harder on yourself by using arrays, and trying to keep items in different arrays related by position. Dictionaries could save you some of the hassle, for example each of your PFFile objects presumably relates to a different URL, and a URL is a perfectly valid key for a dictionary. So you could do the following:

NSMutableDictionary *imageDict = [NSMutableDictionary new];

for (PFFile *picture in pictureArray) {
    [picture getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
        dispatch_async(dispatch_get_main_queue(),
                       ^{ [imageDict setObject:[UIImage imageWithData:data] forKey:picture.URL];
                        };
        [self savePhotos];
    }];
}

And your can locate the image for a PFFile instance by looking up its URL in the dictionary - which will return nil if the image is not yet loaded.

There are other solutions, and you might want to look into making your code more asynchronous. Whatever you do trying to call asynchronous code synchronously is not a good idea and will impact the user experience.

HTH

CRD
  • 52,522
  • 5
  • 70
  • 86
1

You can use GCD approach which is to use dispatch_group. So, before you start an asynchronous task, call dispatch_group_enter, and then when the asynchronous task finishes, call dispatch_group_leave, and you can then create a dispatch_group_notify which will be called when the asynchronous tasks finish. You can marry this with a completion block pattern (which is a good idea for asynchronous methods, anyway):

Here is similar Question. Perhaps it may also be helpful: How to wait for method that has completion block (all on main thread)?

Community
  • 1
  • 1
Niraj
  • 1,939
  • 20
  • 29
0

You could do that using dispatch_async way, using semaphory, groups, but for your need better practice is using block to get response and do what you need.

Doing that:

-(void)organizePhotos:(void(^)(BOOL responseStatus))status {
for (PFFile *picture in pictureArray) {
    //This block loads, after the next index in the loop is called, causing the array to be out of order.
    [picture getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
        [imageArray addObject:[UIImage imageWithData:data]];
        status(YES);
    }];
}

}

So you next at your call you could do that:

__weak typeof(self) weakSelf = self;
[self organizePhotos:^(BOOL responseStatus) {
 if(responseStatus){
       [weakSelf savePhotos];
 }   
}];

Or if you dont wan't create extra parameter response to your method you could do like this:

-(void)organizePhotos {

__weak typeof(self) weakSelf = self;
void (^ResponseBlock)(void) = ^{
     [weakSelf savePhotos];
};

for (PFFile *picture in pictureArray) {
    //This block loads, after the next index in the loop is called, causing the array to be out of order.
    [picture getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
        [imageArray addObject:[UIImage imageWithData:data]];
        ResponseBlock();
    }];
}

}

And another point that you are making wrong when calling "self" inside block, this could lead you to retain cycle and its a bad practice, look at my code how you should do.

Felipe Florencio
  • 325
  • 2
  • 10