| name | axiom-photo-library-ref |
| description | Reference — PHPickerViewController, PHPickerConfiguration, PhotosPicker, PhotosPickerItem, Transferable, PHPhotoLibrary, PHAsset, PHAssetCreationRequest, PHFetchResult, PHAuthorizationStatus, limited library APIs |
| license | MIT |
| metadata | {"version":"1.0.0"} |
Photo Library API Reference
Quick Reference
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) {
}
}
}
var config = PHPickerConfiguration()
config.selectionLimit = 1
config.filter = .images
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
try await PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAsset(from: image)
}
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
PHPickerViewController (iOS 14+)
System photo picker for UIKit apps. No permission required.
Configuration
import PhotosUI
var config = PHPickerConfiguration()
config.selectionLimit = 5
config.filter = .images
config = PHPickerConfiguration(photoLibrary: .shared())
config.preferredAssetRepresentationMode = .automatic
Filter Options
PHPickerFilter.images
PHPickerFilter.videos
PHPickerFilter.livePhotos
PHPickerFilter.any(of: [.images, .videos])
PHPickerFilter.all(of: [.images, .not(.screenshots)])
PHPickerFilter.not(.livePhotos)
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 {
let identifier = result.assetIdentifier
result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
guard let image = object as? UIImage else { return }
DispatchQueue.main.async {
self.displayImage(image)
}
}
result.itemProvider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in
guard let data else { return }
}
result.itemProvider.loadObject(ofClass: PHLivePhoto.self) { object, error in
guard let livePhoto object { }
}
}
}
}
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
@State private var selectedItem: PhotosPickerItem?
PhotosPicker(selection: $selectedItem, matching: .images) {
Label("Select Photo", systemImage: "photo")
}
@State private var selectedItems: [PhotosPickerItem] = []
PhotosPicker(
selection: $selectedItems,
maxSelectionCount: 5,
matching: .images
) {
Text("Select Photos")
}
Filters
matching: .images
matching: .videos
matching: .any(of: [.images, .videos])
matching: .livePhotos
matching: .all(of: [.images, .not(.screenshots)])
Selection Behavior
PhotosPicker(
selection: $items,
maxSelectionCount: 10,
selectionBehavior: .ordered,
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)
.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
) { ... }
| Encoding | Description |
|---|
.automatic | System decides format |
.current | Original format, preserves HDR |
.compatible | Force compatible format |
Loading Images from PhotosPickerItem
if let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) {
}
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)
}
}
}
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
func loadTransferable<T: Transferable>(type: T.Type) async throws -> T?
func loadTransferable<T: Transferable>(
type: T.Type,
completionHandler: @escaping (Result<T?, Error>) -> Void
) -> Progress
PHPhotoLibrary
Access and modify the photo library.
Authorization Status
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
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
PHPhotoLibrary.requestAuthorization(for: .readWrite)
PHPhotoLibrary.requestAuthorization(for: .addOnly)
Limited Library Picker
@MainActor
func presentLimitedLibraryPicker() {
guard let viewController = UIApplication.shared.keyWindow?.rootViewController else { return }
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController)
}
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController) { identifiers in
}
Performing Changes
try await PHPhotoLibrary.shared().performChanges {
}
PHPhotoLibrary.shared().performChanges({
}) { success, error in
}
Change Observer
class PhotoObserver: NSObject, PHPhotoLibraryChangeObserver {
override init() {
super.init()
PHPhotoLibrary.shared().register(self)
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
guard let changes = changeInstance.changeDetails(for: fetchResult) else { return }
DispatchQueue.main.async {
let newResult = changes.fetchResultAfterChanges
}
}
}
PHAsset
Represents an asset in the photo library.
Fetching Assets
let allPhotos = PHAsset.fetchAssets(with: .image, options: nil)
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)
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)
}
try await PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
}
Creating with Resources
try await PHPhotoLibrary.shared().performChanges {
let request = PHAssetCreationRequest.forAsset()
let options = PHAssetResourceCreationOptions()
options.shouldMoveFile = true
request.addResource(with: .photo, fileURL: photoURL, options: options)
request.creationDate = Date()
request.location = CLLocation(latitude: 37.7749, longitude: -122.4194)
}
Deferred Photo Proxy (iOS 17+)
Save camera proxy photos for background processing:
try await PHPhotoLibrary.shared().performChanges {
let request = PHAssetCreationRequest.forAsset()
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)
let placeholder = request.placeholderForCreatedAsset
}
Custom Albums
func getOrCreateAlbum(named title: String) async throws -> PHAssetCollection {
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %@", title)
let existing = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
if let album = existing.firstObject { return album }
var placeholder: PHObjectPlaceholder?
try await PHPhotoLibrary.shared().performChanges {
let request = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: title)
placeholder = request.placeholderForCreatedAssetCollection
}
guard let id = placeholder?.localIdentifier,
let album = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [id], options: nil).firstObject
else { throw PhotoError.albumCreationFailed }
return album
}
( : , : ) {
.shared().performChanges {
assetRequest .creationRequestForAsset(from: image)
placeholder assetRequest.placeholderForCreatedAsset,
albumRequest (for: album) { }
albumRequest.addAssets([placeholder] )
}
}
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
let asset = fetchResult.object(at: 0)
let asset = fetchResult[0]
let assets = fetchResult.objects(at: IndexSet(0..<10))
fetchResult.enumerateObjects { asset, index, stop in
if shouldStop {
stop.pointee = true
}
}
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
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 }
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool) ?? false
if !isDegraded {
}
}
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
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 }
}
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 }
let hasIncrementalChanges = changes.hasIncrementalChanges
let insertedIndexes = changes.insertedIndexes
let removedIndexes = changes.removedIndexes
let changedIndexes = changes.changedIndexes
let newResult = changes.fetchResultAfterChanges
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) })
}
}
} {
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)
}
}
: {
viewModel ()
body: {
{
viewModel.authorizationStatus {
.authorized, .limited:
(assets: viewModel.assets)
.denied, .restricted:
()
.notDetermined:
{
{ viewModel.requestAccess() }
}
:
()
}
}
.task {
viewModel.requestAccess()
}
}
}
Resources
Docs: /photosui/phpickerviewcontroller, /photosui/photospicker, /photos/phphotolibrary, /photos/phasset, /photos/phimagemanager
Skills: axiom-photo-library, axiom-camera-capture