Photo Library API Reference
Quick Reference
// SWIFTUI PHOTO PICKER (iOS 16+) import PhotosUI
@State private var item: PhotosPickerItem?
PhotosPicker(selection: $item, matching: .images) { Text("Select Photo") } .onChange(of: item) { _, newItem in Task { if let data = try? await newItem?.loadTransferable(type: Data.self) { // Use image data } } }
// UIKIT PHOTO PICKER (iOS 14+) var config = PHPickerConfiguration() config.selectionLimit = 1 config.filter = .images let picker = PHPickerViewController(configuration: config) picker.delegate = self
// SAVE TO CAMERA ROLL try await PHPhotoLibrary.shared().performChanges { PHAssetCreationRequest.creationRequestForAsset(from: image) }
// CHECK PERMISSION let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
PHPickerViewController (iOS 14+)
System photo picker for UIKit apps. No permission required.
Configuration
import PhotosUI
var config = PHPickerConfiguration()
// Selection limit (0 = unlimited) config.selectionLimit = 5
// Filter by asset type config.filter = .images
// Use photo library (enables asset identifiers) config = PHPickerConfiguration(photoLibrary: .shared())
// Preferred asset representation config.preferredAssetRepresentationMode = .automatic // default // .current - original format // .compatible - converted to compatible format
Filter Options
// Basic filters PHPickerFilter.images PHPickerFilter.videos PHPickerFilter.livePhotos
// Combined filters PHPickerFilter.any(of: [.images, .videos])
// Exclusion filters (iOS 15+) PHPickerFilter.all(of: [.images, .not(.screenshots)]) PHPickerFilter.not(.livePhotos)
// Playback style filters (iOS 17+) PHPickerFilter.any(of: [.cinematicVideos, .slomoVideos])
Presenting
let picker = PHPickerViewController(configuration: config) picker.delegate = self present(picker, animated: true)
Delegate
extension ViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
for result in results {
// Get asset identifier (if using PHPickerConfiguration(photoLibrary:))
let identifier = result.assetIdentifier
// Load as UIImage
result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
guard let image = object as? UIImage else { return }
DispatchQueue.main.async {
self.displayImage(image)
}
}
// Load as Data
result.itemProvider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in
guard let data else { return }
// Use data
}
// Load Live Photo
result.itemProvider.loadObject(ofClass: PHLivePhoto.self) { object, error in
guard let livePhoto = object as? PHLivePhoto else { return }
// Use live photo
}
}
}
}
PHPickerResult Properties
Property Type Description
itemProvider
NSItemProvider Provides selected asset data
assetIdentifier
String? PHAsset identifier (if using photoLibrary config)
PhotosPicker (SwiftUI, iOS 16+)
SwiftUI view for photo selection. No permission required.
Basic Usage
import SwiftUI import PhotosUI
// Single selection @State private var selectedItem: PhotosPickerItem?
PhotosPicker(selection: $selectedItem, matching: .images) { Label("Select Photo", systemImage: "photo") }
// Multiple selection @State private var selectedItems: [PhotosPickerItem] = []
PhotosPicker( selection: $selectedItems, maxSelectionCount: 5, matching: .images ) { Text("Select Photos") }
Filters
// Images only matching: .images
// Videos only matching: .videos
// Images and videos matching: .any(of: [.images, .videos])
// Live Photos matching: .livePhotos
// Exclude screenshots (iOS 15+) matching: .all(of: [.images, .not(.screenshots)])
Selection Behavior
PhotosPicker( selection: $items, maxSelectionCount: 10, selectionBehavior: .ordered, // .default, .ordered, .continuous matching: .images ) { ... }
Behavior Description
.default
Standard multi-select
.ordered
Selection order preserved
.continuous
Live updates as user selects (iOS 17+)
Embedded Picker (iOS 17+)
PhotosPicker( selection: $items, maxSelectionCount: 10, selectionBehavior: .continuous, matching: .images ) { Text("Select") } .photosPickerStyle(.inline) // Embed in view hierarchy .photosPickerDisabledCapabilities([.selectionActions]) .photosPickerAccessoryVisibility(.hidden, edges: .all)
Style Description
.presentation
Modal sheet (default)
.inline
Embedded in view
.compact
Single row
Disabled Capability Effect
.search
Hide search bar
.collectionNavigation
Hide albums
.stagingArea
Hide selection review
.selectionActions
Hide Add/Cancel
Accessory Visibility Description
.hidden , .automatic , .visible
Per edge
HDR Preservation (iOS 17+)
PhotosPicker( selection: $items, matching: .images, preferredItemEncoding: .current // Don't transcode, preserve HDR ) { ... }
Encoding Description
.automatic
System decides format
.current
Original format, preserves HDR
.compatible
Force compatible format
Loading Images from PhotosPickerItem
// Load as Data (most reliable) if let data = try? await item.loadTransferable(type: Data.self), let image = UIImage(data: data) { // Use image }
// Custom Transferable for direct UIImage struct ImageTransferable: Transferable { let image: UIImage
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(importedContentType: .image) { data in
guard let image = UIImage(data: data) else {
throw TransferError.importFailed
}
return ImageTransferable(image: image)
}
}
}
// Usage if let result = try? await item.loadTransferable(type: ImageTransferable.self) { let image = result.image }
PhotosPickerItem Properties
Property Type Description
itemIdentifier
String Unique identifier
supportedContentTypes
[UTType] Available representations
PhotosPickerItem Methods
// Load transferable func loadTransferable<T: Transferable>(type: T.Type) async throws -> T?
// Load with progress func loadTransferable<T: Transferable>( type: T.Type, completionHandler: @escaping (Result<T?, Error>) -> Void ) -> Progress
PHPhotoLibrary
Access and modify the photo library.
Authorization Status
// Check current status let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
// Request authorization let newStatus = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
PHAuthorizationStatus
Status Description
.notDetermined
User hasn't been asked
.restricted
Parental controls limit access
.denied
User denied access
.authorized
Full access granted
.limited
Access to user-selected photos only (iOS 14+)
Access Levels
// Read and write PHPhotoLibrary.requestAuthorization(for: .readWrite)
// Add only (save photos, no reading) PHPhotoLibrary.requestAuthorization(for: .addOnly)
Limited Library Picker
// Present picker to expand limited selection @MainActor func presentLimitedLibraryPicker() { guard let viewController = UIApplication.shared.keyWindow?.rootViewController else { return } PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController) }
// With completion handler PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController) { identifiers in // identifiers: asset IDs user added }
Performing Changes
// Async changes try await PHPhotoLibrary.shared().performChanges { // Create, update, or delete assets }
// With completion handler PHPhotoLibrary.shared().performChanges({ // Changes }) { success, error in // Handle result }
Change Observer
class PhotoObserver: NSObject, PHPhotoLibraryChangeObserver {
override init() {
super.init()
PHPhotoLibrary.shared().register(self)
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
// Handle changes
guard let changes = changeInstance.changeDetails(for: fetchResult) else { return }
DispatchQueue.main.async {
// Update UI with new fetch result
let newResult = changes.fetchResultAfterChanges
}
}
}
PHAsset
Represents an asset in the photo library.
Fetching Assets
// All photos let allPhotos = PHAsset.fetchAssets(with: .image, options: nil)
// With options let options = PHFetchOptions() options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] options.fetchLimit = 100 options.predicate = NSPredicate(format: "mediaType == %d", PHAssetMediaType.image.rawValue)
let recentPhotos = PHAsset.fetchAssets(with: options)
// By identifier let assets = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil)
Asset Properties
Property Type Description
localIdentifier
String Unique ID
mediaType
PHAssetMediaType .image , .video , .audio
mediaSubtypes
PHAssetMediaSubtype .photoLive , .photoPanorama , etc.
pixelWidth
Int Width in pixels
pixelHeight
Int Height in pixels
creationDate
Date? When taken
modificationDate
Date? Last modified
location
CLLocation? GPS location
duration
TimeInterval Video duration
isFavorite
Bool Marked as favorite
isHidden
Bool In hidden album
PHAssetMediaType
Type Value
.unknown
0
.image
1
.video
2
.audio
3
PHAssetMediaSubtype
Subtype Description
.photoPanorama
Panoramic photo
.photoHDR
HDR photo
.photoScreenshot
Screenshot
.photoLive
Live Photo
.photoDepthEffect
Portrait mode
.videoStreamed
Streamed video
.videoHighFrameRate
Slo-mo video
.videoTimelapse
Timelapse
.videoCinematic
Cinematic mode
PHAssetCreationRequest
Create new assets in the photo library.
Creating from UIImage
try await PHPhotoLibrary.shared().performChanges { PHAssetCreationRequest.creationRequestForAsset(from: image) }
Creating from File URL
try await PHPhotoLibrary.shared().performChanges { PHAssetCreationRequest.creationRequestForAssetFromImage(atFileURL: imageURL) }
// For video try await PHPhotoLibrary.shared().performChanges { PHAssetCreationRequest.creationRequestForAssetFromVideo(atFileURL: videoURL) }
Creating with Resources
try await PHPhotoLibrary.shared().performChanges { let request = PHAssetCreationRequest.forAsset()
// Add photo resource
let options = PHAssetResourceCreationOptions()
options.shouldMoveFile = true // Move instead of copy
request.addResource(with: .photo, fileURL: photoURL, options: options)
// Set creation date
request.creationDate = Date()
// Set location
request.location = CLLocation(latitude: 37.7749, longitude: -122.4194)
}
Deferred Photo Proxy (iOS 17+)
Save camera proxy photos for background processing:
// From AVCaptureDeferredPhotoProxy callback try await PHPhotoLibrary.shared().performChanges { let request = PHAssetCreationRequest.forAsset()
// Use .photoProxy to trigger deferred processing
request.addResource(with: .photoProxy, data: proxyData, options: nil)
}
Resource Type Description
.photo
Standard photo
.video
Video file
.photoProxy
Deferred processing proxy (iOS 17+)
.adjustmentData
Edit adjustments
Getting Created Asset
try await PHPhotoLibrary.shared().performChanges { let request = PHAssetCreationRequest.forAsset() request.addResource(with: .photo, fileURL: url, options: nil)
// Get placeholder for later fetching
let placeholder = request.placeholderForCreatedAsset
// placeholder.localIdentifier available after changes complete
}
PHFetchResult
Ordered list of assets from a fetch.
Properties
Property Type Description
count
Int Number of items
firstObject
T? First item
lastObject
T? Last item
Methods
// Access by index let asset = fetchResult.object(at: 0) let asset = fetchResult[0]
// Get multiple let assets = fetchResult.objects(at: IndexSet(0..<10))
// Iteration fetchResult.enumerateObjects { asset, index, stop in // Process asset if shouldStop { stop.pointee = true } }
// Check contains let contains = fetchResult.contains(asset) let index = fetchResult.index(of: asset)
PHImageManager
Request images from assets.
Request Image
let manager = PHImageManager.default()
let options = PHImageRequestOptions() options.deliveryMode = .highQualityFormat options.resizeMode = .exact options.isNetworkAccessAllowed = true // For iCloud photos
let targetSize = CGSize(width: 300, height: 300)
manager.requestImage( for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options ) { image, info in guard let image else { return }
// Check if this is the final image
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool) ?? false
if !isDegraded {
// Final high-quality image
}
}
PHImageRequestOptions
Property Type Description
deliveryMode
PHImageRequestOptionsDeliveryMode Quality preference
resizeMode
PHImageRequestOptionsResizeMode Resize behavior
isNetworkAccessAllowed
Bool Allow iCloud download
isSynchronous
Bool Synchronous request
progressHandler
Block Download progress
allowSecondaryDegradedImage
Bool Extra callback during deferred processing (iOS 17+)
Secondary Degraded Image (iOS 17+)
For photos undergoing deferred processing, get an intermediate quality image:
let options = PHImageRequestOptions() options.allowSecondaryDegradedImage = true
// Callback order: // 1. Low quality (immediate, isDegraded = true) // 2. Medium quality (new, isDegraded = true) -- while processing // 3. Final quality (isDegraded = false)
Delivery Modes
Mode Description
.opportunistic
Fast thumbnail, then high quality
.highQualityFormat
Only high quality
.fastFormat
Only fast/degraded
Request Video
manager.requestAVAsset(forVideo: asset, options: nil) { avAsset, audioMix, info in guard let avAsset else { return } // Use AVAsset for playback }
// Or export to file manager.requestExportSession( forVideo: asset, options: nil, exportPreset: AVAssetExportPresetHighestQuality ) { session, info in session?.outputURL = outputURL session?.outputFileType = .mp4 session?.exportAsynchronously { ... } }
PHChange
Represents changes to the photo library.
Getting Change Details
func photoLibraryDidChange(_ changeInstance: PHChange) { guard let changes = changeInstance.changeDetails(for: fetchResult) else { return }
// Check what changed
let hasIncrementalChanges = changes.hasIncrementalChanges
let insertedIndexes = changes.insertedIndexes
let removedIndexes = changes.removedIndexes
let changedIndexes = changes.changedIndexes
// Get new fetch result
let newResult = changes.fetchResultAfterChanges
// Update collection view
DispatchQueue.main.async {
if hasIncrementalChanges {
collectionView.performBatchUpdates {
if let removed = removedIndexes {
collectionView.deleteItems(at: removed.map { IndexPath(item: $0, section: 0) })
}
if let inserted = insertedIndexes {
collectionView.insertItems(at: inserted.map { IndexPath(item: $0, section: 0) })
}
if let changed = changedIndexes {
collectionView.reloadItems(at: changed.map { IndexPath(item: $0, section: 0) })
}
}
} else {
collectionView.reloadData()
}
}
}
Common Code Patterns
Complete Photo Gallery View
import SwiftUI import Photos
@MainActor class PhotoGalleryViewModel: ObservableObject { @Published var assets: [PHAsset] = [] @Published var authorizationStatus: PHAuthorizationStatus = .notDetermined
func requestAccess() async {
authorizationStatus = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
if authorizationStatus == .authorized || authorizationStatus == .limited {
fetchAssets()
}
}
func fetchAssets() {
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
options.fetchLimit = 100
let result = PHAsset.fetchAssets(with: .image, options: options)
assets = result.objects(at: IndexSet(0..<result.count))
}
func expandLimitedAccess(from viewController: UIViewController) {
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController)
}
}
struct PhotoGalleryView: View { @StateObject private var viewModel = PhotoGalleryViewModel()
var body: some View {
Group {
switch viewModel.authorizationStatus {
case .authorized, .limited:
PhotoGridView(assets: viewModel.assets)
case .denied, .restricted:
PermissionDeniedView()
case .notDetermined:
RequestAccessView {
Task { await viewModel.requestAccess() }
}
@unknown default:
EmptyView()
}
}
.task {
await viewModel.requestAccess()
}
}
}
Resources
Docs: /photosui/phpickerviewcontroller, /photosui/photospicker, /photos/phphotolibrary, /photos/phasset, /photos/phimagemanager
Skills: axiom-photo-library, axiom-camera-capture