Question

I'm building an iOS app that allows a user to take a picture and save it to a custom album, which populates a UICollectionView in the app with the photos from the album. I found plenty of examples on the web on how to do this, but I could NOT get past a stubborn problem with the most recent picture not showing up in the album. I ended building a work around using notifications and a serial dispatch queue, but I think it can be improved.

Here's how I'm saving the pictures and then repopulating the UICollectionView:

I built a method to call when I need to enumerate the photo album (viewDidLoad and once I've received a ALAssetsLibraryChangedNotification).

-(void)setupCollectionView {
    _assets = [@[] mutableCopy];
    __block NSMutableArray *tmpAssets = [@[] mutableCopy];

    // This grabs a static instance of the ALAssetsLibrary
    ALAssetsLibrary *assetsLibrary = [ProfileViewController defaultAssetsLibrary];    

    // Enumerate through all of the ALAssets (photos) in the user’s Asset Groups (Folders)
    [assetsLibrary enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
        if ([[group valueForProperty:ALAssetsGroupPropertyName] isEqualToString:albumName]) {
            // Make sure we have the group... (this may be redundant) 
            if (group == nil){
                return;
            }

            // I read in some SO posts that setting the filter might help, but I don't think it does
            NSLog(@"Refresh pictures - %d",[group numberOfAssets]);
            [group setAssetsFilter:[ALAssetsFilter allPhotos]];//this will cause group to reload
            if (group!=nil) {
                [group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
                    if(result){
                        [tmpAssets addObject:result];
                    }else{
                        *stop = YES;
                    }
                }];
            }

            self.assets = tmpAssets;

            // Reload the UICollectionView
            dispatch_async(dispatch_get_main_queue(), ^{
                [self.collectionView reloadData];
                [self.collectionView.collectionViewLayout invalidateLayout];
            });
        }
    } failureBlock:^(NSError *error) {
        NSLog(@"Error loading images %@", error);
    }];
}

Initially this seemed to be working just fine, but occasionally, when calling this after writing an image to the custom photo album, the photo that was just taken would not appear in the collection view. The photo would be saved to the library (it shows up in the Photos app) and after exiting and re-running the app the photo would appear, but the enumeration would not include that latest photo.

As suggested by a few Stack Overflow posts on the matter, I tried implementing ALAssets' notifications by listening for ALAssetsLibraryChangedNotification, but I've had some issues with that. Here's what I'm doing once I receive a notification:

- (void) handleAssetChangedNotifiation:(NSNotification *)notification {
    NSDictionary *userInfo = notification.userInfo;
    NSSet *updateAssets = userInfo[ALAssetLibraryUpdatedAssetsKey];
    if (updateAssets.count>0) {        
        dispatch_sync(photoLoadQueue, ^{
            [self setupCollectionView];
        });
    }
}

This seems fine to me. I'm checking to see if there's any data in the ALAssetLibraryUpdatedAssetsKey and then throwing it on the dispatch queue, which I'm started using once I discovered I was receiving several notifications for saving a single photo. Here're the notifications I'm receiving once I write a photo to the custom library:

Received notification: NSConcreteNotification 0x15ed4af0 {name = ALAssetsLibraryChangedNotification; object = <ALAssetsLibrary: 0x15db9e80>; userInfo = {
ALAssetLibraryUpdatedAssetGroupsKey = "{(\n    assets-library://group/?id=43E80EF8-EA91-4C71-AFD2-EBDCB53A1D53\n)}";
ALAssetLibraryUpdatedAssetsKey = "{(\n    assets-library://asset/asset.JPG?id=63E43AF3-3729-43E9-806C-BE463116FDA2&ext=JPG,\n    assets-library://asset/asset.JPG?id=FA2ED24E-DF0F-49A3-85B8-9C181E4077A4&ext=JPG,\n    assets-library://asset/asset.JPG?id=A0B66E2E-6EA1-462C-AD93-DB3087BA65D8&ext=JPG\n)}";}}

Received notification: NSConcreteNotification 0x15d538c0 {name = ALAssetsLibraryChangedNotification; object = <ALAssetsLibrary: 0x15db9e80>; userInfo = {
ALAssetLibraryUpdatedAssetsKey = "{(\n    assets-library://asset/asset.JPG?id=FBDD2086-EC76-473F-A23E-4F4200C0A6DF&ext=JPG\n)}";}}

Received notification: NSConcreteNotification 0x15dc9230 {name = ALAssetsLibraryChangedNotification; object = <ALAssetsLibrary: 0x15db9e80>; userInfo = {}}

Received notification: NSConcreteNotification 0x15df5b40 {name = ALAssetsLibraryChangedNotification; object = <ALAssetsLibrary: 0x15db9e80>; userInfo = {}}

Received notification: NSConcreteNotification 0x15d6a050 {name = ALAssetsLibraryChangedNotification; object = <ALAssetsLibrary: 0x15db9e80>; userInfo = {}}

