Returning NSArray of UIImages from ALAssetsLibrary - ios

I'm working on photo app and here is the deal:
I have created custom photo album an added some photos to it (users can create many albums). I used ALAssetsLibrary for this and CustomPhotoAlbum category. Everything worked flawlessly until I needed to get those photos back to reuse them.
So Ive created custom metod for this category and I'm saving UIImages to NSMutableArray but I want this method to return this array outside... My problem is that enumerateAssetsUsingBlock: is running in background and saving photos to album while I want to access them! How can I do something for this method to wait until every photo is loaded to album and then return NSArray?
Here is my method:
- (void)loadImagesFromAlbumNamed:(NSString *)name
{
NSMutableArray *photos = [[NSMutableArray alloc] init];
[self enumerateGroupsWithTypes:ALAssetsGroupAll
usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if (group == nil) {
return;
}
if ([[group valueForProperty:ALAssetsGroupPropertyName] isEqualToString:name]) {
[group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result == nil) {
return;
}
// NSLog(#"%#", result);
UIImage *image = [UIImage imageWithCGImage:(__bridge CGImageRef)([result defaultRepresentation])];
[photos addObject:image];
}];
NSLog(#"%#", [photos description]);
}
} failureBlock:^(NSError *error) {
NSLog(#"%#", [error localizedDescription]);
}];
}
I hope you get my point of view... If not, just ask and I'll try to explain it better way!

Don't try to wait. Instead, pass a block to your method which should be called when all of the asset processing blocks are completed and will pass the array of images back to the caller. The caller should not block, you just want to structure your method to deal with the asynchronous nature of what you're trying to do.

Related

Can't add ALAssets to NSOperationQueue from enumeration block

