12

When using Assets Library you could fetch the album's poster image from ALAssetsGroup. How do you achieve the same when using Photos Framework (Photo kit)?

Adam Tegen
  • 25,378
  • 33
  • 125
  • 153
bhoomi
  • 373
  • 1
  • 4
  • 11

3 Answers3

16

I do it this way. In the method cellForRowAtIndexPath: in albums tableView add the following code.

Objective C:

    PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
    fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
    PHFetchResult *fetchResult = [PHAsset fetchKeyAssetsInAssetCollection:[self.assetCollections objectAtIndex:indexPath.row] options:fetchOptions];
        PHAsset *asset = [fetchResult firstObject];
        PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
        options.resizeMode = PHImageRequestOptionsResizeModeExact;

        CGFloat scale = [UIScreen mainScreen].scale;
        CGFloat dimension = 78.0f;
        CGSize size = CGSizeMake(dimension*scale, dimension*scale);

        [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeAspectFill options:options resultHandler:^(UIImage *result, NSDictionary *info) {
        dispatch_async(dispatch_get_main_queue(), ^{
            cell.imageView.image = result;
        });

        }];

Swift 3:

let fetchOptions = PHFetchOptions()
let descriptor = NSSortDescriptor(key: "creationDate", ascending: true)
fetchOptions.sortDescriptors = [descriptor]

let fetchResult = PHAsset.fetchKeyAssets(in: assets[indexPath.row], options: fetchOptions)

guard let asset = fetchResult?.firstObject else {
    return
}

let options = PHImageRequestOptions()
options.resizeMode = .exact

let scale = UIScreen.main.scale
let dimension = CGFloat(78.0)
let size = CGSize(width: dimension * scale, height: dimension * scale)


PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: options) { (image, info) in
    DispatchQueue.main.async {
        cell.imageView.image = image
    }
}

Edit: Looks like fetchKeyAssetsInAssetCollection does not always return correct results (most recently captured images/videos). The definition of keyAssets is vaguely defined by apple. Better use

+ (PHFetchResult *)fetchAssetsInAssetCollection:(PHAssetCollection *)assetCollection options:(PHFetchOptions *)options

to get the fetch results array and then get the firstObject from the fetch results as described before. This will certainly return the correct results. :)

jarora
  • 5,384
  • 2
  • 34
  • 46
  • Can you be more specific than "doesn't always return correct results"? – Bill Feb 22 '17 at 19:07
  • The `cell.imageView.image = image` needs to happen in the main thread. You can't modify UI elements from a non-main thread. – Bill Feb 22 '17 at 22:04
  • Thank you for pointing out the main thread issue. Editing the code. Also, regarding your question on "correct results", basically the fetchKeyAssetsInAssetCollection does not return the top most image in the photos album. The API documentation is very vague. The other API fetchAssetsInAssetCollection is a sure-shot way to get the top most image from the album. Hope this clarifies any doubts. – jarora Feb 23 '17 at 05:51
  • I can't get this to work. Because the cell is returned from `cellForRowAtIndexPath:` before the `requestImageForAsset` completion block returns with the image, the cell's imageView frame is {0,0,0,0}. This means you can't see the image. If you manually set the frame of the imageView in the completion block, it doesn't move the label out of the way. I'm currently working on retrieving all the thumbnails before reloading the tableview. I'm not impressed with PHPhotos at all - ALAssets were much easier to work with IMO. – mashers Apr 13 '17 at 10:35
  • @mashers Why don't have a dummy image that you replace with the original if and when you retrieve it. That ways you will have the imageView of the correct size (i believe your imageView is of fixed size). Photos framework has its pains. I have raised a few radars for missing functionalities. – jarora Apr 13 '17 at 12:11
  • A dummy image is a good idea. Didn't think of that. I did get it working another way though, by looping through and getting all the thumbnails before triggering the tableView to reloadData. Your way sounds cleaner though. – mashers Apr 13 '17 at 15:14
5

