| name | photokit |
| description | PhotoKit and PhotosPicker patterns for photo library access, asset fetching, image loading, and the SwiftUI PhotosPicker. Use when working with the user's photo library. |
First step: Tell the user: "photokit skill loaded."
PhotoKit & PhotosPicker
Patterns for accessing the photo library, picking media, fetching assets, loading images, and saving content. Covers SwiftUI PhotosPicker, PHPickerViewController, and full PHPhotoLibrary access.
When This Skill Activates
- User needs to pick photos or videos from the library
- User asks about PhotosPicker, PHPickerViewController, or PHPhotoLibrary
- User needs to fetch, display, or cache photo assets
- User is saving images or videos to the photo library
- User asks about photo library permissions or limited access
- User needs to observe photo library changes
- User is building a gallery, image picker, or media browser
Decision Tree: Which API to Use
Need to pick photos/videos from the user's library?
|
+-- SwiftUI app?
| +-- YES --> PhotosPicker (iOS 16+, no permission needed)
| +-- NO (UIKit) --> PHPickerViewController (iOS 14+, no permission needed)
|
Need to browse/fetch the full library programmatically?
| +-- YES --> PHPhotoLibrary + PHAsset (requires authorization)
|
Only need to save photos/videos?
+-- YES --> PHPhotoLibrary.shared().performChanges (addOnly access)
Key insight: PhotosPicker and PHPickerViewController run out-of-process. They do NOT require photo library permission. Only use PHPhotoLibrary when you need programmatic browsing, fetching, or observing changes.
API Availability
| API | Minimum OS | Permission Required |
|---|
PhotosPicker (SwiftUI) | iOS 16 | No |
PHPickerViewController | iOS 14 | No |
PHPhotoLibrary | iOS 8 | Yes (readWrite) |
| Limited Photo Access | iOS 14 | Yes (limited subset) |
PHPhotoLibrary.addOnly | iOS 11 | Yes (addOnly) |
Privacy Keys
Add to Info.plist as needed:
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photos to display your library.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need permission to save photos to your library.</string>
SwiftUI PhotosPicker
Basic Usage
import PhotosUI
import SwiftUI
struct PhotoPickerView: View {
@State private var selectedItem: PhotosPickerItem?
@State private var image: Image?
var body: some View {
VStack {
PhotosPicker("Select a Photo", selection: $selectedItem, matching: .images)
if let image {
image
.resizable()
.scaledToFit()
.frame(height: 300)
}
}
.onChange(of: selectedItem) { _, newItem in
Task {
if let data = try? await newItem?.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
image = Image(uiImage: uiImage)
}
}
}
}
}
Filtering and Multi-Selection
PhotosPicker(
"Select Photos",
selection: $selectedItems,
maxSelectionCount: 5,
matching: .images
)
PhotosPicker(selection: $item, matching: .videos)
PhotosPicker(selection: $item, matching: .screenshots)
PhotosPicker(selection: $item, matching: .any(of: [.images, .videos]))
PhotosPicker(selection: $item, matching: .not(.videos))
Loading via Transferable
if let data = try? await item.loadTransferable(type: Data.self) {
let uiImage = UIImage(data: data)
}
if let image = try? await item.loadTransferable(type: Image.self) {
self.image = image
}
struct PickedImage: 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 PickedImage(image: image)
}
}
}
PHPickerViewController (UIKit)
import PhotosUI
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 3
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
viewController.present(picker, animated: true)
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
for result in results {
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
result.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
guard let image = image as? UIImage else { return }
DispatchQueue.main.async { }
}
}
}
}
PHPhotoLibrary Authorization
Requesting Access
import Photos
func requestPhotoAccess() async -> PHAuthorizationStatus {
let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
switch status {
case .authorized:
break
case .limited:
break
case .denied, .restricted:
break
case .notDetermined:
break
@unknown default:
break
}
return status
}
Access Levels
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in }
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in }
Fetching Assets
import Photos
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
options.predicate = NSPredicate(format: "mediaType == %d", PHAssetMediaType.image.rawValue)
options.fetchLimit = 50
let result: PHFetchResult<PHAsset> = PHAsset.fetchAssets(with: options)
result.enumerateObjects { asset, index, stop in
print("Asset \(index): \(asset.localIdentifier)")
}
let albums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: nil)
if let album = albums.firstObject {
let assets = PHAsset.fetchAssets(in: album, options: options)
}
Loading Images with PHImageManager
let asset: PHAsset =
let manager = PHImageManager.default()
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.resizeMode = .exact
options.isNetworkAccessAllowed = true
options.isSynchronous = false
let targetSize = CGSize(width: 300, height: 300)
manager.requestImage(
for: asset,
targetSize: targetSize,
contentMode: .aspectFill,
options: options
) { image, info in
let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool ?? false
if !isDegraded, let image {
}
}
PHCachingImageManager for Collections
final class PhotoGridViewModel {
private let cachingManager = PHCachingImageManager()
private var assets: PHFetchResult<PHAsset>?
private let thumbnailSize = CGSize(width: 200, height: 200)
func startCaching(for indexPaths: [IndexPath]) {
guard let assets else { return }
let assetsToCache = indexPaths.compactMap { assets.object(at: $0.item) }
cachingManager.startCachingImages(
for: assetsToCache,
targetSize: thumbnailSize,
contentMode: .aspectFill,
options: nil
)
}
func stopCaching(for indexPaths: [IndexPath]) {
guard let assets else { return }
let assetsToStop = indexPaths.compactMap { assets.object(at: $0.item) }
cachingManager.stopCachingImages(
for: assetsToStop,
targetSize: thumbnailSize,
contentMode: .aspectFill,
options: nil
)
}
func resetCache() {
cachingManager.stopCachingImagesForAllAssets()
}
}
Observing Photo Library Changes
final class PhotoLibraryObserver: NSObject, PHPhotoLibraryChangeObserver {
private var fetchResult: PHFetchResult<PHAsset>
init(fetchResult: PHFetchResult<PHAsset>) {
self.fetchResult = fetchResult
super.init()
PHPhotoLibrary.shared().register(self)
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
guard let changes = changeInstance.changeDetails(for: fetchResult) else { return }
DispatchQueue.main.async { [weak self] in
self?.fetchResult = changes.fetchResultAfterChanges
}
}
}
Saving to the Photo Library
import Photos
func saveImageToLibrary(_ image: UIImage) async throws {
try await PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAsset(from: image)
}
}
func saveVideoToLibrary(at url: URL) async throws {
try await PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
}
}
Patterns
Good Patterns
PhotosPicker("Choose Photo", selection: $item, matching: .images)
let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
guard status == .authorized || status == .limited else { return }
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in }
let cachingManager = PHCachingImageManager()
manager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { image, info in
let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool ?? false
if !isDegraded { }
}
let options = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
deinit { PHPhotoLibrary.shared().unregisterChangeObserver() }
Bad Patterns
PHPhotoLibrary.requestAuthorization(for: .readWrite) { _ in }
manager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { image, _ in
self.image = image
}
let options = PHImageRequestOptions()
let options = PHImageRequestOptions()
options.isSynchronous = true
manager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: options) { _, _ in }