This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
816 lines
21 KiB
Markdown
816 lines
21 KiB
Markdown
---
|
|
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
|