24
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2017-05-31

やりたいこと

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

準備

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

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

コードTips

import
import Photos
アルバムを取得する
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 {
    // 取得失敗
}
  • アルバムを作成 (クロージャー版)
アルバムを作る (クロージャー版)
PHPhotoLibrary.shared().performChanges({
    let createRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: "アルバム名")
    assetCollectionPlaceholder = createRequest.placeholderForCreatedAssetCollection
}) { (isSuccess, error) in
    if isSuccess {
        // 作成成功
    } else {
        // 作成失敗
    }
}
  • アルバムを作成 (Swift Concurrency 版)
アルバムを作る (Swift Concurrency 版)
do {
    try await PHPhotoLibrary.shared().performChanges {
        let createRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: "アルバム名")

        assetCollectionPlaceholder = createRequest.placeholderForCreatedAssetCollection
    }

    // 作成成功
    
} catch {
    // 作成失敗
}
  • ファイルをアルバムに保存する (クロージャー版)
ファイルをアルバムに保存する (クロージャー版)
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 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 {
    // 保存失敗
}

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

  • クロージャーを使って書く場合
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 を使って書く場合
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
}

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

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導入の一助となれば幸いです。
間違っている・より良い方法がある などあれば、コメント・編集リクエスト頂けると助かります。

24
32
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?