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...)
Related
This code worked fine in iOS 7 but in iOS 8.1 all assets located in the "My Photo Stream" album are nil from within the result block. (The failureBlock is not called.) Regular albums and shared albums work just fine.
I tried the accepted answer from: Error trying to assigning __block ALAsset from inside assetForURL:resultBlock:
That is, I'm holding a reference to an ALAssetsLibrary object, listening for the ALAssetsLibraryChangedNotification event (which doesn't happen btw, but oh well.) I made sure my app has permission to access photos, I'm on wi-fi, I see the photos' thumbnails just fine in my tableView. It's just when I try to load them with assetForURL: they're always nil.
// example URL: assets-library://asset/asset.JPG?id=1ECB69B9-DC7A-45A7-B135-F43317D3412C&ext=JPG
[self.library assetForURL:[NSURL URLWithString:url] resultBlock:^(ALAsset *asset) {
NSLog(#"Asset: %#", asset); // nil :(
} failureBlock:^(NSError *error) {
NSLog(#"Failure, wahhh!");
}];
Is anyone else seeing this issue?
I had the same problem. Switching to Photos framework is not an option for me at this moment, but fortunately I have found a workaround. You may find it a big ugly and I suspect it may work slow when Photo Stream contains a lot of photos, but it is better than nothing.
The idea is to enumerate all items in the Photo Stream asset group and compare the necessary URL with the URL of each item. Fortunately, it still works.
I have a method like this (library is ALAssetsLibrary property of the same class, you may need to initialise it inside this code):
- (void)loadItem:(NSURL *)url withSuccessBlock:(void (^)(void))successBlock andFailureBlock:(void (^)(void))failureBlock {
[library assetForURL:url
resultBlock:^(ALAsset *asset)
{
if (asset){
//////////////////////////////////////////////////////
// SUCCESS POINT #1 - asset is what we are looking for
//////////////////////////////////////////////////////
successBlock();
}
else {
// On iOS 8.1 [library assetForUrl] Photo Streams always returns nil. Try to obtain it in an alternative way
[library enumerateGroupsWithTypes:ALAssetsGroupPhotoStream
usingBlock:^(ALAssetsGroup *group, BOOL *stop)
{
[group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if([result.defaultRepresentation.url isEqual:url])
{
///////////////////////////////////////////////////////
// SUCCESS POINT #2 - result is what we are looking for
///////////////////////////////////////////////////////
successBlock();
*stop = YES;
}
}];
}
failureBlock:^(NSError *error)
{
NSLog(#"Error: Cannot load asset from photo stream - %#", [error localizedDescription]);
failureBlock();
}];
}
}
failureBlock:^(NSError *error)
{
NSLog(#"Error: Cannot load asset - %#", [error localizedDescription]);
failureBlock();
}
];
}
Hope this helps.
I've made the observation that trying to retrieve an asset using assetForURL inside a writeImage toSavedPhotosAlbum success block for that same asset will yield an asset of nil (most of the time).
However, retrieving the asset with assetForURL some time after the writeImage success block has completed execution does yield the correct asset.
Waiting for 1 second did work, while waiting for only 300 ms did not. But this of course will be different for each and every device and situation.
This does not really answer the question in a satisfying way, but maybe it helps someone else figuring out the underlying problem.
From iOS 8.0 and later, Apple suggests to use Photos framework instead of the Assets Library framework.
Tested with iPad mini on iOS 8.1, this is how you should do it with the new Photos Framework:
NSURL *url = /* your asset url from the old ALAsset library prior to iOS 8 */
PHFetchResult<PHAsset *> *assets = [PHAsset fetchAssetsWithALAssetURLs:#[url]
options:nil];
assert(assets.count == 1);
PHAsset *asset = assets.firstObject;
[[PHImageManager defaultManager] requestImageForAsset:asset
targetSize:CGSizeMake(800, 800) // TODO: your target size
contentMode:PHImageContentModeDefault
options:nil
resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info)
{
// Do whatever you want to the result
}];
After not finding any answers for this anywhere, I created the following extension to PHAsset which works great as of iOS 8.2 although I assume it's theoretically slow. Even though one of the prior comments says that this is fixed on iOS8.2 beta, the bug was still present for me now that iOS8.2 is released.
import Photos
import UIKit
extension PHAsset {
class func fetchAssetWithALAssetURL (alURL: NSURL) -> PHAsset? {
let phPhotoLibrary = PHPhotoLibrary.sharedPhotoLibrary()
let assetManager = PHImageManager()
var phAsset : PHAsset?
let optionsForFetch = PHFetchOptions()
optionsForFetch.includeHiddenAssets = true
var fetchResult = PHAsset.fetchAssetsWithALAssetURLs([alURL], options: optionsForFetch)
if fetchResult?.count > 0 {
return fetchResult[0] as? PHAsset
} else {
var str = alURL.absoluteString!
let startOfString = advance(find(str, "=")!, 1)
let endOfString = advance(startOfString, 36)
let range = Range<String.Index>(start:startOfString, end:endOfString)
let localIDFragment = str.substringWithRange(range)
let fetchResultForPhotostream = PHAssetCollection.fetchAssetCollectionsWithType(PHAssetCollectionType.Album, subtype: PHAssetCollectionSubtype.AlbumMyPhotoStream, options: nil)
if fetchResultForPhotostream?.count > 0 {
let photostream = fetchResultForPhotostream![0] as PHAssetCollection
let fetchResultForPhotostreamAssets = PHAsset.fetchAssetsInAssetCollection(photostream, options: optionsForFetch)
if fetchResultForPhotostreamAssets?.count >= 0 {
var stop : Bool = false
for var i = 0; i < fetchResultForPhotostreamAssets.count && !stop; i++ {
let phAssetBeingCompared = fetchResultForPhotostreamAssets[i] as PHAsset
if phAssetBeingCompared.localIdentifier.rangeOfString(localIDFragment, options: nil, range: nil, locale: nil) != nil {
phAsset = phAssetBeingCompared
stop = true
}
}
return phAsset
}
}
return nil
}
}
}
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.
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.
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.
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.