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