Reference — PHPickerViewController, PHPickerConfiguration, PhotosPicker, PhotosPickerItem, Transferable, PHPhotoLibrary, PHAsset, PHAssetCreationRequest, PHFetchResult, PHAuthorizationStatus, limited library APIs
/plugin marketplace add CharlesWiltgen/Axiom/plugin install axiom@axiom-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
// 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)
System photo picker for UIKit apps. No permission required.
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
// 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])
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
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
}
}
}
}
| Property | Type | Description |
|---|---|---|
itemProvider | NSItemProvider | Provides selected asset data |
assetIdentifier | String? | PHAsset identifier (if using photoLibrary config) |
SwiftUI view for photo selection. No permission required.
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")
}
// 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)])
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+) |
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 |
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 |
// 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
}
| Property | Type | Description |
|---|---|---|
itemIdentifier | String | Unique identifier |
supportedContentTypes | [UTType] | Available representations |
// 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
Access and modify the photo library.
// Check current status
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
// Request authorization
let newStatus = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
| 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+) |
// Read and write
PHPhotoLibrary.requestAuthorization(for: .readWrite)
// Add only (save photos, no reading)
PHPhotoLibrary.requestAuthorization(for: .addOnly)
// 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
}
// Async changes
try await PHPhotoLibrary.shared().performChanges {
// Create, update, or delete assets
}
// With completion handler
PHPhotoLibrary.shared().performChanges({
// Changes
}) { success, error in
// Handle result
}
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
}
}
}
Represents an asset in the photo library.
// 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)
| 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 |
| Type | Value |
|---|---|
.unknown | 0 |
.image | 1 |
.video | 2 |
.audio | 3 |
| 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 |
Create new assets in the photo library.
try await PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAsset(from: image)
}
try await PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAssetFromImage(atFileURL: imageURL)
}
// For video
try await PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
}
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)
}
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 |
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
}
Ordered list of assets from a fetch.
| Property | Type | Description |
|---|---|---|
count | Int | Number of items |
firstObject | T? | First item |
lastObject | T? | Last item |
// 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)
Request images from assets.
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
}
}
| 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+) |
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)
| Mode | Description |
|---|---|
.opportunistic | Fast thumbnail, then high quality |
.highQualityFormat | Only high quality |
.fastFormat | Only fast/degraded |
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 { ... }
}
Represents changes to the photo library.
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()
}
}
}
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()
}
}
}
Docs: /photosui/phpickerviewcontroller, /photosui/photospicker, /photos/phphotolibrary, /photos/phasset, /photos/phimagemanager
Skills: axiom-photo-library, axiom-camera-capture
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.