axiom-photo-library-ref について
AppleのPhotosUIとPhotoKit APIを参照するiOS開発者向けSwiftスキルです。写真選択、ライブラリ権限、アセットの検索と変更、画像や動画の取得を扱います。
主な機能
- 01SwiftUIとUIKitの写真ピッカー利用
- 02選択フィルターと上限数の設定
- 03ライブラリ権限の要求と確認
- 04写真アセットの検索と変更
- 05画像と動画アセットの取得
活用シーン
- 01ユーザーによる写真や動画の選択
- 02写真ライブラリ権限の管理
- 03種類や日付によるアセット検索
- 04画像や動画のライブラリへの保存
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-photo-library-refインストール後、Claude Code で/axiom-photo-library-refを入力して読み込まれたか確認します。
詳細
写真ライブラリ API リファレンス
クイックリファレンス
// 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+)
UIKit アプリ向けのシステム写真ピッカーです。アクセス許可は不要です。
設定
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
フィルターオプション
// 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])
表示
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
デリゲート
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 のプロパティ
| プロパティ | 型 | 説明 |
|---|---|---|
itemProvider | NSItemProvider | 選択したアセットデータを提供します |
assetIdentifier | String? | PHAsset の識別子(photoLibrary 設定を使用する場合) |
PhotosPicker(SwiftUI、iOS 16+)
写真を選択するための SwiftUI ビューです。アクセス許可は不要です。
基本的な使い方
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")
}
フィルター
// 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)])
選択時の動作
PhotosPicker(
selection: $items,
maxSelectionCount: 10,
selectionBehavior: .ordered, // .default, .ordered, .continuous
matching: .images
) { ... }
| 動作 | 説明 |
|---|---|
.default | 標準の複数選択 |
.ordered | 選択順を保持します |
.continuous | ユーザーの選択に応じてリアルタイムに更新します(iOS 17+) |
埋め込みピッカー(iOS 17+)
PhotosPicker(
selection: $items,
maxSelectionCount: 10,
selectionBehavior: .continuous,
matching: .images
) {
Text("Select")
}
.photosPickerStyle(.inline) // Embed in view hierarchy
.photosPickerDisabledCapabilities([.selectionActions])
.photosPickerAccessoryVisibility(.hidden, edges: .all)
| スタイル | 説明 |
|---|---|
.presentation | モーダルシート(デフォルト) |
.inline | ビュー内に埋め込みます |
.compact | 1 行表示 |
| 無効にする機能 | 効果 |
|---|---|
.search | 検索バーを非表示にします |
.collectionNavigation | アルバムを非表示にします |
.stagingArea | 選択内容の確認画面を非表示にします |
.selectionActions | 追加/キャンセルを非表示にします |
| アクセサリの表示状態 | 説明 |
|---|---|
.hidden、.automatic、.visible | エッジごとに設定 |
HDR の保持(iOS 17+)
PhotosPicker(
selection: $items,
matching: .images,
preferredItemEncoding: .current // Don't transcode, preserve HDR
) { ... }
| エンコーディング | 説明 |
|---|---|
.automatic | システムが形式を決定します |
.current | 元の形式を使用し、HDR を保持します |
.compatible | 互換形式を強制します |
PhotosPickerItem からの画像の読み込み
// 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 のプロパティ
| プロパティ | 型 | 説明 |
|---|---|---|
itemIdentifier | String | 一意の識別子 |
supportedContentTypes | [UTType] | 利用可能な表現 |
PhotosPickerItem のメソッド
// 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
写真ライブラリにアクセスし、変更します。
承認ステータス
// Check current status
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
// Request authorization
let newStatus = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
PHAuthorizationStatus
| ステータス | 説明 |
|---|---|
.notDetermined | ユーザーに確認していません |
.restricted | ペアレンタルコントロールによりアクセスが制限されています |
.denied | ユーザーがアクセスを拒否しました |
.authorized | フルアクセスが許可されています |
.limited | ユーザーが選択した写真のみアクセスできます(iOS 14+) |
アクセスレベル
// Read and write
PHPhotoLibrary.requestAuthorization(for: .readWrite)
// Add only (save photos, no reading)
PHPhotoLibrary.requestAuthorization(for: .addOnly)
制限付きライブラリピッカー
// 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
}
変更の実行
// Async changes
try await PHPhotoLibrary.shared().performChanges {
// Create, update, or delete assets
}
// With completion handler
PHPhotoLibrary.shared().performChanges({
// Changes
}) { success, error in
// Handle result
}
変更オブザーバー
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
写真ライブラリ内のアセットを表します。
アセットの取得
// 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)
アセットのプロパティ
| プロパティ | 型 | 説明 |
|---|---|---|
localIdentifier | String | 一意の ID |
mediaType | PHAssetMediaType | .image、.video、.audio |
mediaSubtypes | PHAssetMediaSubtype | .photoLive、.photoPanorama など |
pixelWidth | Int | ピクセル単位の幅 |
pixelHeight | Int | ピクセル単位の高さ |
creationDate | Date? | 撮影日時 |
modificationDate | Date? | 最終更新日時 |
location | CLLocation? | GPS 位置情報 |
duration | TimeInterval | 動画の長さ |
isFavorite | Bool | お気に入りとしてマークされているか |
isHidden | Bool | 非表示アルバムに含まれているか |
PHAssetMediaType
| 型 | 値 |
|---|---|
.unknown | 0 |
.image | 1 |
.video | 2 |
.audio | 3 |
PHAssetMediaSubtype
| サブタイプ | 説明 |
|---|---|
.photoPanorama | パノラマ写真 |
.photoHDR | HDR 写真 |
.photoScreenshot | スクリーンショット |
.photoLive | Live Photo |
.photoDepthEffect | ポートレートモード |
.videoStreamed | ストリーミング動画 |
.videoHighFrameRate | スローモーション動画 |
.videoTimelapse | タイムラプス |
.videoCinematic | シネマティックモード |
PHAssetCreationRequest
写真ライブラリに新しいアセットを作成します。
UIImage から作成
try await PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAsset(from: image)
}
ファイル URL から作成
try await PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAssetFromImage(atFileURL: imageURL)
}
// For video
try await PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
}
リソースを使用して作成
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)
}
遅延写真プロキシ(iOS 17+)
バックグラウンド処理用にカメラのプロキシ写真を保存します。
// 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)
}
| リソースタイプ | 説明 |
|---|---|
.photo | 標準の写真 |
.video | 動画ファイル |
.photoProxy | 遅延処理用プロキシ(iOS 17+) |
.adjustmentData | 編集内容の調整データ |
作成したアセットの取得
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
}
カスタムアルバム
// 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
フェッチしたアセットの順序付きリストです。
プロパティ
| プロパティ | 型 | 説明 |
|---|---|---|
count | Int | 項目数 |
firstObject | T? | 最初の項目 |
lastObject | T? | 最後の項目 |
メソッド
// 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
アセットから画像をリクエストします。
画像のリクエスト
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
| プロパティ | 型 | 説明 |
|---|---|---|
deliveryMode | PHImageRequestOptionsDeliveryMode | 品質の設定 |
resizeMode | PHImageRequestOptionsResizeMode | リサイズの動作 |
isNetworkAccessAllowed | Bool | iCloud からのダウンロードを許可 |
isSynchronous | Bool | 同期リクエスト |
progressHandler | Block | ダウンロードの進行状況 |
allowSecondaryDegradedImage | Bool | 遅延処理中に追加のコールバックを許可(iOS 17+) |
二次的な低品質画像(iOS 17+)
遅延処理中の写真について、中間品質の画像を取得します。
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)
配信モード
| モード | 説明 |
|---|---|
.opportunistic | 高速なサムネイルの後に高品質画像を取得 |
.highQualityFormat | 高品質画像のみ |
.fastFormat | 高速画像/低品質画像のみ |
動画のリクエスト
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
写真ライブラリの変更を表します。
変更の詳細を取得
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()
}
}
}
よく使われるコードパターン
完全なフォトギャラリービュー
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()
}
}
}
リソース
ドキュメント: /photosui/phpickerviewcontroller, /photosui/photospicker, /photos/phphotolibrary, /photos/phasset, /photos/phimagemanager
スキル: axiom-photo-library, axiom-camera-capture
コメント
Markdown 対応