Photos.frameworkを使ってアルバムに画像を保存(+取得)する

# やりたいこと
写真ライブラリのアルバム内に画像を保存する。
指定アルバムが無ければアルバムを作成して保存する。

# 準備
Info.plistに使用目的を記載。
初回アクセス時のダイアログメッセージに利用されます。

```tex:Info.plist
<key>NSPhotoLibraryUsageDescription</key>
<string>画像の保存に使用します。</string>
```

# コードTips
```swift:import
import Photos
```

```swift:アルバムを取得する
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %@", "アルバム名")
let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
if collection.firstObject != nil {
    // 取得成功(->collection.firstObject)
} else {
    // 取得失敗
}
```

- アルバムを作成 (クロージャー版)

```swift:アルバムを作る (クロージャー版)
PHPhotoLibrary.shared().performChanges({
    let createRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: "アルバム名")
    assetCollectionPlaceholder = createRequest.placeholderForCreatedAssetCollection
}) { (isSuccess, error) in
    if isSuccess {
        // 作成成功
    } else {
        // 作成失敗
    }
}
```

- アルバムを作成 (Swift Concurrency 版)

```swift:アルバムを作る (Swift Concurrency 版)
do {
    try await PHPhotoLibrary.shared().performChanges {
        let createRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: "アルバム名")

        assetCollectionPlaceholder = createRequest.placeholderForCreatedAssetCollection
    }

    // 作成成功
    
} catch {
    // 作成失敗
}
```

- ファイルをアルバムに保存する (クロージャー版)

```swift:ファイルをアルバムに保存する (クロージャー版)
let album: PHAssetCollection = アルバム情報
PHPhotoLibrary.shared().performChanges({
    let assetRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: fileURL)!
    let albumChangeRequest = PHAssetCollectionChangeRequest(for: album)
    let placeHolder = assetRequest.placeholderForCreatedAsset

    albumChangeRequest?.addAssets([placeHolder!] as NSArray)
}) { (isSuccess, error) in
    if isSuccess {
        // 保存成功
    } else {
        // 保存失敗
    }
}
```

- ファイルをアルバムに保存する (Swift Concurrency 版)

```swift:ファイルをアルバムに保存する (Swift Concurrency 版)
let album: PHAssetCollection = アルバム情報

do {
    try await PHPhotoLibrary.shared().performChanges {
        let assetRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: fileURL)!
        let albumChangeRequest = PHAssetCollectionChangeRequest(for: album)
        let placeHolder = assetRequest.placeholderForCreatedAsset
    
        albumChangeRequest?.addAssets([placeHolder!] as NSArray)
    }

    // 保存成功
    
} catch {
    // 保存失敗
}
```

# コードサンプル(保存の一連処理)

- クロージャーを使って書く場合

```swift:sample_closure_version.swift
func saveImage(_ imageData: Data, toAlbum albumName: String, completionBlock: @escaping (String?) -> Void, failureBlock: @escaping (Error?) -> Void) {
    let TempFilePath = "\(NSTemporaryDirectory())temp.jpg"
    var imageID: String? = nil

    findOrCreatePhotoAlbum(name: albumName) { (album, error) in
        // 画像データを一時ファイルとして保存
        let fileURL = URL(fileURLWithPath: TempFilePath)
        try? imageData.write(to: fileURL, options: .atomic)

        if let album = album, FileManager.default.fileExists(atPath: TempFilePath) {
            PHPhotoLibrary.shared().performChanges({
                let assetRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: fileURL)!
                let albumChangeRequest = PHAssetCollectionChangeRequest(for: album)
                let placeHolder = assetRequest.placeholderForCreatedAsset
                albumChangeRequest?.addAssets([placeHolder!] as NSArray)
                imageID = assetRequest.placeholderForCreatedAsset?.localIdentifier
            }) { (isSuccess, error) in
                if isSuccess {
                    // 保存した画像にアクセスする為のimageIDを返却
                    completionBlock(imageID)
                } else {
                    failureBlock(error)
                }
                _ = try? FileManager.default.removeItem(atPath: TempFilePath)
            }
        } else {
            failureBlock(error)
            _ = try? FileManager.default.removeItem(atPath: TempFilePath)
        }
    }
}

private func findOrCreatePhotoAlbum(name: String, completion: @escaping (PHAssetCollection?, Error?) -> Void) {
    var assetCollection: PHAssetCollection?
    var assetCollectionPlaceholder: PHObjectPlaceholder?

    let fetchOptions = PHFetchOptions()
    fetchOptions.predicate = NSPredicate(format: "title = %@", name)
    let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
    if collection.firstObject != nil {
        assetCollection = collection.firstObject
        completion(assetCollection, nil)
    } else {
        PHPhotoLibrary.shared().performChanges({
            let createRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: name)
            assetCollectionPlaceholder = createRequest.placeholderForCreatedAssetCollection
        }) { (isSuccess, error) in
            if isSuccess {
                let refetchResult = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [assetCollectionPlaceholder!.localIdentifier], options: nil)
                assetCollection = refetchResult.firstObject
                completion(assetCollection, nil)
            } else {
                completion(nil, error)
            }
        }
    }
}
```

