Getting Started with PhotoKit

In this tutorial, you’ll learn how to use PhotoKit to access and modify photos, smart albums and user collections. You’ll also learn how to save and revert edits made to photos. By Corey Davis.

4.5 (11) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Prepping the Collection View

You now have assets, so it’s time to do something with them. Add the following to the end of the class:

override func collectionView(
  _ collectionView: UICollectionView,
  numberOfItemsInSection section: Int
) -> Int {
  switch sections[section] {
  case .all: return 1
  case .smartAlbums: return smartAlbums.count
  case .userCollections: return userCollections.count
  }
}

Here you return the number of items in each section so the collection view knows how many items to display in each section. Except for the “all photos” section, this is a good example of how you treat PHFetchResult as an array.

Updating the Cell

Next, replace the code in collectionView(_:cellForItemAt:) with the following:

// 1
guard let cell = collectionView.dequeueReusableCell(
  withReuseIdentifier: AlbumCollectionViewCell.reuseIdentifier,
  for: indexPath) as? AlbumCollectionViewCell
  else {
    fatalError("Unable to dequeue AlbumCollectionViewCell")
}
// 2
var coverAsset: PHAsset?
let sectionType = sections[indexPath.section]
switch sectionType {
// 3
case .all:
  coverAsset = allPhotos.firstObject
  cell.update(title: sectionType.description, count: allPhotos.count)
// 4
case .smartAlbums, .userCollections:
  let collection = sectionType == .smartAlbums ? 
    smartAlbums[indexPath.item] : 
    userCollections[indexPath.item]
  let fetchedAssets = PHAsset.fetchAssets(in: collection, options: nil)
  coverAsset = fetchedAssets.firstObject
  cell.update(title: collection.localizedTitle, count: fetchedAssets.count)
}
// 5
guard let asset = coverAsset else { return cell }
cell.photoView.fetchImageAsset(asset, targetSize: cell.bounds.size) { success in
  cell.photoView.isHidden = !success
  cell.emptyView.isHidden = success
}
return cell
  1. First, dequeue an AlbumCollectionViewCell.
  2. Create variables to hold an asset, which is used as the album cover image, and the section type. Then, process the cell based on its section type.
  3. For the “all photos” section, set the cover image to allPhotos‘s first asset. Update the cell with the section name and count.
  4. Because smartAlbums and userCollections are both collection types, handle them similarly. First, get the collection for this cell and section type from the fetch result. After that, get the collection’s assets using PHAsset‘s ability to fetch assets from a collection. Get the collection’s first asset and use it as the cover asset. Finally, update the cell with the album title and asset count.
  5. If you don’t have a cover asset, return the cell as it is. Otherwise, fetch the image from the asset. In the fetch completion block, use the returned success state to set the hidden property on both the cell’s photo view and default empty view. Finally, return the cell.

Build and run. You now see an entry for All Photos, each smart album in the library and each user collection. Scroll to the bottom to see your My Cool Pics album.

The album view of NoirIt.

Not too bad, but what happened to the cover images? You’ll fix this next.

Fetching Images from Assets

That default album image is kind of boring. It would be nice to see an image from the album.

In the previous step, you called fetchImageAsset(_:targetSize:contentMode:options:completionHandler:) to get the asset’s image. This is a custom method added to UIImage in an extension. Right now, it doesn’t have any code to fetch images and always returns false. To fix this, you’ll use PHImageManager. The image manager handles fetching images from assets and caching the results for quick retrieval later.

Open UIImageView+Extension.swift and replace the code in fetchImageAsset(_:targetSize:contentMode:options:completionHandler:) with:

// 1
guard let asset = asset else {
  completionHandler?(false)
  return
}
// 2
let resultHandler: (UIImage?, [AnyHashable: Any]?) -> Void = { image, info in
  self.image = image
  completionHandler?(true)
}
// 3
PHImageManager.default().requestImage(
  for: asset,
  targetSize: size,
  contentMode: contentMode,
  options: options,
  resultHandler: resultHandler)
  1. If asset is nil, then return false and you’re done. Otherwise, continue.
  2. Next, create the result handler that the image manager will call when the image request is complete. Assign the returned image to UIImageView‘s image property. Call the completion handler with a value of true, indicating that the request is complete.
  3. Finally, request the image from the image manager. Provide the asset, size, content mode, options and result handler. All of these, except for resultHandler, are provided by the calling code. size is the size at which you would like the image returned. contentMode is how you would like the image to fit within the aspect ratio of the size. The default value is aspectFill.

Build and run. Your albums now have cover images!

The album view with cover images.

If you select any of the albums, the next view is empty. Your next task awaits.

Displaying Album Assets

Displaying all assets for an album is simply a matter of requesting each image from PHImageManager. The PhotosCollectionViewController is already set up to do this using the fetch image asset extension that you just worked on. To get this working, you only need to set up the segue to pass the fetch result.

In AlbumCollectionViewController.swift, find makePhotosCollectionViewController(_:) and replace its code with:

// 1
guard
  let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first
  else { return nil }

// 2
let sectionType = sections[selectedIndexPath.section]
let item = selectedIndexPath.item

// 3
let assets: PHFetchResult<PHAsset>
let title: String

switch sectionType {
// 4
case .all:
  assets = allPhotos
  title = AlbumCollectionSectionType.all.description
// 5
case .smartAlbums, .userCollections:
  let album =
    sectionType == .smartAlbums ? smartAlbums[item] : userCollections[item]
  assets = PHAsset.fetchAssets(in: album, options: nil)
  title = album.localizedTitle ?? ""
}

// 6
return PhotosCollectionViewController(assets: assets, title: title, coder: coder)

Now:

  1. Get the selected index path.
  2. Get the section type and item for the selected item.
  3. PhotosCollectionViewController needs a list of assets and a title.
  4. If the user selects the “all photos” section, use allPhotos as the assets assets and set the title.
  5. If the user selects an album or user collection, then use the section and item to get the selected album. Fetch all assets from the album.
  6. Create the view controller.

Build and run. Tap All Photos in the album view. You now see a collection of all your photos.

The photo collection view.

Tap one of the photos.

The photo detail view.

Things are coming into focus!

Modifying Asset Metadata

Change Requests

The ability to modify assets is a key component of NoirIt. Take a first pass at asset modification by allowing users to mark a photo as a favorite. PHAssetChangeRequest facilitates the creation, modification and deletion of assets.

Open PhotoViewController.swift and add this code in toggleFavorite():

// 1
let changeHandler: () -> Void = {
  let request = PHAssetChangeRequest(for: self.asset)
  request.isFavorite = !self.asset.isFavorite
}
// 2
PHPhotoLibrary.shared().performChanges(changeHandler, completionHandler: nil)
  1. You create a code block to encapsulate the change. First, create a change request for the asset. Next, set the request’s isFavorite property to the opposite of the current value.
  2. Instruct the photo library to perform the change by passing in the change request block. You do not need the completion handler here.

Next, replace the code in updateFavoriteButton() with the following:

if asset.isFavorite {
  favoriteButton.image = UIImage(systemName: "heart.fill")
} else {
  favoriteButton.image = UIImage(systemName: "heart")
}

Check the PHAsset‘s favorite status by using the isFavorite property and set the button image to either an empty heart or a filled heart.

Build and run. Navigate through the app and select your favorite photo. Tap the favorite button and … nothing happens. So what went wrong?

Magnifying glass