Querying the iOS Photo Library
Programatically access the iOS Photo Library with PhotoKit
To allow users to select a photo from their photo library the simplest option is to use UIImagePickerController to present the built in system interface to pick a photo and return it to your app.
However, UIImagePickerController
has limitations, such as no customisations, programmatic library access or even multi-selection. So if you need more access, or more control, than UIImagePickerController
provides you'll need to dig into the PhotoKit framework.
Required App Permissions
To access the users Photo library your app will need to ask for permission. First the app has to declare this in the Info.plist
file, using the NSPhotoLibraryUsageDescription
key and a description (which can be localised).
<key>NSPhotoLibraryUsageDescription</key>
<string>This app wants to use your photos.</string>
You can query the current authorisation status using the API [PHPhotoLibrary authorizationStatus]
or PHPhotoLibrary.authorizationStatus()
.
This API will return one of the following status codes:
// The user has not yet made a choice
PHAuthorizationStatusNotDetermined // ObjC
PHAuthorizationStatus.notDetermined // Swift
// Access is not authorized and the user cannot change this
// (possibly due to restrictions such as parental controls)
PHAuthorizationStatusRestricted
PHAuthorizationStatus.restricted
// The user has explicitly denied the app access
PHAuthorizationStatusDenied
PHAuthorizationStatus.denied
// The user has authorized access to the photo library
PHAuthorizationStatusAuthorized
PHAuthorizationStatus.authorized
The user will be prompted for permission, either when you explicitly ask for permission using the requestAuthorization
method on PHPhotoLibrary
, or the first time you call an authorisation required Photos framework method.
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
NSLog(@"Permission Status: %ld", (long)status);
}];
PHPhotoLibrary.requestAuthorization(handler: { (status) in
print("Permission status: \(status)")
})
Using the PhotoKit Framework
Querying for Media
How to get the 50 most recently taken photos.
// 50 most recently created images
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
options.fetchLimit = 50
options.wantsIncrementalChangeDetails = false
let result: PHFetchResult<PHAsset> = PHAsset.fetchAssets(with: .image, options: options)
result.enumerateObjects(options: [], using: { (asset, index, stop) in
self.assets.append(asset)
})
How to get a list of 50 most recently taken screenshots.
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
options.fetchLimit = 50
options.predicate = NSPredicate(
format: "(mediaSubtype & %d) != 0",
PHAssetMediaSubtype.photoScreenshot.rawValue
)
let result: PHFetchResult<PHAsset> = PHAsset.fetchAssets(with: .image, options: options)
Documentation for the predicate
.
A predicate that specifies which properties to select results by and that also specifies any constraints on selection.
Construct a predicate with the properties of the class of objects that you want to fetch, listed in the PHFetchOptions table.
For example, the following code uses a predicate to fetch assets matching a specific set of mediaSubtypes values.
Photos does not support predicates created with the NSPredicate method init or the predicateWithBlock method.
let format: String = "(mediaSubtypes & %d) != 0 || (mediaSubtypes & %d) != 0"
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(
format: format,
argumentArray: [PHAssetMediaSubtype.photoPanorama, PHAssetMediaSubtype.videoHighFrameRate]
)
let fetchResult = PHAsset.fetchAssets(
with: PHAssetMediaType.image,
options: fetchOptions
)
Class for Fetch Method | Supported Keys |
---|---|
PHAsset |
SELF , localIdentifier , creationDate , modificationDate , mediaType , mediaSubtypes , duration , pixelWidth , pixelHeight , favorite (or isFavorite ), hidden (or isHidden ), burstIdentifier |
PHAssetCollection |
SELF , localIdentifier , localizedTitle (or title ), startDate , endDate , estimatedAssetCount |
PHCollectionList |
SELF , localIdentifier , localizedTitle (or title ), startDate , endDate |
PHCollection (can fetch a mix of PHCollectionList and PHAssetCollection objects) |
SELF , localIdentifier , localizedTitle (or title ), startDate , endDate |
API Docs: https://developer.apple.com/documentation/photos/phfetchoptions#overview
Querying Albums
How to fetch user-created albums and smart albums, then retrieve assets from a specific album.
// Fetch user-created albums
let userAlbums = PHAssetCollection.fetchAssetCollections(
with: .album,
subtype: .albumRegular,
options: nil
)
// Fetch smart albums like Favorites, Recently Added, etc.
let smartAlbums = PHAssetCollection.fetchAssetCollections(
with: .smartAlbum,
subtype: .any,
options: nil
)
// Get assets from a specific album
func getAssetsFromAlbum(album: PHAssetCollection, limit: Int = 0) -> [PHAsset] {
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
if limit > 0 {
options.fetchLimit = limit
}
let result = PHAsset.fetchAssets(in: album, options: options)
var assets = [PHAsset]()
result.enumerateObjects(options: [], using: { (asset, index, stop) in
assets.append(asset)
})
return assets
}
Modifying Albums
How to create new albums, add assets to existing albums, and remove assets from albums.
// Create a new album
func createAlbum(withTitle title: String, completion: @escaping (PHAssetCollection?) -> Void) {
var placeholder: String?
PHPhotoLibrary.shared().performChanges({
let createRequest: PHAssetCollectionChangeRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: title)
placeholder = createRequest.placeholderForCreatedAssetCollection.localIdentifier
}, completionHandler: { (success: Bool, error: (any Error)?) in
if success, let identifier = placeholder
{
let fetchResult: PHFetchResult<PHAssetCollection> = PHAssetCollection.fetchAssetCollections(
withLocalIdentifiers: [identifier],
options: nil
)
let collection: PHAssetCollection? = fetchResult.firstObject
DispatchQueue.main.async(execute: {
completion(collection)
})
}
else {
DispatchQueue.main.async(execute: {
completion(nil)
})
}
})
}
// Add assets to an album
func addAssets(assets: [PHAsset], toAlbum album: PHAssetCollection, completion: @escaping (Bool) -> Void)
{
PHPhotoLibrary.shared().performChanges({
if let changeRequest = PHAssetCollectionChangeRequest(for: album) {
changeRequest.addAssets(PHAsset.fetchAssets(withLocalIdentifiers: assets.map { $0.localIdentifier }, options: nil) as NSFastEnumeration)
}
}, completionHandler: { (success: Bool, error: (any Error)?) in
DispatchQueue.main.async(execute: {
completion(success)
})
})
}
// Remove assets from an album
func removeAssets(assets: [PHAsset], fromAlbum album: PHAssetCollection, completion: @escaping (Bool) -> Void)
{
PHPhotoLibrary.shared().performChanges({
if let changeRequest = PHAssetCollectionChangeRequest(for: album) {
changeRequest.removeAssets(PHAsset.fetchAssets(withLocalIdentifiers: assets.map { $0.localIdentifier }, options: nil) as NSFastEnumeration)
}
}, completionHandler: { (success: Bool, error: (any Error)?) in
DispatchQueue.main.async(execute: {
completion(success)
})
})
}
Getting Folders of Albums
How to fetch folders containing albums and retrieve the albums within each folder.
// Fetch folders that contain albums
func getFolders() -> [PHCollectionList] {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "localizedTitle", ascending: true)]
let foldersFetchResult = PHCollectionList.fetchTopLevelUserCollections(with: fetchOptions)
var folders = [PHCollectionList]()
foldersFetchResult.enumerateObjects(options: [], using: { (collection, index, stop) in
if let folder = collection as? PHCollectionList {
folders.append(folder)
}
})
return folders
}
// Get albums within a folder
func getAlbumsInFolder(folder: PHCollectionList) -> [PHAssetCollection] {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "localizedTitle", ascending: true)]
let albumsFetchResult = PHCollection.fetchCollections(in: folder, options: fetchOptions)
var albums = [PHAssetCollection]()
albumsFetchResult.enumerateObjects(options: [], using: { (collection, index, stop) in
if let album = collection as? PHAssetCollection {
albums.append(album)
}
})
return albums
}
Modifying Folders
How to create new folders and add albums to existing folders in the photo library.
// Create a new folder
func createFolder(withTitle title: String, completion: @escaping (PHCollectionList?) -> Void) {
var placeholder: String?
PHPhotoLibrary.shared().performChanges({
let createRequest = PHCollectionListChangeRequest.creationRequestForCollectionList(withTitle: title)
placeholder = createRequest.placeholderForCreatedCollectionList.localIdentifier
}, completionHandler: { success, error in
if success, let identifier: String = placeholder
{
let fetchResult: PHFetchResult<PHCollectionList> = PHCollectionList.fetchCollectionLists(
withLocalIdentifiers: [identifier],
options: nil
)
let folder: PHCollectionList? = fetchResult.firstObject
DispatchQueue.main.async(execute: {
completion(folder)
})
}
else {
DispatchQueue.main.async(execute: {
completion(nil)
})
}
})
}
// Add albums to a folder
func addAlbums(albums: [PHAssetCollection], toFolder folder: PHCollectionList, completion: @escaping (Bool) -> Void) {
PHPhotoLibrary.shared().performChanges({
if let changeRequest = PHCollectionListChangeRequest(for: folder)
{
changeRequest.addChildCollections(PHAssetCollection.fetchAssetCollections(
withLocalIdentifiers: albums.map({ $0.localIdentifier }),
options: nil
) as NSFastEnumeration)
}
}, completionHandler: { (success: Bool, error: (any Error)?) in
DispatchQueue.main.async(execute: {
completion(success)
})
})
}
Getting thumbnails
How to fetch optimized thumbnail images for photo assets to display in a UI collection view.
// Get thumbnail for UICollectionView
func getThumbnail(for asset: PHAsset, targetSize: CGSize, completion: @escaping (UIImage?) -> Void)
{
let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic
options.resizeMode = .exact
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(
for: asset,
targetSize: targetSize,
contentMode: .aspectFill,
options: options,
resultHandler: { (image: UIImage?, info: [AnyHashable : Any]?) in
DispatchQueue.main.async(execute: {
completion(image)
})
})
}
The options for the delivery mode are:
/// Options for delivering requested image data
enum PHImageRequestOptionsDeliveryMode {
/// Photos automatically provides one or more results in order to balance image quality and responsiveness.
case opportunistic
/// Photos provides only the highest-quality image available, regardless of how much time it takes to load.
case highQualityFormat
/// Photos provides only a fast-loading image, possibly sacrificing image quality.
case fastFormat
}
There is also a property to control which version of the photo you would like:
options.version = PHImageRequestOptionsVersion.current
/// Options for requesting an image asset with or without adjustments.
enum PHImageRequestOptionsVersion {
/// Request the most recent version of the image asset (the one that reflects all edits).
case current
/// Request a version of the image asset without adjustments.
case unadjusted
/// Request the original, highest-fidelity version of the image asset.
case original
}
You can also specify how you would like the image resized to the target size you asked for.
options.resizeMode = PHImageRequestOptionsResizeMode.none
/// Options for how to resize the requested image to fit a target size.
enum PHImageRequestOptionsResizeMode {
/// Photos does not resize the image asset.
case none
/// Photos efficiently resizes the image to a size similar to, or slightly larger than, the target size.
case fast
/// Photos resizes the image to match the target size exactly.
case exact
}
You can also specify how the image should be cropped.
options.normalizedCropRect = CGRect.zero
To request a cropped image, specify the crop rectangle in a unit coordinate space relative to the image. In this coordinate system, the point {0.0,0.0} refers to the upper left corner of the image, and the point {1.0,1.0} refers to the opposite corner regardless of the image’s aspect ratio.
This property defaults to CGRectZero, which specifies no cropping.
If you specify a crop rectangle, you must also specify the PHImageRequestOptionsResizeMode.exact option for the resizeMode property.
https://developer.apple.com/documentation/photos/phimagerequestoptions/normalizedcroprect
Getting Full Size Images
How to retrieve full-resolution images from the photo library for detailed viewing or editing.
// Get full size image for editing
func getFullSizeImage(for asset: PHAsset, completion: @escaping (UIImage?, [AnyHashable: Any]?) -> Void) {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
options.isSynchronous = false
PHImageManager.default().requestImage(
for: asset,
targetSize: PHImageManagerMaximumSize,
contentMode: .aspectFit,
options: options,
resultHandler: { (image: UIImage?, info: [AnyHashable : Any]?) in
DispatchQueue.main.async(execute: {
completion(image, info)
})
}
)
}
Editing Metadata
How to update photo metadata including location and favorite status.
// Modify photo location
func updateLocation(for asset: PHAsset, to newLocation: CLLocation, completion: @escaping (Bool) -> Void)
{
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest(for: asset)
request.location = newLocation
}, completionHandler: { (success: Bool, error: (any Error)?) in
DispatchQueue.main.async(execute: {
completion(success)
})
})
}
// Favorite or unfavorite an asset
func setFavorite(asset: PHAsset, isFavorite: Bool, completion: @escaping (Bool) -> Void)
{
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest(for: asset)
request.isFavorite = isFavorite
}, completionHandler: { (success: Bool, error: (any Error)?) in
DispatchQueue.main.async(execute: {
completion(success)
})
})
}
Getting Videos
How to fetch and stream video assets from the photo library, including iCloud videos.
// Stream iCloud video
func getVideoAsset(for asset: PHAsset, completion: @escaping (AVAsset?, [AnyHashable: Any]?) -> Void) {
let options = PHVideoRequestOptions()
options.deliveryMode = .automatic
options.isNetworkAccessAllowed = true
PHImageManager.default().requestAVAsset(
forVideo: asset,
options: options,
resultHandler: { (avAsset: AVAsset?, audioMix: AVAudioMix?, info: [AnyHashable : Any]?) in
DispatchQueue.main.async(execute: {
completion(avAsset, info)
})
}
)
}
The options for the delivery mode are:
enum PHVideoRequestOptionsDeliveryMode {
/// Photos automatically determines which quality of video data to provide based on the request and current conditions.
case automatic
/// Photos provides only the highest quality video available.
case highQualityFormat
/// Photos provides a video of moderate quality unless a higher quality version is locally cached.
case mediumQualityFormat
/// Photos provides whatever quality of video can be most quickly loaded.
case fastFormat
}
There is also a property to control which version of the video you would like:
options.version = PHVideoRequestOptionsVersion.current
/// Options for requesting a video asset with or without adjustments.
enum PHVideoRequestOptionsVersion {
/// Request the most recent version of the video asset, reflecting all edits.
case current
/// Request a version of the video asset without adjustments.
case original
}
Playing Live Photos
How to retrieve and display Live Photos with their interactive capabilities.
// Get PHLivePhoto object for display
func getLivePhoto(for asset: PHAsset, targetSize: CGSize, completion: @escaping (PHLivePhoto?, [AnyHashable: Any]?) -> Void) {
let options = PHLivePhotoRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
PHImageManager.default().requestLivePhoto(
for: asset,
targetSize: targetSize,
contentMode: .aspectFit,
options: options,
resultHandler: { (livePhoto: PHLivePhoto?, info: [AnyHashable : Any]?) in
DispatchQueue.main.async(execute: {
completion(livePhoto, info)
})
}
)
}
Get video from live photo
How to extract and access the video component from a Live Photo asset.
// Extract video component from a Live Photo
func getVideoFromLivePhoto(asset: PHAsset, completion: @escaping (AVAsset?) -> Void)
{
guard asset.mediaSubtypes.contains(.photoLive) else {
completion(nil)
return
}
let resources: [PHAssetResource] = PHAssetResource.assetResources(for: asset)
guard let videoResource: PHAssetResource = resources.first(where: { $0.type == .pairedVideo }) else {
completion(nil)
return
}
let options = PHAssetResourceRequestOptions()
options.isNetworkAccessAllowed = true
let fileURL: URL = FileManager.default.temporaryDirectory.appendingPathComponent("\(NSUUID().uuidString).mov")
PHAssetResourceManager.default().requestData(
for: videoResource,
options: options,
dataReceivedHandler: { (data: Data) in
// This handler might be called multiple times
try? data.write(to: fileURL, options: .atomic)
},
completionHandler: { (error: (any Error)?) in
if error == nil {
let asset = AVAsset(url: fileURL)
DispatchQueue.main.async(execute: {
completion(asset)
})
}
else {
DispatchQueue.main.async(execute: {
completion(nil)
})
}
}
)
}
Getting updates to your PHFetchResult
How to track and respond to changes in the photo library using the persistent change API.
/// Using the new change history API to track changes
class PhotoLibraryChangeTracker {
private var lastChangeToken: PHPersistentChangeToken?
init() {
self.lastChangeToken = PHPhotoLibrary.shared().currentChangeToken
}
/// Fetch all asset changes since the last change update `lastChangeToken`
/// - Parameter completion: inserted, updated, deleted
func fetchChanges(completion: @escaping ([String], [String], [String]) -> Void) {
guard let token: PHPersistentChangeToken = self.lastChangeToken else {
completion([], [], [])
return
}
do {
let persistentChanges: PHPersistentChangeFetchResult = try PHPhotoLibrary.shared().fetchPersistentChanges(since: token)
var insertedIdentifiers = [String]()
var updatedIdentifiers = [String]()
var deletedIdentifiers = [String]()
for persistentChange in persistentChanges {
do {
let changeDetails: PHPersistentObjectChangeDetails = try persistentChange.changeDetails(for: .asset)
insertedIdentifiers.append(contentsOf: changeDetails.insertedLocalIdentifiers)
updatedIdentifiers.append(contentsOf: changeDetails.updatedLocalIdentifiers)
deletedIdentifiers.append(contentsOf: changeDetails.deletedLocalIdentifiers)
// Store the last change token for future use
self.lastChangeToken = persistentChange.changeToken
}
catch {
print("ERROR: \(error)")
}
}
DispatchQueue.main.async(execute: {
completion(insertedIdentifiers, updatedIdentifiers, deletedIdentifiers)
})
}
catch
PHPhotosError.persistentChangeTokenExpired,
PHPhotosError.persistentChangeDetailsUnavailable
{
// Handle errors by refetching tracked objects
DispatchQueue.main.async(execute: {
completion([], [], [])
})
}
catch {
DispatchQueue.main.async(execute: {
completion([], [], [])
})
}
}
}