axiom-photo-library

安装量: 117
排名: #7289

安装

npx skills add https://github.com/charleswiltgen/axiom --skill axiom-photo-library
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

< key

NSPhotoLibraryUsageDescription </ key

< string

Access your photos to share them </ string

<
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.
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"
)
}
Advanced Filters (iOS 15+/16+)
// 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
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.
Pattern 2: UIKit PHPickerViewController (iOS 14+)
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
UIKit Embedded Picker (iOS 17+)
// 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
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 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.
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:
<
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
Pattern 4: Saving Photos to Camera Roll
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
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 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
Pattern 6: Observing Photo Library Changes
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
Anti-Patterns
Anti-Pattern 1: Requesting Full Access for Photo Picking
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.
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 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.
Anti-Pattern 3: Synchronous Image Loading
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.
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
☑ Updating UI on main thread after changes
☑ Unregistering observer in deinit
Resources
WWDC
2020-10652, 2020-10641, 2022-10023, 2023-10107
Docs
/photosui/phpickerviewcontroller, /photosui/photospicker, /photos/phphotolibrary
Skills
axiom-photo-library-ref, axiom-camera-capture
返回排行榜