Android Test-Driven Development by Tutorials,
Second Edition – Now Updated!

Build testable, sustainable Android apps via JUnit, Mockito, and Espresso
by diving into test-driven development (TDD) in this newly-updated book.

Home iOS & Swift Tutorials

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.

5/5 6 Ratings


  • Swift 5, iOS 13, Xcode 11

The Photos app manages image assets in iOS through a set of APIs known as PhotoKit. If you’ve been wondering how to build an app like Photos or just access the photo library, PhotoKit is the answer. This tutorial will focus on iOS, but PhotoKit is also available for macOS, Catalyst and tvOS.

You’ll work on NoirIt, an app that applies a gorgeous noir filter to your photos. To accomplish this, you’ll:

  • Learn about PhotoKit’s permission model.
  • Access image asset data.
  • Learn how to access user collection and smart album data.
  • Display image assets.
  • Modify asset metadata.
  • Edit an asset’s image.
  • Save a modified image asset.
  • Revert a modified image asset to the original image.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Start by opening NoirIt.xcodeproj inside the starter folder. Expand the Resources folder and open Main.storyboard.

The NoirIt storyboard.

The layout of the app is pretty straightforward. There is an album collection view controller, a photos collection view controller and a photo detail view controller.

Build and run.

Launching NoirIt for the first time.

It might not be much to look at now, but it will be soon.

Prepping the Photos App

Before you start, create an album in Photos so you have at least one album to view in NoirIt later.

  1. Open Photos app. There is a bug in Photos when running on a simulator which might cause it to crash. If it does, re-open it.
  2. Tap Albums on the tab bar.
  3. Tap the + at the top of the screen.
  4. Select New Album.
  5. Name it My Cool Pics and tap Save.
  6. Select a few photos to add to the the new album.
  7. Navigate back to the main albums view and see your new album.

Adding an album to the Photos app.

That is all you need to do in Photos.

Getting PhotoKit Permissions

As with many iOS APIs, PhotoKit uses a permissions model. It presents users with a dialog box asking for permission for the app to access their images. Before you dive into accessing and modifying images, you must get permission. You do so using PHPhotoLibrary, a shared object that manages access to the photo library.

Modifying Info.plist

Your first step is to add a key to Info.plist describing why you want permission to access the library.

  1. Open Info.plist.
  2. Right-click Information Property List and select Add Row. A new line appears.
  3. Enter the key NSPhotoLibraryUsageDescription and press enter.
  4. In the value column, enter To add a noir filter. When iOS requests permission to access the library for the first time, it displays this information.

Your Info.plist should look like this:

The NoirIt info.plist.

Requesting Authorization

Open AlbumCollectionViewController.swift. Find getPermissionIfNecessary(completionHandler:) and replace its implementation with:

