Reference — PHPickerViewController, PHPickerConfiguration, PhotosPicker, PhotosPickerItem, Transferable, PHPhotoLibrary, PHAsset, PHAssetCreationRequest, PHFetchResult, PHAuthorizationStatus, limited library APIs
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
skills listSkill Instructions
name: axiom-photo-library-ref description: Reference — PHPickerViewController, PHPickerConfiguration, PhotosPicker, PhotosPickerItem, Transferable, PHPhotoLibrary, PHAsset, PHAssetCreationRequest, PHFetchResult, PHAuthorizationStatus, limited library APIs skill_type: reference version: 1.0.0
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
More by CharlesWiltgen
View allUse when porting OpenGL/DirectX to Metal - translation layer vs native rewrite decisions, migration planning, anti-patterns
Use when UI is slow, scrolling lags, animations stutter, or when asking 'why is my SwiftUI view slow', 'how do I optimize List performance', 'my app drops frames', 'view body is called too often', 'List is laggy' - SwiftUI performance optimization with Instruments 26 and WWDC 2025 patterns
Use for Core Location troubleshooting - no location updates, background location broken, authorization denied, geofence not triggering
Reference — Comprehensive SwiftUI navigation guide covering NavigationStack (iOS 16+), NavigationSplitView (iOS 16+), NavigationPath, deep linking, state restoration, Tab+Navigation integration (iOS 18+), Liquid Glass navigation (iOS 26+), and coordinator patterns