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 3 of 4 of this article. Click here to view the first page.

Photo View Controller Change Observer

PhotoKit caches the results of fetch requests for better performance. When you tap the favorite button, the asset updates in the library, but the view controller’s copy of the asset is now out of date. The controller needs to listen for updates to the library and update its asset when necessary. Do this by conforming the controller to PHPhotoLibraryChangeObserver.

At the end of the file, after the last curly brace, add:

// 1
extension PhotoViewController: PHPhotoLibraryChangeObserver {
  func photoLibraryDidChange(_ changeInstance: PHChange) {
    // 2
    guard
      let change = changeInstance.changeDetails(for: asset),
      let updatedAsset = change.objectAfterChanges
      else { return }
    // 3
    DispatchQueue.main.sync {
      // 4
      asset = updatedAsset
      imageView.fetchImageAsset(
        asset, 
        targetSize: view.bounds.size
      ) { [weak self] _ in
        guard let self = self else { return }
        // 5
        self.updateFavoriteButton()
        self.updateUndoButton()
      }
    }
  }
}
  1. The change observer has only one method: photoLibraryDidChange(:). Every time the library changes, it calls this method.
  2. You need to check if the update affects your asset. Use changeInstance, a property that describes the library changes, by calling its changeDetails(for:) and pass in your asset. It returns nil if your asset is not affected by the changes. Otherwise, you retrieve the updated version of the asset by calling objectAfterChanges.
  3. Because this method runs in the background, dispatch the rest of the logic on the main thread because it updates the UI.
  4. Update the controller’s asset property with the updated asset and fetch the new image.
  5. Refresh the UI.

Registering the Photo View Controller

Still in PhotoViewController.swift, find viewDidLoad() and add this as the last line:

PHPhotoLibrary.shared().register(self)

The view controller must register to receive updates. After viewDidLoad(), add:

deinit {
  PHPhotoLibrary.shared().unregisterChangeObserver(self)
}

The view controller must also unregister when done.

Build and run. Navigate to one of your favorite photos. Tap the heart button, and the heart fills. Tap again, and it reverts back.

Modifying an asset's favorite metadata.

But there is a new problem. Tap the favorite button again to fill the heart. Navigate back to the All Photos view and then select the same photo again. The heart is no longer filled, and if you try to select it nothing happens. Something is very wrong.

Photos View Controller Change Observer

PhotosCollectionViewController also does not conform to PHPhotoLibraryChangeObserver. Because of this, its asset is also out of date. The fix is pretty simple: You need to make it conform to PHPhotoLibraryChangeObserver.

Open PhotosCollectionViewController.swift and scroll to the end of the file. Add the following code:

extension PhotosCollectionViewController: PHPhotoLibraryChangeObserver {
  func photoLibraryDidChange(_ changeInstance: PHChange) {
    // 1
    guard let change = changeInstance.changeDetails(for: assets) else {
      return
    }
    DispatchQueue.main.sync {
      // 2
      assets = change.fetchResultAfterChanges
      collectionView.reloadData()
    }
  }
}

This code is similar to what you did in PhotoViewController, with a few small differences:

  1. Because this view displays several assets, request change details for them all.
  2. Replace assets with the updated fetch results and reload the collection view.

Registering the Photos View Controller

Scroll to viewDidLoad() and add this after super.viewDidLoad():

PHPhotoLibrary.shared().register(self)

Like last time, the view registers to receive library updates. After viewDidLoad() add:

deinit {
  PHPhotoLibrary.shared().unregisterChangeObserver(self)
}

The view also needs to unregister.

Album View Controller Change Observer

While you are at it, you should add similar code to AlbumCollectionViewController.swift. If you don’t, you end up with a similar issue when navigating all the way back. Open AlbumCollectionViewController.swift and add the following to the end of the file:

extension AlbumCollectionViewController: PHPhotoLibraryChangeObserver {
  func photoLibraryDidChange(_ changeInstance: PHChange) {
    DispatchQueue.main.sync {
      // 1
      if let changeDetails = changeInstance.changeDetails(for: allPhotos) {
        allPhotos = changeDetails.fetchResultAfterChanges
      }
      // 2
      if let changeDetails = changeInstance.changeDetails(for: smartAlbums) {
        smartAlbums = changeDetails.fetchResultAfterChanges
      }
      if let changeDetails = changeInstance.changeDetails(for: userCollections) {
        userCollections = changeDetails.fetchResultAfterChanges
      }
      // 4
      collectionView.reloadData()
    }
  }
}

This code is a bit different, because you are checking if the change affects multiple fetch results.

  1. If there was a change to any of the assets in allPhotos, update the property with the new changes.
  2. If the change affects smart albums or user collections, update those as well.
  3. Finally, reload the collection view.

Album View Controller Registration

Add code to register for library updates to the end of viewDidLoad() in AlbumCollectionViewController.swift:

PHPhotoLibrary.shared().register(self)

After viewDidLoad() add:

deinit {
  PHPhotoLibrary.shared().unregisterChangeObserver(self)
}

Again, this view also needs to unregister.

Build and run. Tap All Photos and tap a photo. Mark it as a favorite, then navigate all the way back to the main view. Again, tap All Photos and tap the same photo. You can see that it’s still marked as a favorite. Navigate back to the album collection view. Notice that the Favorites album count is up to date and the cover image is set for Favorites.

The updated favorite album.

Great work! You are now persisting metadata changes to assets and showing those changes in each view controller.

Editing a Photo

Open PhotoViewController.swift and add the following after declaring the asset property:

private var editingOutput: PHContentEditingOutput?

PHContentEditingOutput is a container that stores edits to an asset. You’ll see how this works in a moment. Find applyFilter() and add this code to it:

// 1
asset.requestContentEditingInput(with: nil) { [weak self] input, _ in
  guard let self = self else { return }

  // 2
  guard let bundleID = Bundle.main.bundleIdentifier else {
    fatalError("Error: unable to get bundle identifier")
  }
  guard let input = input else {
    fatalError("Error: cannot get editing input")
  }
  guard let filterData = Filter.noir.data else {
    fatalError("Error: cannot get filter data")
  }
  // 3
  let adjustmentData = PHAdjustmentData(
    formatIdentifier: bundleID,
    formatVersion: "1.0",
    data: filterData)
  // 4
  self.editingOutput = PHContentEditingOutput(contentEditingInput: input)
  guard let editingOutput = self.editingOutput else { return }
  editingOutput.adjustmentData = adjustmentData
  // 5
  let fitleredImage = self.imageView.image?.applyFilter(.noir)
  self.imageView.image = fitleredImage
  // 6
  let jpegData = fitleredImage?.jpegData(compressionQuality: 1.0)
  do {
    try jpegData?.write(to: editingOutput.renderedContentURL)
  } catch {
    print(error.localizedDescription)
  }
  // 7
  DispatchQueue.main.async {
    self.saveButton.isEnabled = true
  }
}
  1. Edits are done within containers. The input container gives you access to the image. The editing logic takes place inside the completion handler.
  2. You need the bundle identifier, the completion handler’s input container and the filter data to continue.
  3. Adjustment data is a way of describing changes to the asset. To create this data, use a unique identifier to identify your change. The bundle ID is a great choice. Also supply a version number and the data used to modify the image.
  4. You also need an output container for the final modified image. To create this, you pass in the input container. Assign the new output container to the editingOutput property you created above.
  5. Apply the filter to the image. Describing how this is done is out of scope for this article, but you’ll find the code in UIImage+Extensions.swift.
  6. Create JPEG data for the image and write it to the output container.
  7. Finally, enable the save button.

Build and run. Select a photo. Tap Apply Filter. Your photo should now have a nice noir filter added.

Modifying an asset by applying a filter to the image.

Tap the save button. Nothing happens. You’ll fix that next.