I'm trying to enumerate through the ALAssetLibrary to retrieve all saved photos. From the enumeration block, I'm trying to to send each ALAsset for asynchronous processing by passing it to an NSInvocationOperation object, then adding that to an NSOperationQueue. However, only the first ALAsset object is properly passed to the processing method. All subsequent assets are just passed as nil.
Here is my code:
ViewDidLoad:
queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop)
{
[group setAssetsFilter:[ALAssetsFilter allPhotos]];
[group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop)
{
if (! result) {
return;
}
NSInvocationOperation* operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(processAsset:) object:result];
[queue addOperation:operation];
return;
}];
} failureBlock:^(NSError *error) {
NSLog(#"%#", error);
}];
And the -processAsset method
- (void)processAsset:(ALAsset *)asset
{
NSLog(#"Asset: %#", asset);
// Asset is nil after the first iteration
}
Any help would be really appreciated, thanks!
All ALAssets objects are deallocated when the ALAssetsLibrary that obtained them goes out of scope. If you want to do what you're doing, you'll need to keep a strong reference to library, and then deallocate library when you're done with it.
(In your case, they deallocate at the end of viewDidLoad.)

writeImageToSavedPhotosAlbum and enumerate images in the album

I'm using writeImageToSavedPhotosAlbum:orientation:completionBlock: to save an image and it's ok. Then I want an array of assets in the Album.
The first time I refresh my array it doesn't count the new image, while calling it a second time it works fine.
Why? I understand that the writeImageToSavedPhotosAlbum start another thread and that the Block is called after the save operation completes, isn't it?
This is the code I use to save the image and refresh my array:
[_album refresh];
ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library writeImageToSavedPhotosAlbum:image
metadata:metadataAsMutable
completionBlock:^(NSURL *assetURL, NSError *error) {
if (error) {
NSLog(#"ERROR: the image failed to be written");
}
else {
NSLog(#"PHOTO SAVED - assetURL: %#", assetURL);
}
[_album refresh];
[_album refresh];
[spinner stopAnimating];
[self close];
}];
This is the refresh function:
- (void)refresh {
[_imgAssets removeAllObjects];
if (_albumGroup) {
[_albumGroup enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if(result == nil) {
NSLog(#"done enumerating photos (%d)", [_imgAssets count]);
return;
}
[_imgAssets addObject:result];
//NSLog(#">> enumerating photos (%d)", [_imgAssets count]);
}];
}
}
And this is what I obtain:
2013-06-04 23:19:07.962 PhotInfo[4835:c07] done enumerating photos (29)
2013-06-04 23:19:08.041 PhotInfo[4835:c07] PHOTO SAVED - assetURL: assets-library://asset/asset.JPG?id=CDC2E97A-FF47-4A52-99E2-574037155D92&ext=JPG
2013-06-04 23:19:08.042 PhotInfo[4835:c07] done enumerating photos (29)
2013-06-04 23:19:08.044 PhotInfo[4835:c07] done enumerating photos (30)
Moreover, if I replace the second of the 3 refresh with
[_album performSelectorOnMainThread:#selector(refresh) withObject:nil waitUntilDone:YES];
it may or may not read the added image.
Where is the problem?
Thanks for the help.
I've resolved setting an observer on ALAssetsLibraryChangedNotification.

getting the posterImage of ALAssetsGroupAlbum by name of album

Struggling a bit here with blocks and the ALAssetLibrary routines. I've been through the WWDC + Stanford videos on blocks and read up a bit but it hasn't quite clicked for me yet.
What I am trying to do is get the posterImage for a custom photo library. The first routine below is in my main viewController and calls the function getPosterImageForAlbum: which is in an ALAssetLibrary class extension.
The things I am struggling with/confused about/have tried:
Should I pass in a reference (CGImageRef or UIImage) and have the
ALAssetLibrary method set it or define a return value for the
ALAssetLibrary method and then handle the image in my main class?
I've tried it both ways without success.
The asynchronous nature of the ALAssetLibrary enumeration methods are
kind of hard to work with - so I guess I am doing it wrong.
Defining a block to be passed as a param: do I always need to
typedef it?
I think I've got all the conceptual bits and pieces swirling around but I haven't yet been able to wrangle them into a clear understanding of working with blocks. Any tips or pointers* to good articles would be much appreciated.
//
- (IBAction)getPosterImage:(id)sender {
NSString *groupName = self.groupNameField.text;
NSLog(#"%#", groupName);
__weak typeof(self) weakSelf = self;
CGImageRef tmpImg = [weakSelf.library getPosterImageForAlbum:groupName withCompletionBlock:(CGImageRef)(^GetPosterImageCompletion)(NSError *error){
if (error!=nil) {
NSLog(#"getPosterImage error: %#", [error description]);
} else {
if (tmpImg != nil){
UIImage * posterImg = [UIImage imageWithCGImage:tmpImg];
weakSelf.pImage.image = posterImg;
}
}
}];
}
// This is in an extension of the ALAssetLibrary
typedef CGImageRef(^GetPosterImageCompletion)(NSError* error);
-(CGImageRef)getPosterImageForAlbum:(NSString*)albumName
withCompletionBlock:(GetPosterImageCompletion)completionBlock
{
__block BOOL albumWasFound = NO;
__block CGImageRef thePosterImage = nil;
SaveImageCompletion test;
//search all photo albums in the library
[self enumerateGroupsWithTypes:ALAssetsGroupAlbum
usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
NSLog(#"this group name: %#",
[group valueForProperty:ALAssetsGroupPropertyName]);
//compare the names of the albums
if ([albumName compare:
[group valueForProperty:ALAssetsGroupPropertyName]]==NSOrderedSame) {
printf("matches \n"); //target album is found
albumWasFound = YES;
thePosterImage = group.posterImage;
*stop = true;
return ;
}
} failureBlock: test];
if (albumWasFound==NO) {
NSLog(#"%#", #"No group found");
}
return thePosterImage;
}
I'm editing my original answer in a big way because only after I posted it did I really understand what you are trying to do -- write a class extension on ALAssetLibrary that finds an album's poster image by name, rather than just find an album's poster image by name in any way possible.
I would handle this by writing an extension method that takes, as a parameter, a completion block that accepts a single parameter of type UIImage (or optionally an NSError parameter as well). In that completion block, the caller can do whatever they want with the image that's asynchronously returned. There is no need to typedef the block -- you could write the method this way:
- (void) getPosterImageForAlbumNamed: (NSString *) albumName completionBlock: (void (^)(UIImage *, NSError *)) completionBlock
{
__block ALAssetsGroup * foundAlbum = nil;
[self enumerateGroupsWithTypes: ALAssetsGroupAlbum
usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if (group) {
if ([(NSString *) [group valueForProperty: ALAssetsGroupPropertyName] compare: albumName] == NSOrderedSame) {
*stop = YES;
foundAlbum = group;
completionBlock([UIImage imageWithCGImage: [group posterImage]], nil);
}
} else{
if (! foundAlbum) {
NSLog(#"Album wasn't found");
// completionBlock(nil, XX SOME ERROR XX);
}
}
} failureBlock:^(NSError *error) {
NSLog(#"Couldn't access photo library");
// completionBlock(nil, XX SOME ERROR XX);
}];
}
Then, you could call the method this way:
-(IBAction) getPosterForAlbum: (id) sender
{
NSString * albumName = self.textField.text;
ALAssetsLibrary * library = [[ALAssetsLibrary alloc] init];
__weak typeof(self) weakSelf = self;
[library getPosterImageForAlbumNamed: albumName completionBlock:^(UIImage * image, NSError * error) {
if (! error) {
[weakSelf doSomethingWithPosterImage: image];
}
}];
}
Is that along the lines of what you are trying to do? (Sorry for the major edit...)

How to use ALAssetLibrary to get Local Photos in Camera Roll

ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos
usingBlock:libraryGroupsEnumeration
failureBlock:failureblock];
ALAssetsGroupEnumerationResultsBlock groupEnumerAtion = ^(ALAsset *result, NSUInteger index, BOOL *stop){
if (result!=NULL) {
if ([[result valueForProperty:ALAssetPropertyType] isEqualToString:ALAssetTypePhoto]) {
[self._dataArray addObject:result];
}
}
};
ALAssetsLibraryGroupsEnumerationResultsBlock
libraryGroupsEnumeration = ^(ALAssetsGroup* group, BOOL* stop){
//within the group enumeration block.filter to enumerate just photos.
[group setAssetsFilter:[ALAssetsFilter allPhotos]];
if (group!=nil) {
NSString *g=[NSString stringWithFormat:#"%#",group];
NSLog(#"gg:%#",g);//gg:ALAssetsGroup - Name:Camera Roll, Type:Saved Photos, Assets count:71
[group enumerateAssetsUsingBlock:groupEnumerAtion];
}
else {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saveToDB:self._dataArray];
});
}
};
Assume that my Camera Roll have 100 photos,and I want get first 30 to save to my database.but in above code ,I must wait for 100 results fisishing.After 30 write to database , continue to get another 30 until end.
because get 100 or even more photos will delay my UI refresh.It looks not comfortable.
Thanks a lot!
what should I write.?
Try this one
if (result!=NULL) {
if ([[result valueForProperty:ALAssetPropertyType] isEqualToString:ALAssetTypePhoto]) {
[self._dataArray addObject:result];
if([self._dataArray count] == 30){
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSArray *array = [[NSArray alloc] initWithArray:self._dataArray]; //please change the array declaration to top of this method. Because the Block will not allow to do it here.
[self._dataArray removeAllObjects];
[self saveToDB:array];
//array release,if not using ARC
});
}
}
}
and
if (group!=nil) {
[group enumerateAssetsUsingBlock:groupEnumerAtion];
}
else if([self._dataArray count] > 0) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saveToDB:self._dataArray];
});
}
You should try saving to the database on a background thread, and let CoreData (assuming you use CoreData) update the NSManagedObjectContext on the main thread by handling NSManagedObjectContextDidSaveNotification.
You could still save after 30 photos, that might relieve some of the I/O stress, but you'd have to test performance.

