---
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

```swift
// 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

```swift
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

```swift
// 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

```swift
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
```

### Delegate

```swift
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

```swift
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

```swift
// 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

```swift
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+)

```swift
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+)

```swift
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

```swift
// 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

```swift
// 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

```swift
// 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

```swift
// Read and write
PHPhotoLibrary.requestAuthorization(for: .readWrite)

// Add only (save photos, no reading)
PHPhotoLibrary.requestAuthorization(for: .addOnly)
```

### Limited Library Picker

```swift
// 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

```swift
// 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

```swift
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

```swift
// 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

```swift
try await PHPhotoLibrary.shared().performChanges {
    PHAssetCreationRequest.creationRequestForAsset(from: image)
}
```

### Creating from File URL

```swift
try await PHPhotoLibrary.shared().performChanges {
    PHAssetCreationRequest.creationRequestForAssetFromImage(atFileURL: imageURL)
}

// For video
try await PHPhotoLibrary.shared().performChanges {
    PHAssetCreationRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
}
```

### Creating with Resources

```swift
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:

```swift
// 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

```swift
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
}
```

### Custom Albums

```swift
// Create a custom album
func getOrCreateAlbum(named title: String) async throws -> PHAssetCollection {
    // Check if album already exists
    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 }

    // Create new 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
}

// Save photo to custom album
func saveToAlbum(_ image: UIImage, album: PHAssetCollection) async throws {
    try await PHPhotoLibrary.shared().performChanges {
        let assetRequest = PHAssetCreationRequest.creationRequestForAsset(from: image)
        guard let placeholder = assetRequest.placeholderForCreatedAsset,
              let albumRequest = PHAssetCollectionChangeRequest(for: album) else { return }
        albumRequest.addAssets([placeholder] as NSFastEnumeration)
    }
}
```

---

## 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

```swift
// 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

```swift
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:

```swift
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

```swift
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

```swift
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

```swift
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