// 1
guard PHPhotoLibrary.authorizationStatus() != .authorized else {
// 2
PHPhotoLibrary.requestAuthorization { status in
  completionHandler(status == .authorized)
  1. The first thing you do is get the current authorization status from PHPhotoLibrary. If it’s already authorized, call the completion handler with a value of true.
  2. If permission was not previously granted, request it. When requesting authorization, iOS displays an alert dialog box asking for permission. It passes back the status as a PHAuthorizationStatus object in its completion handler. Call your completion handler and return true if the status value is .authorized, otherwise return false.
Note: PHAuthorizationStatus is an enum, which can also return notDetermined, restricted, denied and, new to iOS 14, limited. You might want to check for these and handle them appropriately. For now, keep NoirIt simple.

viewDidLoad() is already calling this method, so build and run. iOS asks for permission to access the photo library when NoirIt launches. If you are using, iOS 13 tap OK or on iOS 14, tap Allow Access to All Photos.

The PhotoKit library permission dialog box.

Understanding Assets

Even though you eventually will be getting images, it’s important to understand that you mostly work with assets in PhotoKit. Think about how you interact with the Photos app. Sure, you look at images, but there is also metadata such as favorites and geocoded location data. And there are more than images. Photos contains LivePhotos and video. Stuffing these things into UIImage doesn’t make sense. And that is where PHAsset comes in.

PHAsset is metadata describing an image, LivePhoto or video. It is immutable and doesn’t contain the image itself, but does provide the information you need to get the image. It also contains tons of information, such as creation and modification dates, location data, favorite and hidden status, burst data and much more. As you’ll soon see, PHAsset is a real workhorse.

Sometimes you work with a group of assets. These are usually returned as a PHAssetCollection object.

Asset Data Models

Open AlbumCollectionViewController.swift. Near the top of the file, add the following under the declaration for the sections property:

private var allPhotos = PHFetchResult<PHAsset>()
private var smartAlbums = PHFetchResult<PHAssetCollection>()
private var userCollections = PHFetchResult<PHAssetCollection>()

You might say to yourself, “Hey, self, what are these PHFetchResult things? I thought I was getting PHAssets and PHAssetCollections?” A simplified way of thinking of PHFetchResult is to consider it an array, which it is, in essence. It contains all the same methods and conventions of arrays, such as count() and index(of:). Plus, it intelligently handles fetching data, caching it and re-fetching it as needed. You’ll be fine if you think of PHFetchResult as an intelligent array of assets or collections. These properties are the app’s data store.

Fetching Assets and Asset Collections

Still within AlbumCollectionViewController.swift, find fetchAssets() and add the following code to it:

// 1
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [
    key: "creationDate",
    ascending: false)
// 2
allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
// 3
smartAlbums = PHAssetCollection.fetchAssetCollections(
  with: .smartAlbum,
  subtype: .albumRegular,
  options: nil)
// 4
userCollections = PHAssetCollection.fetchAssetCollections(
  with: .album,
  subtype: .albumRegular,
  options: nil)
  1. When fetching assets, you can apply a set of options that dictate the sorting, filtering and management of results. Here, you create a sort descriptor that sorts assets by creation date from newest to oldest.
  2. PHAsset provides functionality for fetching assets and returning the results as a PHFetchResult. Here, you pass it the options created above and assign the result to allPhotos.
  3. The Photos app automatically creates smart albums, such as Favorites and Recents. Albums are a group of assets and, as such, belong in PHAssetCollection objects. Here you fetch smart album collections. You won’t sort these, so options is nil.
  4. Accessing user created albums is similar, except that you fetch the .album type.

With your data store now populated, the next task is to update the UI.

Layout with Swifty

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] : 
  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 {
// 2
let resultHandler: (UIImage?, [AnyHashable: Any]?) -> Void = { image, info in
  self.image = image
// 3
  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
  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)


  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

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
      let change = changeInstance.changeDetails(for: asset),
      let updatedAsset = change.objectAfterChanges
      else { return }
    // 3
    DispatchQueue.main.sync {
      // 4
      asset = updatedAsset
        targetSize: view.bounds.size
      ) { [weak self] _ in
        guard let self = self else { return }
        // 5
  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:


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

deinit {

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 {
    DispatchQueue.main.sync {
      // 2
      assets = change.fetchResultAfterChanges

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():


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

deinit {

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

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:


After viewDidLoad() add:

deinit {

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 = 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 {
  // 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.

Saving Edits

Use the editing output container created above to save the change to the library. Again, use a PHAssetChangeRequest like you did for changing metadata earlier.

Still in PhotoViewController.swift, find saveImage() and add the following to it:

// 1
let changeRequest: () -> Void = { [weak self] in
  guard let self = self else { return }
  let changeRequest = PHAssetChangeRequest(for: self.asset)
  changeRequest.contentEditingOutput = self.editingOutput
// 2
let completionHandler: (Bool, Error?) -> Void = { [weak self] success, error in
  guard let self = self else { return }

  guard success else {
    print("Error: cannot edit asset: \(String(describing: error))")
  // 3
  self.editingOutput = nil
  DispatchQueue.main.async {
    self.saveButton.isEnabled = false
// 4
  completionHandler: completionHandler)
  1. Like before, you handle the change within a code block. Create a PHAssetChangeRequest for the asset and apply the editing output container.
  2. Create a completion handler to run after the change completes. Check for a successful result and print out the error if the result is not successful.
  3. If the change is a success, assign nil to the container property because it is no longer needed. Disable the save button, because there is nothing else left to save.
  4. Call the library’s performChanges(:completionHandler:) and pass in the change request and completion handler.

Build and run. Navigate to a photo and tap the Apply Filter button. Tap the save button. iOS displays a dialog box asking for permission to modify the photo. Tap Modify.

PhotoKit's permission dialog box for modifying an asset.

Navigate back to All Photos and select the photo again. You should see that the modified image saved successfully.

Undoing Edits

There is one button left in the photo view controller that does not work: Undo. An undo, or revert, is also handled with … well, you probably know by now: PHAssetChangeRequest.

Use the existence of asset change data to determine the Undo button’s enabled state. Find updateUndoButton() and replace its contents with:

let adjustmentResources = PHAssetResource.assetResources(for: asset)
  .filter { $0.type == .adjustmentData }
undoButton.isEnabled = !adjustmentResources.isEmpty

Each edit to an asset creates a PHAssetResource object. The assetResources(for:) returns an array of resources for the given asset. Filter the assets by the existence of adjustment data. The button’s isEnabled property is set to true if there are edits, otherwise it is false.

It’s time to add the undo logic. Find undo() and add this code to it:

// 1
let changeRequest: () -> Void = { [weak self] in
  guard let self = self else { return }
  let request = PHAssetChangeRequest(for: self.asset)
// 2
let completionHandler: (Bool, Error?) -> Void = { [weak self] success, error in
  guard let self = self else { return }

  guard success else {
    print("Error: can't revert the asset: \(String(describing: error))")
  DispatchQueue.main.async {
    self.undoButton.isEnabled = false
// 3
  completionHandler: completionHandler)

By now, this pattern should be pretty familiar.

  1. Create a change request block to contain the change logic. For this example, you create a change request for the asset and call revertAssetContentToOriginal(). As you might expect, this requests that the asset change back to its original state. This does not affect metadata.
  2. The completion handler checks for a successful result and disables the undo button if the result succeeded.
  3. Finally, instruct the library to perform the change.

Build and run. Select the photo to which you applied the filter. Tap Undo. Just like when you saved the asset earlier, iOS asks the user for permission to undo all changes.

PhotoKit's revert permission dialog box.

Tap Revert. The image changes back to the original image.

Where to Go From Here?

Congratulations! You’ve covered a lot of ground in a short time. You can download the final project by clicking the Download Materials button at the top or bottom of the tutorial. You learned about:

  • PhotoKit’s permission model.
  • Accessing all photos, smart albums and user collections.
  • Fetching images from assets.
  • Modifying asset metadata.
  • Editing an asset’s image.
  • Saving asset modifications.
  • Reverting asset modifications.

There is much more that PhotoKit has to offer, such as LivePhoto, video and the photo editing extension. Check out the PhotoKit documentation for more information:

Please share any comments or questions about this article in the forum discussion!

Average Rating


Add a rating for this content

6 ratings

More like this