Received notification: NSConcreteNotification 0x15d379e0 {name = ALAssetsLibraryChangedNotification; object = <ALAssetsLibrary: 0x15db9e80>; userInfo = {
ALAssetLibraryUpdatedAssetGroupsKey = "{(\n    assets-library://group/?id=1E76AFA7-89B4-4277-A175-D7C8E62E49D0&filter=1,\n    assets-library://group/?id=1E76AFA7-89B4-4277-A175-D7C8E62E49D0\n)}";
ALAssetLibraryUpdatedAssetsKey = "{(\n    assets-library://asset/asset.JPG?id=FBDD2086-EC76-473F-A23E-4F4200C0A6DF&ext=JPG\n)}";

Received notification: NSConcreteNotification 0x15df7bf0 {name = ALAssetsLibraryChangedNotification; object = <ALAssetsLibrary: 0x15db9e80>; userInfo = {
ALAssetLibraryUpdatedAssetGroupsKey = "{(\n    assets-library://group/?id=8E390051-E107-42BC-AE55-24BA35966642\n)}";}}

Received notification: NSConcreteNotification 0x15d40360 {name = ALAssetsLibraryChangedNotification; object = <ALAssetsLibrary: 0x15db9e80>; userInfo = {
ALAssetLibraryUpdatedAssetsKey = "{(\n    assets-library://asset/asset.JPG?id=5A45779A-C3C1-4545-9BD6-88F094FFA5B9&ext=JPG\n)}";}}

Why am I getting so many notifications? I don't see a way to look for one particular message that will tell me once the image is ready, so I used that dispatch queue to serially fire off the enumeration. Does anyone have any experience with this or a solution? Mine works, but I'm definitely doing more work than I should.

Just to be complete, here's the method where I'm saving the image (I probably shouldn't nest these blocks like I have, but I did this to try and fix the added image not being detected):

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
    UIImage *originalImage, *editedImage, *imageToSave;
    editedImage = (UIImage *) [info objectForKey:UIImagePickerControllerEditedImage];
    originalImage = (UIImage *) [info objectForKey:UIImagePickerControllerOriginalImage];
    if (editedImage) {
        imageToSave = editedImage;
    }else {
        imageToSave = originalImage;
    }

    // Get group/asset library/album
    __block ALAssetsGroup* groupToAddTo;
    ALAssetsLibrary *assetsLibrary = [ProfileViewController defaultAssetsLibrary];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:ALAssetsLibraryChangedNotification object:nil];

    [assetsLibrary enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
    if ([[group valueForProperty:ALAssetsGroupPropertyName] isEqualToString:albumName]) {
        NSLog(@"found album %@", albumName);
        groupToAddTo = group;

        // save to album
        CGImageRef img = [imageToSave CGImage];

        // There's some orientation stuff here I pulled out for brevity's sake 

        [assetsLibrary writeImageToSavedPhotosAlbum:img orientation: alOrientation completionBlock:^(NSURL* assetURL, NSError* error) {
            if (error.code == 0) {
                [assetsLibrary assetForURL:assetURL resultBlock:^(ALAsset *asset) {
                // assign the photo to the album
                    if ([groupToAddTo valueForProperty:ALAssetsGroupPropertyURL] == nil){
                        NSLog(@"group properties are nil!");
                    }else {
                        if([groupToAddTo addAsset:asset]){
                            // If the asset writes to the library, listen for the notification
                            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAssetChangedNotifiation:) name:ALAssetsLibraryChangedNotification object:nil];
                        }else {
                            NSLog(@"Image Not Added!");
                        }
                    }
                }
                failureBlock:^(NSError* error) {
                    NSLog(@"failed to retrieve image asset:\nError: %@ ", [error localizedDescription]);
                }];
            }else {
                NSLog(@"saved image failed.\nerror code %i\n%@", error.code, [error localizedDescription]);
            }
        }];
    }}
    failureBlock:^(NSError* error) {
        NSLog(@"failed to enumerate albums:\nError: %@", [error localizedDescription]);}];

    [picker dismissViewControllerAnimated:YES completion:NULL];
}
Was it helpful?

Solution

The thing to realize is that the blocks associated with ALAssetsLibrary are asynchronous. Thus, code like this doesn't work:

        if (group!=nil) {
            [group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
                if(result){
                    [tmpAssets addObject:result];
                }else{
                    *stop = YES;
                }
            }];
        }
        self.assets = tmpAssets;

The "last" line, self.assets = tmpAssets, runs before the block - and thus tmpAssets never has a chance to be set properly.

The same sort of thing is true for all blocks associated with ALAssetsLibrary calls.

Once you grasp this you can restructure your code so that everything happens in the correct order. In fact, it turns out that this is what the extra pass through the enumerating blocks is for. As I write in my book:

I was initially mystified by this curious block enumeration behavior, but one day the reason for it came to me in a flash: these blocks are all called asynchronously (on the main thread), meaning that the rest of your code has already finished running, so you're given an extra pass through the block as your first opportunity to do something with all the data you've presumably gathered in the previous passes.

So you see, you want your code to be structured more like this:

        if (group!=nil) {
            [group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
                if(result){
                    [tmpAssets addObject:result];
                }else{
                    self.assets = tmpAssets; // <--- !!!!
                    // and now proceed to whatever the _next_ step is...!
                }
            }];
        }

So, you see, you've totally gone down the wrong road. Get rid of all your notifications and restructure all your code completely in the light of this new knowledge. ALAssetsLibrary is completely coherent once you understand this simple but poorly documented fact about how it works.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top