Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
PHPicker, PhotosPicker, photo selection, limited library access, presentLimitedLibraryPicker, save to camera roll, PHPhotoLibrary, PHAssetCreationRequest, Transferable, PhotosPickerItem, photo permissions
license
MIT
compatibility
iOS 14+, iPadOS 14+, macOS 13+
metadata
{"version":"1.0.0","last-updated":"2026-01-03"}
Photo Library Access with PhotoKit
Guides you through photo picking, limited library handling, and saving photos to the camera roll using privacy-forward patterns.
When to Use This Skill
Use when you need to:
☑ Let users select photos from their library
☑ Handle limited photo library access
☑ Save photos/videos to the camera roll
☑ Choose between PHPicker and PhotosPicker
☑ Load images from PhotosPickerItem
☑ Observe photo library changes
☑ Request appropriate permission level
Example Prompts
"How do I let users pick photos in SwiftUI?"
"User says they can't see their photos"
"How do I save a photo to the camera roll?"
"What's the difference between PHPicker and PhotosPicker?"
"How do I handle limited photo access?"
"User granted limited access but can't see photos"
"How do I load an image from PhotosPickerItem?"
Red Flags
Signs you're making this harder than it needs to be:
❌ Using UIImagePickerController (deprecated for photo selection)
❌ Requesting full library access when picker suffices (privacy violation)
❌ Ignoring .limited authorization status (users can't expand selection)
❌ Not handling Transferable loading failures (crashes on large photos)
❌ Synchronously loading images from picker results (blocks UI)
❌ Using PhotoKit APIs when you only need to pick photos (over-engineering)
❌ Assuming .authorized after user grants access (could be .limited)
Mandatory First Steps
Before implementing photo library features:
1. Choose Your Approach
What do you need?
┌─ User picks photos (no library browsing)?
│ ├─ SwiftUI app → PhotosPicker (iOS 16+)
│ └─ UIKit app → PHPickerViewController (iOS 14+)
│ └─ NO library permission needed! Picker handles it.
│
├─ Display user's full photo library (gallery UI)?
│ └─ Requires PHPhotoLibrary authorization
│ └─ Request .readWrite for browsing
│ └─ Handle .limited status with presentLimitedLibraryPicker
│
├─ Save photos to camera roll?
│ └─ Requires PHPhotoLibrary authorization
│ └─ Request .addOnly (minimal) or .readWrite
│
└─ Just capture with camera?
└─ Don't use PhotoKit - see camera-capture skill
2. Understand Permission Levels
Level
What It Allows
Request Method
No permission
User picks via system picker
PHPicker/PhotosPicker (automatic)
.addOnly
Save to camera roll only
requestAuthorization(for: .addOnly)
.limited
User-selected subset only
User chooses in system UI
.authorized
Full library access
requestAuthorization(for: .readWrite)
Key insight: PHPicker and PhotosPicker require NO permission. The system handles privacy.
3. Info.plist Keys
<!-- Required for any PhotoKit access --><key>NSPhotoLibraryUsageDescription</key><string>Access your photos to share them</string><!-- Required if saving photos --><key>NSPhotoLibraryAddUsageDescription</key><string>Save photos to your library</string>
Core Patterns
Pattern 1: SwiftUI PhotosPicker (iOS 16+)
Use case: Let users select photos in a SwiftUI app.
// Screenshots only
matching: .screenshots
// Screen recordings only
matching: .screenRecordings
// Slo-mo videos
matching: .sloMoVideos
// Cinematic videos (iOS 16+)
matching: .cinematicVideos
// Depth effect photos
matching: .depthEffectPhotos
// Bursts
matching: .bursts
// Compound filters with .any, .all, .not// Videos AND Live Photos
matching: .any(of: [.videos, .livePhotos])
// All images EXCEPT screenshots
matching: .all(of: [.images, .not(.screenshots)])
// All images EXCEPT screenshots AND panoramas
matching: .all(of: [.images, .not(.any(of: [.screenshots, .panoramas]))])
Cost: 15 min implementation, no permissions required
Pattern 1b: Embedded PhotosPicker (iOS 17+)
Use case: Embed picker inline in your UI instead of presenting as sheet.
import SwiftUI
import PhotosUI
structEmbeddedPickerView: View {
@Stateprivatevar selectedItems: [PhotosPickerItem] = []
var body: someView {
VStack {
// Your content above pickerSelectedPhotosGrid(items: selectedItems)
// Embedded picker fills available spacePhotosPicker(
selection: $selectedItems,
maxSelectionCount: 10,
selectionBehavior: .continuous, // Live updates as user taps
matching: .images
) {
// Label is ignored for inline styleText("Select")
}
.photosPickerStyle(.inline) // Embed instead of present
.photosPickerDisabledCapabilities([.selectionActions]) // Hide Add/Cancel buttons
.photosPickerAccessoryVisibility(.hidden, edges: .all) // Hide nav/toolbar
.frame(height: 300) // Control picker height
.ignoresSafeArea(.container, edges: .bottom) // Extend to bottom edge
}
}
}
Picker Styles:
Style
Description
.presentation
Default modal sheet
.inline
Embedded in your view hierarchy
.compact
Single row, minimal vertical space
Customization modifiers:
// Hide navigation/toolbar accessories
.photosPickerAccessoryVisibility(.hidden, edges: .all)
.photosPickerAccessoryVisibility(.hidden, edges: .top) // Just navigation bar
.photosPickerAccessoryVisibility(.hidden, edges: .bottom) // Just toolbar// Disable capabilities (hides UI for them)
.photosPickerDisabledCapabilities([.search]) // Hide search
.photosPickerDisabledCapabilities([.collectionNavigation]) // Hide albums
.photosPickerDisabledCapabilities([.stagingArea]) // Hide selection review
.photosPickerDisabledCapabilities([.selectionActions]) // Hide Add/Cancel// Continuous selection for live updates
selectionBehavior: .continuous
Privacy note: First time an embedded picker appears, iOS shows an onboarding UI explaining your app can only access selected photos. A privacy badge indicates the picker is out-of-process.
// Configure for embedded usevar config =PHPickerConfiguration()
config.selection = .continuous // Live updates instead of waiting for Add button
config.mode = .compact // Single row layout (optional)
config.selectionLimit =10// Hide accessories
config.edgesWithoutContentMargins = .all // No margins around picker// Disable capabilities
config.disabledCapabilities = [.search, .selectionActions]
let picker =PHPickerViewController(configuration: config)
picker.delegate =self// Add as child view controller (required for embedded)
addChild(picker)
containerView.addSubview(picker.view)
picker.view.frame = containerView.bounds
picker.didMove(toParent: self)
Updating picker while displayed (iOS 17+):
// Deselect assets by their identifiers
picker.deselectAssets(withIdentifiers: ["assetID1", "assetID2"])
// Reorder assets in selection
picker.moveAsset(withIdentifier: "assetID", afterAssetWithIdentifier: "otherID")
Cost: 20 min implementation, no permissions required
Pattern 2b: Options Menu & HDR Support (iOS 17+)
The picker now shows an Options menu letting users choose to strip location metadata from photos. This works automatically with PhotosPicker and PHPicker.
Preserving HDR content:
By default, picker may transcode to JPEG, losing HDR data. To receive original format:
// SwiftUI - Use .current encoding to preserve HDRPhotosPicker(
selection: $selectedItems,
matching: .images,
preferredItemEncoding: .current // Don't transcode
) { ... }
// Loading with original format preservationstructHDRImage: Transferable {
let data: Datastaticvar transferRepresentation: someTransferRepresentation {
DataRepresentation(importedContentType: .image) { data inHDRImage(data: data)
}
}
}
// Request .image content type (generic) not .jpeg (specific)let result =tryawait item.loadTransferable(type: HDRImage.self)
UIKit equivalent:
var config =PHPickerConfiguration()
config.preferredAssetRepresentationMode = .current // Don't transcode
Cinematic mode videos: Picker returns rendered version with depth effects baked in. To get original with decision points, use PhotoKit with library access instead.
Pattern 3: Handling Limited Library Access
Use case: User granted limited access; let them add more photos.
Suppressing automatic prompt (iOS 14+):
By default, iOS shows "Select More Photos" prompt when .limited is detected. To handle it yourself:
<!-- Info.plist - Add this to handle limited access UI yourself --><key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key><true/>
Manual limited access handling:
import Photos
classPhotoLibraryManager {
funccheckAndRequestAccess() async -> PHAuthorizationStatus {
let status =PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .notDetermined:
returnawaitPHPhotoLibrary.requestAuthorization(for: .readWrite)
case .limited:
// User granted limited access - show UI to expandawait presentLimitedLibraryPicker()
return .limited
case .authorized:
return .authorized
case .denied, .restricted:
return status
@unknowndefault:
return status
}
}
@MainActorfuncpresentLimitedLibraryPicker() {
guardlet windowScene =UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as?UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else {
return
}
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: rootVC)
}
}
Observe limited selection changes:
// Register for changesPHPhotoLibrary.shared().register(self)
// In delegatefuncphotoLibraryDidChange(_changeInstance: PHChange) {
// User may have modified their limited selection// Refresh your photo grid
}
Cost: 30 min implementation
Pattern 4: Saving Photos to Camera Roll
Use case: Save captured or edited photos.
import Photos
funcsaveImageToLibrary(_image: UIImage) asyncthrows {
// Request add-only permission (minimal access)let status =awaitPHPhotoLibrary.requestAuthorization(for: .addOnly)
guard status == .authorized || status == .limited else {
throwPhotoError.permissionDenied
}
tryawaitPHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAsset(from: image)
}
}
// With metadata preservationfuncsavePhotoData(_data: Data, metadata: [String: Any]?=nil) asyncthrows {
tryawaitPHPhotoLibrary.shared().performChanges {
let request =PHAssetCreationRequest.forAsset()
// Write data to temp file for addResourcelet tempURL =FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("jpg")
try? data.write(to: tempURL)
request.addResource(with: .photo, fileURL: tempURL, options: nil)
}
}
Cost: 15 min implementation
Pattern 5: Loading Images from PhotosPickerItem
Use case: Properly handle async image loading with error handling.
The problem: Default Image Transferable only supports PNG. Most photos are JPEG/HEIF.
// Custom Transferable for any image formatstructTransferableImage: Transferable {
let image: UIImagestaticvar transferRepresentation: someTransferRepresentation {
DataRepresentation(importedContentType: .image) { data inguardlet image =UIImage(data: data) else {
throwTransferError.importFailed
}
returnTransferableImage(image: image)
}
}
enumTransferError: Error {
case importFailed
}
}
// UsagefuncloadImage(fromitem: PhotosPickerItem) async -> UIImage? {
do {
let result =tryawait item.loadTransferable(type: TransferableImage.self)
return result?.image
} catch {
print("Failed to load image: \(error)")
returnnil
}
}
Loading with progress:
funcloadImageWithProgress(fromitem: PhotosPickerItem) async -> UIImage? {
let progress =Progress()
returnawait withCheckedContinuation { continuation in_= item.loadTransferable(type: TransferableImage.self) { result inswitch result {
case .success(let transferable):
continuation.resume(returning: transferable?.image)
case .failure:
continuation.resume(returning: nil)
}
}
}
}
Cost: 20 min implementation
Pattern 6: Observing Photo Library Changes
Use case: Keep your gallery UI in sync with Photos app.
Anti-Pattern 1: Requesting Full Access for Photo Picking
Wrong:
// Over-requesting - picker doesn't need this!let status =awaitPHPhotoLibrary.requestAuthorization(for: .readWrite)
if status == .authorized {
showPhotoPicker()
}
Right:
// Just show the picker - no permission neededPhotosPicker(selection: $item, matching: .images) {
Text("Select Photo")
}
Why it matters: PHPicker and PhotosPicker handle privacy automatically. Requesting library access when you only need to pick photos is a privacy violation and may cause App Store rejection.
Anti-Pattern 2: Ignoring Limited Status
Wrong:
let status =PHPhotoLibrary.authorizationStatus(for: .readWrite)
if status == .authorized {
showGallery()
} else {
showPermissionDenied() // Wrong! .limited is valid
}
Right:
let status =PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .authorized:
showGallery()
case .limited:
showGallery() // Works with limited selection
showLimitedBanner() // Explain to usercase .denied, .restricted:
showPermissionDenied()
case .notDetermined:
requestAccess()
@unknowndefault:
break
}
Why it matters: iOS 14+ users can grant limited access. Treating it as denied frustrates users.
Anti-Pattern 3: Synchronous Image Loading
Wrong:
// Blocks UI threadlet data =try! selectedItem.loadTransferable(type: Data.self)
Right:
Task {
iflet data =try?await selectedItem.loadTransferable(type: Data.self) {
// Use data
}
}
Why it matters: Large photos (RAW, panoramas) take seconds to load. Blocking UI causes ANR.
Anti-Pattern 4: Using UIImagePickerController for Photo Selection
Wrong:
let picker =UIImagePickerController()
picker.sourceType = .photoLibrary
present(picker, animated: true)
Right:
var config =PHPickerConfiguration()
config.filter = .images
let picker =PHPickerViewController(configuration: config)
present(picker, animated: true)
Why it matters: UIImagePickerController is deprecated for photo selection. PHPicker is more reliable, handles large assets, and provides better privacy.
Pressure Scenarios
Scenario 1: "Just Get Photo Access Working"
Context: Product wants photo import feature. You're considering requesting full library access "to be safe."
Pressure: "Users will just tap Allow anyway."
Reality: Since iOS 14, users can grant limited access. Full access request triggers additional privacy prompt. App Store Review may reject unnecessary permission requests.
Correct action:
Use PhotosPicker or PHPicker (no permission needed)
Only request .readWrite if building a gallery browser
Only request .addOnly if just saving photos
Push-back template: "PHPicker works without any permission request - users can select photos directly. Requesting library access when we only need picking is a privacy violation that App Store Review may flag."
Scenario 2: "Users Say They Can't See Their Photos"
Context: Support tickets about "no photos available" even though user granted access.
Pressure: "Just ask for full access again."
Reality: User likely granted .limited access and selected 0 photos initially.
Correct action:
Check for .limited status
Show presentLimitedLibraryPicker() to let user add photos
Explain in UI: "Tap here to add more photos"
Push-back template: "The user has limited access - they need to expand their selection. I'll add a button that opens the limited library picker so they can add more photos."
Scenario 3: "Photo Loads Taking Forever"
Context: Users complain photo picker is slow to display selected images.
Pressure: "Can you cache or preload somehow?"
Reality: Large photos (RAW, panoramas, Live Photos) are slow to decode. Solution is UX, not caching.
Correct action:
Show loading placeholder immediately
Load thumbnail first, full image second
Show progress indicator for large files
Use async/await to avoid blocking
Push-back template: "Large photos take time to load - that's physics. I'll show a placeholder immediately and load progressively. For the picker UI, thumbnail loading is already optimized by the system."
Checklist
Before shipping photo library features:
Permission Strategy:
☑ Using PHPicker/PhotosPicker for simple selection (no permission needed)
☑ Only requesting .readWrite if building gallery UI
☑ Only requesting .addOnly if only saving photos
☑ Info.plist usage descriptions present
Limited Library:
☑ Handling .limited status (not treating as denied)
☑ Offering presentLimitedLibraryPicker() for users to add photos
☑ UI explains limited access to users
Image Loading:
☑ All loading is async (no UI blocking)
☑ Custom Transferable handles JPEG/HEIF (not just PNG)
☑ Error handling for failed loads
☑ Loading indicator for large files
Saving Photos:
☑ Using .addOnly when full access not needed
☑ Using performChanges for atomic operations
☑ Handling save failures gracefully
Photo Library Changes:
☑ Registered as PHPhotoLibraryChangeObserver if displaying library