The execution of block is always delayed

I'm pretty new to IOS. I want to get all pictures on a device in viewDidLoad. but the block is always executed after I called NSLog(#"%d", photos.count) in the code as follows. How to handle such a case?
__block NSMutableArray* photos = [[[NSMutableArray alloc] init] retain];
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
void (^assertsEnumerator)(ALAsset *, NSUInteger , BOOL *) = ^(ALAsset *result, NSUInteger index, BOOL *stop)
{
if(result)
{
NSLog(#"Assert name : %#", [result valueForProperty:ALAssetPropertyURLs]);
[photos addObject:[UIImage imageWithCGImage:[result aspectRatioThumbnail]]];
}
};
void (^groupEnumerator)(ALAssetsGroup*, BOOL*) = ^(ALAssetsGroup *group, BOOL *stop) {
if(group != nil) {
NSLog(#"Adding group %#", [group valueForProperty:ALAssetsGroupPropertyName]);
[group enumerateAssetsUsingBlock: assertsEnumerator];
}
};
[library enumerateGroupsWithTypes: ALAssetsGroupSavedPhotos
usingBlock:groupEnumerator
failureBlock:^(NSError * err) {NSLog(#"Erorr: %#", [err localizedDescription]);}];
[library release];
NSLog(#"%d", photos.count);
[photos release];
As the ALAssetsLibrary documentation states, enumerateGroupsWithTypes:usingBlock:failureBlock: is asynchronous, so instead of being called in place, it's scheduled to be called from within the next run loop cycle. The documentation states clearly what's the reason for that and how you should proceed:
This method is asynchronous. When groups are enumerated, the user may be asked to confirm the application's access to the data; the method, though, returns immediately. You should perform whatever work you want with the assets in enumerationBlock.

Resources