- Swift Concurrency を使って書く場合

```swift:sample_concurrency_version.swift
func saveImage(_ imageData: Data, toAlbum albumName: String) async throws -> String? {
    let TempFilePath = "\(NSTemporaryDirectory())temp.jpg"

    do {
        var imageID: String? = nil
    
        guard let album: PHAssetCollection? = try await findOrCreatePhotoAlbum(name: String) else { return nil }

        guard FileManager.default.fileExists(atPath: TempFilePath) else { return nil }

        try await PHPhotoLibrary.shared().performChanges {
            let assetRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: fileURL)!
            let albumChangeRequest = PHAssetCollectionChangeRequest(for: album)

            let placeHolder = assetRequest.placeholderForCreatedAsset

            albumChangeRequest?.addAssets([placeHolder!] as NSArray)

            imageID = assetRequest.placeholderForCreatedAsset?.localIdentifier
        }

        try? FileManager.default.removeItem(atPath: TempFilePath)

        // 保存した画像にアクセスする為のimageIDを返却
        return imageID
    } catch {
        try? FileManager.default.removeItem(atPath: TempFilePath)

        throw error
    }
}

private func findOrCreatePhotoAlbum(name: String) async throws -> PHAssetCollection? {

    let fetchOptions = PHFetchOptions()

    fetchOptions.predicate = NSPredicate(format: "title = %@", name)

    let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)

    let assetCollection = collection.firstObject
    
    if assetCollection == nil {
        do {
            var assetCollectionPlaceholder: PHObjectPlaceholder?

            try await PHPhotoLibrary.shared().performChanges {
                let createRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: name)

                assetCollectionPlaceholder = createRequest.placeholderForCreatedAssetCollection
            }

            let refetchResult = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [assetCollectionPlaceholder!.localIdentifier], options: nil)

            return refetchResult.firstObject
        } catch {
            throw error
        }
    }

    return assetCollection
}
```

# コードサンプル(保存した写真データを取得)

```swift:sample2.swift
// 保存する際に取得したimageIDを指定して画像データを取得
func getImageData(id: String, completionBlock: @escaping (Data?) -> Void) {
    if let asset = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil).firstObject {
        PHImageManager.default().requestImageData(for: asset, options: nil) { (data, _, _, _) in
            completionBlock(data)
        }
    }
}
```

# 注意点
画像を保存する関数は2種類がありますが、
`open class func creationRequestForAsset(from image: UIImage) -> Self`
を使用して画像を保存した場合にはExifなどの情報が抜け落ちてしまう為、
元データと同じ状態で画像を保存する場合には
`open class func creationRequestForAssetFromImage(atFileURL fileURL: URL) -> Self?`
を利用する必要があります。
また、その際に必要なPath情報はサーバー上のURLではなく、ローカルのPath情報なので一時ファイルとして一旦ファイルを保存してそのPathを利用する必要があります。

# おわり
Photos.framework導入の一助となれば幸いです。
間違っている・より良い方法がある などあれば、コメント・編集リクエスト頂けると助かります。