Search code examples
iosswiftphotosframework

Video picker using Photos framework


I'm trying to pull all the videos in a user's library using the Photos framework and display them in a collection view. Basically implementing my own video picker.

I'm using PHCachingImageManager to start caching image thumbnails for each video. When 'cellForItemAt indexPath' gets called, I get 'unexpectedly found nil when unwrapping an Optional value' when I call PHCachingImageManager.requestImage(...) and trying to set my cell's image in the closure.

viewDidLoad() {
    // Register Cell classes
    self.collectionView!.register(PhotosCell.self, forCellWithReuseIdentifier: reuseIdentifier)

    let videosAlbum = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumVideos, options: nil)

    print(videosAlbum.count) // prints as 1 bc videosAlbum contains 1 collection
    self.videos = PHAsset.fetchAssets(in: videosAlbum.object(at: 0), options: nil)
    print(self.videos.count) // prints the # of videos in the Videos smart album

    self.imageManager.startCachingImages(for: self.videos.objects(at: [0, videos.count-1]), targetSize: CGSize(width: 150, height: 150), contentMode: .aspectFit, options: nil)
}

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! PhotosCell
    let asset = videos.object(at: indexPath.item)

    imageManager.requestImage(for: asset, targetSize: CGSize(width: 150, height: 150), contentMode: .aspectFill, options: nil, resultHandler: {
        image, _ in
                cell.thumbnailImage = image //this is the line throwing the error
    })
    return cell
}

Could this be a problem with the way I'm implementing my PhotosCell class? Or a problem with how I'm using 'collectionView.dequeueReusableCell(...)?

class PhotosCell: UICollectionViewCell {

@IBOutlet weak var photoThumbnail: UIImageView!

var thumbnailImage: UIImage! {
    didSet {
        photoThumbnail.image = thumbnailImage
    }
}
}

I looked around and thought I saw where closures passed to the cache manager are put on a background thread, but that shouldn't affect the optional UIImageView.image being set on the line with the error, right? The GitHub code is here for more context.


Solution

  • Since your self-answer fixed the problem without you being sure why, here's a fuller explanation...

    When you set a prototype cell in the storyboard, it automatically registers that cell definition with the collection view, so you don't need to call registerCellClass or registerNib.

    In fact, if you do call registerCellClass, dequeueReusableCell will try to create new cell instances using your cell class' default initializer (init(frame:) most likely). That means it won't load your cell class from the storyboard, so any IBOutlet connections you set up there won't be connected — so your cell's photoThumbnail outlet is nil, causing the trap you found.

    So your self-answer is right — just don't call registerCellClass and let the storyboard handle it for you. (Behind the scenes, Xcode compiles your storyboard into multiple nibs, and the nib loading code calls registerNib:forCellWithReuseIdentifier: with the one you set as the prototype cell.)


    While we're here, let's fix another possible problem in your code. Your requestImage completion handler doesn't execute until after collectionView(_:cellForItemAt:) returns. This means that during or after scrolling, the cell you're setting an image on might have been reused — so it now represents a different indexPath than the one you requested an image for, and you're now setting the wrong image on it.

    If you look in Apple's Using Photos Framework sample code, they do some extra work to make sure that the asynchronous setting of images on cells works out right. In their AssetGridViewController:

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let asset = fetchResult.object(at: indexPath.item)
    
        // Dequeue a GridViewCell.
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GridViewCell.self), for: indexPath) as? GridViewCell
            else { fatalError("unexpected cell in collection view") }
    
        // Request an image for the asset from the PHCachingImageManager.
        cell.representedAssetIdentifier = asset.localIdentifier
        imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: { image, _ in
            // The cell may have been recycled by the time this handler gets called;
            // set the cell's thumbnail image only if it's still showing the same asset.
            if cell.representedAssetIdentifier == asset.localIdentifier {
                cell.thumbnailImage = image
            }
        })
        return cell
    }