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([], [], [])
			})
		}
	}
	
}