axiom-photo-library by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-photo-library指导您通过注重隐私的模式进行照片选择、处理有限照片库访问以及将照片保存到相机胶卷。
当您需要时使用:
"如何在 SwiftUI 中让用户选择照片?" "用户说他们看不到自己的照片" "如何将照片保存到相机胶卷?" "PHPicker 和 PhotosPicker 有什么区别?" "如何处理有限的照片访问权限?" "用户授予了有限访问权限但看不到照片" "如何从 PhotosPickerItem 加载图像?"
表明您把事情复杂化的迹象:
.limited 授权状态(用户无法扩展选择范围).authorized(可能是 .limited)在实现照片库功能之前:
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
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 级别 | 允许的操作 | 请求方法 |
|---|---|---|
| 无权限 | 用户通过系统选择器选择 | PHPicker/PhotosPicker(自动) |
.addOnly | 仅保存到相机胶卷 | requestAuthorization(for: .addOnly) |
.limited | 仅用户选择的子集 | 用户在系统 UI 中选择 |
.authorized | 完整的库访问权限 | requestAuthorization(for: .readWrite) |
关键见解:PHPicker 和 PhotosPicker 不需要任何权限。系统处理隐私问题。
<!-- 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>
使用场景:在 SwiftUI 应用中让用户选择照片。
import SwiftUI
import PhotosUI
struct ContentView: View {
@State private var selectedItem: PhotosPickerItem?
@State private var selectedImage: Image?
var body: some View {
VStack {
PhotosPicker(
selection: $selectedItem,
matching: .images // Filter to images only
) {
Label("Select Photo", systemImage: "photo")
}
if let image = selectedImage {
image
.resizable()
.scaledToFit()
}
}
.onChange(of: selectedItem) { _, newItem in
Task {
await loadImage(from: newItem)
}
}
}
private func loadImage(from item: PhotosPickerItem?) async {
guard let item else {
selectedImage = nil
return
}
// Load as Data first (more reliable than Image)
if let data = try? await item.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
selectedImage = Image(uiImage: uiImage)
}
}
}
多选:
@State private var selectedItems: [PhotosPickerItem] = []
PhotosPicker(
selection: $selectedItems,
maxSelectionCount: 5,
matching: .images
) {
Text("Select Photos")
}
// 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]))])
成本:15 分钟实现,无需权限
使用场景:将选择器内嵌在您的 UI 中,而不是以表单形式呈现。
import SwiftUI
import PhotosUI
struct EmbeddedPickerView: View {
@State private var selectedItems: [PhotosPickerItem] = []
var body: some View {
VStack {
// Your content above picker
SelectedPhotosGrid(items: selectedItems)
// Embedded picker fills available space
PhotosPicker(
selection: $selectedItems,
maxSelectionCount: 10,
selectionBehavior: .continuous, // Live updates as user taps
matching: .images
) {
// Label is ignored for inline style
Text("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
}
}
}
选择器样式:
| 样式 | 描述 |
|---|---|
.presentation | 默认模态表单 |
.inline | 嵌入到您的视图层次结构中 |
.compact | 单行,占用最小垂直空间 |
自定义修饰符:
// 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
隐私说明:嵌入式选择器首次出现时,iOS 会显示一个引导界面,说明您的应用只能访问选定的照片。隐私徽章指示选择器是进程外的。
使用场景:在 UIKit 应用中进行照片选择。
import PhotosUI
class PhotoPickerViewController: UIViewController, PHPickerViewControllerDelegate {
func showPicker() {
var config = PHPickerConfiguration()
config.selectionLimit = 1 // 0 = unlimited
config.filter = .images // or .videos, .any(of: [.images, .videos])
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let result = results.first else { return }
// Load image asynchronously
result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] object, error in
guard let image = object as? UIImage else { return }
DispatchQueue.main.async {
self?.displayImage(image)
}
}
}
}
过滤器选项:
// Images only
config.filter = .images
// Videos only
config.filter = .videos
// Live Photos only
config.filter = .livePhotos
// Images and videos
config.filter = .any(of: [.images, .videos])
// Exclude screenshots (iOS 15+)
config.filter = .all(of: [.images, .not(.screenshots)])
// iOS 16+ filters
config.filter = .cinematicVideos
config.filter = .depthEffectPhotos
config.filter = .bursts
// Configure for embedded use
var 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)
在显示时更新选择器 (iOS 17+):
// Deselect assets by their identifiers
picker.deselectAssets(withIdentifiers: ["assetID1", "assetID2"])
// Reorder assets in selection
picker.moveAsset(withIdentifier: "assetID", afterAssetWithIdentifier: "otherID")
成本:20 分钟实现,无需权限
选择器现在显示一个选项菜单,让用户选择从照片中剥离位置元数据。这可以与 PhotosPicker 和 PHPicker 自动配合使用。
保留 HDR 内容:
默认情况下,选择器可能会转码为 JPEG,丢失 HDR 数据。要接收原始格式:
// SwiftUI - Use .current encoding to preserve HDR
PhotosPicker(
selection: $selectedItems,
matching: .images,
preferredItemEncoding: .current // Don't transcode
) { ... }
// Loading with original format preservation
struct HDRImage: Transferable {
let data: Data
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(importedContentType: .image) { data in
HDRImage(data: data)
}
}
}
// Request .image content type (generic) not .jpeg (specific)
let result = try await item.loadTransferable(type: HDRImage.self)
UIKit 等效:
var config = PHPickerConfiguration()
config.preferredAssetRepresentationMode = .current // Don't transcode
电影模式视频:选择器返回带有深度效果烘焙的渲染版本。要获取带有决策点的原始版本,请改用具有库访问权限的 PhotoKit。
使用场景:用户授予了有限的访问权限;让他们添加更多照片。
抑制自动提示 (iOS 14+):
默认情况下,当检测到 .limited 时,iOS 会显示"选择更多照片"提示。要自行处理:
<!-- Info.plist - Add this to handle limited access UI yourself -->
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
<true/>
手动有限访问处理:
import Photos
class PhotoLibraryManager {
func checkAndRequestAccess() async -> PHAuthorizationStatus {
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .notDetermined:
return await PHPhotoLibrary.requestAuthorization(for: .readWrite)
case .limited:
// User granted limited access - show UI to expand
await presentLimitedLibraryPicker()
return .limited
case .authorized:
return .authorized
case .denied, .restricted:
return status
@unknown default:
return status
}
}
@MainActor
func presentLimitedLibraryPicker() {
guard let windowScene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else {
return
}
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: rootVC)
}
}
观察有限选择变化:
// Register for changes
PHPhotoLibrary.shared().register(self)
// In delegate
func photoLibraryDidChange(_ changeInstance: PHChange) {
// User may have modified their limited selection
// Refresh your photo grid
}
成本:30 分钟实现
使用场景:保存拍摄或编辑的照片。
import Photos
func saveImageToLibrary(_ image: UIImage) async throws {
// Request add-only permission (minimal access)
let status = await PHPhotoLibrary.requestAuthorization(for: .addOnly)
guard status == .authorized || status == .limited else {
throw PhotoError.permissionDenied
}
try await PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAsset(from: image)
}
}
// With metadata preservation
func savePhotoData(_ data: Data, metadata: [String: Any]? = nil) async throws {
try await PHPhotoLibrary.shared().performChanges {
let request = PHAssetCreationRequest.forAsset()
// Write data to temp file for addResource
let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("jpg")
try? data.write(to: tempURL)
request.addResource(with: .photo, fileURL: tempURL, options: nil)
}
}
成本:15 分钟实现
使用场景:正确处理异步图像加载和错误处理。
问题:默认的 Image Transferable 仅支持 PNG。大多数照片是 JPEG/HEIF。
// Custom Transferable for any image format
struct TransferableImage: 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 TransferableImage(image: image)
}
}
enum TransferError: Error {
case importFailed
}
}
// Usage
func loadImage(from item: PhotosPickerItem) async -> UIImage? {
do {
let result = try await item.loadTransferable(type: TransferableImage.self)
return result?.image
} catch {
print("Failed to load image: \(error)")
return nil
}
}
带进度加载:
func loadImageWithProgress(from item: PhotosPickerItem) async -> UIImage? {
let progress = Progress()
return await withCheckedContinuation { continuation in
_ = item.loadTransferable(type: TransferableImage.self) { result in
switch result {
case .success(let transferable):
continuation.resume(returning: transferable?.image)
case .failure:
continuation.resume(returning: nil)
}
}
}
}
成本:20 分钟实现
使用场景:保持您的图库 UI 与 Photos 应用同步。
import Photos
class PhotoGalleryViewModel: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
@Published var photos: [PHAsset] = []
private var fetchResult: PHFetchResult<PHAsset>?
override init() {
super.init()
PHPhotoLibrary.shared().register(self)
fetchPhotos()
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
func fetchPhotos() {
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchResult = PHAsset.fetchAssets(with: .image, options: options)
photos = fetchResult?.objects(at: IndexSet(0..<(fetchResult?.count ?? 0))) ?? []
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
guard let fetchResult = fetchResult,
let changes = changeInstance.changeDetails(for: fetchResult) else {
return
}
DispatchQueue.main.async {
self.fetchResult = changes.fetchResultAfterChanges
self.photos = changes.fetchResultAfterChanges.objects(at:
IndexSet(0..<changes.fetchResultAfterChanges.count)
)
}
}
}
成本:30 分钟实现
错误:
// Over-requesting - picker doesn't need this!
let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
if status == .authorized {
showPhotoPicker()
}
正确:
// Just show the picker - no permission needed
PhotosPicker(selection: $item, matching: .images) {
Text("Select Photo")
}
为什么重要:PHPicker 和 PhotosPicker 自动处理隐私问题。当您只需要选择照片时请求库访问权限是侵犯隐私的行为,可能导致 App Store 审核拒绝。
错误:
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
if status == .authorized {
showGallery()
} else {
showPermissionDenied() // Wrong! .limited is valid
}
正确:
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .authorized:
showGallery()
case .limited:
showGallery() // Works with limited selection
showLimitedBanner() // Explain to user
case .denied, .restricted:
showPermissionDenied()
case .notDetermined:
requestAccess()
@unknown default:
break
}
为什么重要:iOS 14+ 用户可以授予有限的访问权限。将其视为拒绝会让用户感到沮丧。
错误:
// Blocks UI thread
let data = try! selectedItem.loadTransferable(type: Data.self)
正确:
Task {
if let data = try? await selectedItem.loadTransferable(type: Data.self) {
// Use data
}
}
为什么重要:大照片(RAW、全景图)需要几秒钟来加载。阻塞 UI 会导致 ANR。
错误:
let picker = UIImagePickerController()
picker.sourceType = .photoLibrary
present(picker, animated: true)
正确:
var config = PHPickerConfiguration()
config.filter = .images
let picker = PHPickerViewController(configuration: config)
present(picker, animated: true)
为什么重要:UIImagePickerController 已弃用用于照片选择。PHPicker 更可靠,处理大资源文件,并提供更好的隐私保护。
背景:产品需要照片导入功能。您正在考虑"为了安全起见"请求完整的库访问权限。
压力:"用户只会点击允许。"
现实:自 iOS 14 起,用户可以授予有限的访问权限。完全访问权限请求会触发额外的隐私提示。App Store 审核可能会拒绝不必要的权限请求。
正确操作:
反驳模板:"PHPicker 无需任何权限请求即可工作 - 用户可以直接选择照片。当我们只需要选择功能时请求库访问权限是侵犯隐私的行为,App Store 审核可能会标记。"
背景:支持工单关于"没有可用照片",即使用户已授予访问权限。
压力:"只需再次请求完全访问权限。"
现实:用户可能授予了 .limited 访问权限,并且最初选择了 0 张照片。
正确操作:
.limited 状态presentLimitedLibraryPicker() 让用户添加照片反驳模板:"用户拥有有限的访问权限 - 他们需要扩展选择范围。我将添加一个按钮,打开有限的库选择器,以便他们可以添加更多照片。"
背景:用户抱怨照片选择器显示所选图像很慢。
压力:"你能缓存或预加载吗?"
现实:大照片(RAW、全景图、Live Photos)解码速度慢。解决方案是 UX,而不是缓存。
正确操作:
反驳模板:"大照片需要时间加载 - 这是物理限制。我将立即显示占位符并逐步加载。对于选择器 UI,缩略图加载已经由系统优化。"
在发布照片库功能之前:
权限策略:
有限库:
.limited 状态(不视为拒绝)presentLimitedLibraryPicker() 让用户添加照片图像加载:
保存照片:
照片库变化:
WWDC:2020-10652, 2020-10641, 2022-10023, 2023-10107
文档:/photosui/phpickerviewcontroller, /photosui/photospicker, /photos/phphotolibrary
技能:axiom-photo-library-ref, axiom-camera-capture
每周安装数
91
仓库
GitHub 星标数
590
首次出现
2026年1月21日
安全审计
安装于
opencode76
codex71
claude-code70
gemini-cli69
cursor68
github-copilot66
Guides you through photo picking, limited library handling, and saving photos to the camera roll using privacy-forward patterns.
Use when you need to:
"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?"
Signs you're making this harder than it needs to be:
.limited authorization status (users can't expand selection).authorized after user grants access (could be .limited)Before implementing photo library features:
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
| 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.
<!-- 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>
Use case : Let users select photos in a SwiftUI app.
import SwiftUI
import PhotosUI
struct ContentView: View {
@State private var selectedItem: PhotosPickerItem?
@State private var selectedImage: Image?
var body: some View {
VStack {
PhotosPicker(
selection: $selectedItem,
matching: .images // Filter to images only
) {
Label("Select Photo", systemImage: "photo")
}
if let image = selectedImage {
image
.resizable()
.scaledToFit()
}
}
.onChange(of: selectedItem) { _, newItem in
Task {
await loadImage(from: newItem)
}
}
}
private func loadImage(from item: PhotosPickerItem?) async {
guard let item else {
selectedImage = nil
return
}
// Load as Data first (more reliable than Image)
if let data = try? await item.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
selectedImage = Image(uiImage: uiImage)
}
}
}
Multi-selection :
@State private var selectedItems: [PhotosPickerItem] = []
PhotosPicker(
selection: $selectedItems,
maxSelectionCount: 5,
matching: .images
) {
Text("Select Photos")
}
// 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
Use case : Embed picker inline in your UI instead of presenting as sheet.
import SwiftUI
import PhotosUI
struct EmbeddedPickerView: View {
@State private var selectedItems: [PhotosPickerItem] = []
var body: some View {
VStack {
// Your content above picker
SelectedPhotosGrid(items: selectedItems)
// Embedded picker fills available space
PhotosPicker(
selection: $selectedItems,
maxSelectionCount: 10,
selectionBehavior: .continuous, // Live updates as user taps
matching: .images
) {
// Label is ignored for inline style
Text("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.
Use case : Photo selection in UIKit apps.
import PhotosUI
class PhotoPickerViewController: UIViewController, PHPickerViewControllerDelegate {
func showPicker() {
var config = PHPickerConfiguration()
config.selectionLimit = 1 // 0 = unlimited
config.filter = .images // or .videos, .any(of: [.images, .videos])
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let result = results.first else { return }
// Load image asynchronously
result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] object, error in
guard let image = object as? UIImage else { return }
DispatchQueue.main.async {
self?.displayImage(image)
}
}
}
}
Filter options :
// Images only
config.filter = .images
// Videos only
config.filter = .videos
// Live Photos only
config.filter = .livePhotos
// Images and videos
config.filter = .any(of: [.images, .videos])
// Exclude screenshots (iOS 15+)
config.filter = .all(of: [.images, .not(.screenshots)])
// iOS 16+ filters
config.filter = .cinematicVideos
config.filter = .depthEffectPhotos
config.filter = .bursts
// Configure for embedded use
var 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
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 HDR
PhotosPicker(
selection: $selectedItems,
matching: .images,
preferredItemEncoding: .current // Don't transcode
) { ... }
// Loading with original format preservation
struct HDRImage: Transferable {
let data: Data
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(importedContentType: .image) { data in
HDRImage(data: data)
}
}
}
// Request .image content type (generic) not .jpeg (specific)
let result = try await 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.
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
class PhotoLibraryManager {
func checkAndRequestAccess() async -> PHAuthorizationStatus {
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .notDetermined:
return await PHPhotoLibrary.requestAuthorization(for: .readWrite)
case .limited:
// User granted limited access - show UI to expand
await presentLimitedLibraryPicker()
return .limited
case .authorized:
return .authorized
case .denied, .restricted:
return status
@unknown default:
return status
}
}
@MainActor
func presentLimitedLibraryPicker() {
guard let 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 changes
PHPhotoLibrary.shared().register(self)
// In delegate
func photoLibraryDidChange(_ changeInstance: PHChange) {
// User may have modified their limited selection
// Refresh your photo grid
}
Cost : 30 min implementation
Use case : Save captured or edited photos.
import Photos
func saveImageToLibrary(_ image: UIImage) async throws {
// Request add-only permission (minimal access)
let status = await PHPhotoLibrary.requestAuthorization(for: .addOnly)
guard status == .authorized || status == .limited else {
throw PhotoError.permissionDenied
}
try await PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAsset(from: image)
}
}
// With metadata preservation
func savePhotoData(_ data: Data, metadata: [String: Any]? = nil) async throws {
try await PHPhotoLibrary.shared().performChanges {
let request = PHAssetCreationRequest.forAsset()
// Write data to temp file for addResource
let 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
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 format
struct TransferableImage: 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 TransferableImage(image: image)
}
}
enum TransferError: Error {
case importFailed
}
}
// Usage
func loadImage(from item: PhotosPickerItem) async -> UIImage? {
do {
let result = try await item.loadTransferable(type: TransferableImage.self)
return result?.image
} catch {
print("Failed to load image: \(error)")
return nil
}
}
Loading with progress :
func loadImageWithProgress(from item: PhotosPickerItem) async -> UIImage? {
let progress = Progress()
return await withCheckedContinuation { continuation in
_ = item.loadTransferable(type: TransferableImage.self) { result in
switch result {
case .success(let transferable):
continuation.resume(returning: transferable?.image)
case .failure:
continuation.resume(returning: nil)
}
}
}
}
Cost : 20 min implementation
Use case : Keep your gallery UI in sync with Photos app.
import Photos
class PhotoGalleryViewModel: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
@Published var photos: [PHAsset] = []
private var fetchResult: PHFetchResult<PHAsset>?
override init() {
super.init()
PHPhotoLibrary.shared().register(self)
fetchPhotos()
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
func fetchPhotos() {
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchResult = PHAsset.fetchAssets(with: .image, options: options)
photos = fetchResult?.objects(at: IndexSet(0..<(fetchResult?.count ?? 0))) ?? []
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
guard let fetchResult = fetchResult,
let changes = changeInstance.changeDetails(for: fetchResult) else {
return
}
DispatchQueue.main.async {
self.fetchResult = changes.fetchResultAfterChanges
self.photos = changes.fetchResultAfterChanges.objects(at:
IndexSet(0..<changes.fetchResultAfterChanges.count)
)
}
}
}
Cost : 30 min implementation
Wrong :
// Over-requesting - picker doesn't need this!
let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
if status == .authorized {
showPhotoPicker()
}
Right :
// Just show the picker - no permission needed
PhotosPicker(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.
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 user
case .denied, .restricted:
showPermissionDenied()
case .notDetermined:
requestAccess()
@unknown default:
break
}
Why it matters : iOS 14+ users can grant limited access. Treating it as denied frustrates users.
Wrong :
// Blocks UI thread
let data = try! selectedItem.loadTransferable(type: Data.self)
Right :
Task {
if let 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.
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.
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 :
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."
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 :
.limited statuspresentLimitedLibraryPicker() to let user add photosPush-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."
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 :
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."
Before shipping photo library features:
Permission Strategy :
Limited Library :
.limited status (not treating as denied)presentLimitedLibraryPicker() for users to add photosImage Loading :
Saving Photos :
Photo Library Changes :
WWDC : 2020-10652, 2020-10641, 2022-10023, 2023-10107
Docs : /photosui/phpickerviewcontroller, /photosui/photospicker, /photos/phphotolibrary
Skills : axiom-photo-library-ref, axiom-camera-capture
Weekly Installs
91
Repository
GitHub Stars
590
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode76
codex71
claude-code70
gemini-cli69
cursor68
github-copilot66