Question

Previous: I am trying to get the number of photos in my camera roll, I started using blocks but have been having some difficulties.

Now: Here is my updated code, I am using async behavior but am returning a value before my block has a chance to complete.

#import "CoverViewController.h"
#import <AssetsLibrary/AssetsLibrary.h>


@interface CoverViewController () <UITextFieldDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>

@property(nonatomic, weak) IBOutlet UICollectionView *collectionView;

@property (assign) NSUInteger numberOfPhotos;


@end



@implementation CoverViewController


@synthesize CoverView;


- (void)viewWillAppear:(BOOL)animated
{
    NSLog(@"viewWillAppear");
    [self beginLoadingPhotoInfo];
}




//Photo collection info
- (void)beginLoadingPhotoInfo {
    NSLog(@"loaded beginloadingphotoinfo");
    __weak CoverViewController *__self = self;
    [self PhotoCount:^(NSUInteger photoCount) {
        __self.numberOfPhotos = photoCount;
    }];
    //[__self.CoverView reloadData];
}

- (void)PhotoCount:(void(^)(NSUInteger))completionBlock {

    NSLog(@"photo count start");
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    __block NSInteger result = 0;


    void (^assetGroupEnumerator)(ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop){
        if(group != nil) {
            NSLog(@"if");
            result += [group numberOfAssets];
            NSLog(@"enum: %d", result);
        }
        else {
            NSLog(@"else");
            //nil means we are done with enumeration
            if (completionBlock) {
                completionBlock(result);
            }
        }
    };

    [library enumerateGroupsWithTypes: ALAssetsGroupSavedPhotos
                           usingBlock:assetGroupEnumerator
                         failureBlock:^(NSError *error) {NSLog(@"Problems");}
     ];

    NSLog(@"result: %u", result);
}




// Layout of collection


//Number of cells in collection
- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section {

    NSLog(@"returned number: %d", self.numberOfPhotos);
    return self.numberOfPhotos;
}


@end

I've added everything that is relevant in my coverviewcontroller.h file but the code seems to be loading before the block finishes. Here is my attempt to find the error and it shows that photoCount is returning 0 before my block finishes.

2013-07-27 21:16:00.986 slidr[1523:c07] viewWillAppear
2013-07-27 21:16:00.987 slidr[1523:c07] loaded beginloadingphotoinfo
2013-07-27 21:16:00.987 slidr[1523:c07] photo count start
2013-07-27 21:16:00.988 slidr[1523:c07] result: 0
2013-07-27 21:16:00.989 slidr[1523:c07] returned number: 0
2013-07-27 21:16:01.012 slidr[1523:c07] if
2013-07-27 21:16:01.013 slidr[1523:c07] enum: 3
2013-07-27 21:16:01.014 slidr[1523:c07] else

any ideas?

Was it helpful?

Solution

You are mixing and matching synchronous and asynchronous methodologies. There are two possible approaches.

  1. Make the return of your photoCount method asynchronous by passing a completion block.

    - (void)PhotoCount:(void(^)(NSUInteger))completionBlock {
    
        ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
        __block NSInteger result = 0;
    
        void (^assetGroupEnumerator)(ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop){
            if(group != nil) {
                if([[group valueForProperty:ALAssetPropertyType] isEqualToString:ALAssetTypePhoto]) {
                    result += [group numberOfAssets];
                }
            } else {
                // nil group signals we're done with enumeration
                if (completionBlock) {
                    completionBlock(result);
                }
            }
        };
    
        [library enumerateGroupsWithTypes: ALAssetsGroupSavedPhotos
                               usingBlock:assetGroupEnumerator
                             failureBlock:^(NSError *error) {NSLog(@"Problems");}
         ];
    }
    

or

  1. Block the current thread until the operation is complete. This is not usually a good idea in an interactive user app. If you're doing this, you should rethink how this part of the application is structured:

    - (NSUInteger)PhotoCount {
    
        ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
        __block NSInteger result = 0;
    
        dispatch_semaphore_t blockSemaphore = dispatch_semaphore_create(0);
    
        void (^assetGroupEnumerator)(ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop){
            if(group != nil) {
                if([[group valueForProperty:ALAssetPropertyType] isEqualToString:ALAssetTypePhoto]) {
                    result += [group numberOfAssets];
                }
            } else {
                // nil group signals we're done with enumeration
                dispatch_semaphore_signal(blockSemaphore);
            }
        };
    
        [library enumerateGroupsWithTypes: ALAssetsGroupSavedPhotos
                               usingBlock:assetGroupEnumerator
                             failureBlock:^(NSError *error) {NSLog(@"Problems");}
         ];
    
        dispatch_semaphore_wait(blockSemaphore, DISPATCH_TIME_FOREVER);
    
        NSLog(@"%u", result);
        return result;
    }
    

You were also using the resultBlock variable in a way I didn't understand at all, so I omitted it from my answer.

Just to be clear, I wouldn't go with option 2. It will cause a noticeable delay in the responsiveness of your app, especially if you're calling this on the main thread, and especially if the user has a large asset library.

A great benefit of programming with blocks is that you can defer doing work until you have all the information you need to do the work. Presumably you're going to change some UI element in response to whatever this number turns out to be. Put the code to do that in the completion block that you pass to the method above. Or better yet, factor that code out into a method that you then invoke from the block.


To continue with how you'd use the asynchronous approach, in the collection view controller, you'd want a method that precomputes this number, perhaps when the controller loads:

@property (assign) NSUInteger numberOfPhotos;


- (void)beginLoadingCollectionInformation {
    __weak <<<Type of Self>>> *__self = self;
    [self PhotoCount:^(NSUInteger photoCount) {
        __self.numberOfPhotos = photoCount;
        [__self.collectionView reloadData];
    }];
}

- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section {
    return self.numberOfPhotos;
}

If you can manage to get the photo count information into your class before the view appears, that might be ideal. Otherwise, you'll have to insert the content afterwards. It's not ideal, and it's not exactly what I'd do in this case. But this question is starting to stray off into data sources, delegation patterns, and app architecture – well beyond the question of synchronous and asyncronous return values on methods.

Hope that helps.


One last note answering the last question I think you're asking. Change your data reloading call so that it happens within the completion block:

//Photo collection info
- (void)beginLoadingPhotoInfo {
    NSLog(@"loaded beginloadingphotoinfo");
    __weak CoverViewController *__self = self;
    [self PhotoCount:^(NSUInteger photoCount) {
        __self.numberOfPhotos = photoCount;
        [__self.CoverView reloadData];
    }];
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top