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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started with PhotoKit
30 mins
- Getting Started
- Prepping the Photos App
- Getting PhotoKit Permissions
- Modifying Info.plist
- Requesting Authorization
- Understanding Assets
- Asset Data Models
- Fetching Assets and Asset Collections
- Prepping the Collection View
- Updating the Cell
- Fetching Images from Assets
- Displaying Album Assets
- Modifying Asset Metadata
- Change Requests
- Photo View Controller Change Observer
- Registering the Photo View Controller
- Photos View Controller Change Observer
- Registering the Photos View Controller
- Album View Controller Change Observer
- Album View Controller Registration
- Editing a Photo
- Saving Edits
- Undoing Edits
- Where to Go From Here?
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
- First, dequeue an
AlbumCollectionViewCell
. - 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.
- For the “all photos” section, set the cover image to
allPhotos
‘s first asset. Update the cell with the section name and count. - Because
smartAlbums
anduserCollections
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 usingPHAsset
‘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. - 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.
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)
- If
asset
isnil
, then returnfalse
and you’re done. Otherwise, continue. - Next, create the result handler that the image manager will call when the image request is complete. Assign the returned image to
UIImageView
‘simage
property. Call the completion handler with a value oftrue
, indicating that the request is complete. - 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 thesize
. The default value isaspectFill
.
Build and run. Your albums now have 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:
- Get the selected index path.
- Get the section type and item for the selected item.
-
PhotosCollectionViewController
needs a list of assets and a title. - If the user selects the “all photos” section, use
allPhotos
as the assetsassets
and set the title. - 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.
- Create the view controller.
Build and run. Tap All Photos in the album view. You now see a collection of all your photos.
Tap one of the photos.
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)
- 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. - 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?