mcpField
A
axiom-photo-library-ref

axiom-photo-library-ref

github.com/charleswiltgen開発1.2k166 回インストール今日更新
リポジトリ

axiom-photo-library-ref について

AppleのPhotosUIとPhotoKit APIを参照するiOS開発者向けSwiftスキルです。写真選択、ライブラリ権限、アセットの検索と変更、画像や動画の取得を扱います。

主な機能

  1. 01SwiftUIとUIKitの写真ピッカー利用
  2. 02選択フィルターと上限数の設定
  3. 03ライブラリ権限の要求と確認
  4. 04写真アセットの検索と変更
  5. 05画像と動画アセットの取得

活用シーン

  1. 01ユーザーによる写真や動画の選択
  2. 02写真ライブラリ権限の管理
  3. 03種類や日付によるアセット検索
  4. 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 のプロパティ
プロパティ型説明
itemProviderNSItemProvider選択したアセットデータを提供します
assetIdentifierString?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ビュー内に埋め込みます
.compact1 行表示
無効にする機能効果
.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 のプロパティ
プロパティ型説明
itemIdentifierString一意の識別子
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)
アセットのプロパティ
プロパティ型説明
localIdentifierString一意の ID
mediaTypePHAssetMediaType.image、.video、.audio
mediaSubtypesPHAssetMediaSubtype.photoLive、.photoPanorama など
pixelWidthIntピクセル単位の幅
pixelHeightIntピクセル単位の高さ
creationDateDate?撮影日時
modificationDateDate?最終更新日時
locationCLLocation?GPS 位置情報
durationTimeInterval動画の長さ
isFavoriteBoolお気に入りとしてマークされているか
isHiddenBool非表示アルバムに含まれているか
PHAssetMediaType
型値
.unknown0
.image1
.video2
.audio3
PHAssetMediaSubtype
サブタイプ説明
.photoPanoramaパノラマ写真
.photoHDRHDR 写真
.photoScreenshotスクリーンショット
.photoLiveLive 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

フェッチしたアセットの順序付きリストです。

プロパティ
プロパティ型説明
countInt項目数
firstObjectT?最初の項目
lastObjectT?最後の項目
メソッド
// 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
プロパティ型説明
deliveryModePHImageRequestOptionsDeliveryMode品質の設定
resizeModePHImageRequestOptionsResizeModeリサイズの動作
isNetworkAccessAllowedBooliCloud からのダウンロードを許可
isSynchronousBool同期リクエスト
progressHandlerBlockダウンロードの進行状況
allowSecondaryDegradedImageBool遅延処理中に追加のコールバックを許可(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

コメント