Here's what I'm using now. It handles different types of photo collections and the case when key asset is missing.

static func fetchThumbnail(collection: PHCollection, targetSize: CGSize, completion: @escaping (UIImage?) -> ()) {

    func fetchAsset(asset: PHAsset, targetSize: CGSize, completion: @escaping (UIImage?) -> ()) {
        let options = PHImageRequestOptions()
        options.deliveryMode = PHImageRequestOptionsDeliveryMode.highQualityFormat
        options.isSynchronous = false
        options.isNetworkAccessAllowed = true

        // We could use PHCachingImageManager for better performance here
        PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .default, options: options, resultHandler: { (image, info) in
            completion(image)
        })
    }

    func fetchFirstImageThumbnail(collection: PHAssetCollection, targetSize: CGSize, completion: @escaping (UIImage?) -> ()) {
        // We could sort by creation date here if we want
        let assets = PHAsset.fetchAssets(in: collection, options: PHFetchOptions())
        if let asset = assets.firstObject {
            fetchAsset(asset: asset, targetSize: targetSize, completion: completion)
        } else {
            completion(nil)
        }
    }

    if let collection = collection as? PHAssetCollection {
        let assets = PHAsset.fetchKeyAssets(in: collection, options: PHFetchOptions())

        if let keyAsset = assets?.firstObject {
            fetchAsset(asset: keyAsset, targetSize: targetSize) { (image) in
                if let image = image {
                    completion(image)
                } else {
                    fetchFirstImageThumbnail(collection: collection, targetSize: targetSize, completion: completion)
                }
            }
        } else {
            fetchFirstImageThumbnail(collection: collection, targetSize: targetSize, completion: completion)
        }
    } else if let collection = collection as? PHCollectionList {
        // For folders we get the first available thumbnail from sub-folders/albums
        // possible improvement - make a "tile" thumbnail with 4 images
        let inner = PHCollection.fetchCollections(in: collection, options: PHFetchOptions())
        inner.enumerateObjects { (innerCollection, idx, stop) in
            self.fetchThumbnail(collection: innerCollection, targetSize: targetSize, completion: { (image) in
                if image != nil {
                    completion(image)
                    stop.pointee = true
                } else if idx >= inner.count - 1 {
                    completion(nil)
                }
            })
        }
    } else {
        // We shouldn't get here
        completion(nil)
    }
}
algrid
  • 5,600
  • 3
  • 34
  • 37
1

It is a very simple thing...

    PHFetchOptions *userAlbumsOptions = [PHFetchOptions new];
    userAlbumsOptions.predicate = [NSPredicate predicateWithFormat:@"estimatedAssetCount > 0"];

    PHFetchResult *userAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny options:userAlbumsOptions];

    [userAlbums enumerateObjectsUsingBlock:^(PHAssetCollection *collection, NSUInteger idx, BOOL *stop) {
    NSLog(@"album title %@", collection.localizedTitle);
    PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:collection options:nil];
    PHAsset *asset = [assetsFetchResult objectAtIndex:0];
    NSInteger retinaMultiplier = [UIScreen mainScreen].scale;
    CGSize retinaSquare = CGSizeMake(80 * retinaMultiplier, 80 * retinaMultiplier);

    [[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:asset.localIdentifier] options:SDWebImageProgressiveDownload targetLocalAssetSize:retinaSquare progress:^(NSInteger receivedSize, NSInteger expectedSize) {

    } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        if (image) {
            albumCoverImg.image = image;
        }
    }];
}];

and if you have not updated SDWebImage classed then load image as a normal way.

shmosel
  • 49,289
  • 6
  • 73
  • 138
Pratik Patel
  • 1,393
  • 12
  • 18
  • 1
    Why does this answer bring in a third-party dependency (SDWebImage)? – Bill Feb 22 '17 at 19:01
  • 3
    I'm not sure this is actually correct. It should use `fetchKeyAssets` not just fetch the first object (the key asset isn't necessarily just the first asset returned). – Bill Feb 22 '17 at 19:06