【Swift/PhotoKit】PHImageManagerの使い方!アセット操作と画像の取得

この記事からわかること

  • PHImageManagerとは?
  • フォトライブラリの中から画質サイズなどをカスタマイズして画像動画取得するには?
  • ユーザー承認状態の取得方法
  • info.plist設定するキー
  • アセットから画像(UIImage)を取得する

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

参考文献:Apple-PhotoKit

参考文献:Can you retrieve the PHAsset using the New Photo Picker?

PhotoKitとは?

そもそもPhotoKitとはApple製のデバイスに入っている「写真アプリ」で管理されている写真や動画を操作するためのAPIを提供している技術です。実際の中身はPhotos FrameworkとPhotosUI Frameworkの2つのフレームワークに分けられて構成されており、それぞれをimportすることで使用できるようになります。

import Photos
import PhotosUI

Photos

カメラロール(フォトライブラリ)を管理するオブジェクトを提供するPHPhotoLibraryや、実際の画像や動画などのアセットを管理するPHAsset、アセットのコレクション(アルバムなど)を表現するPHAssetCollectionなどといった基本的な写真アプリ操作機能を提供。

PhotosUI

画像のピッカービューを構築するためのPHPickerViewControllerやその構成を定義するPHPickerConfigurationなどUIに関する機能を提供。

またPhotosUIの中にPhotosが含まれているためPhotosUIのみでPhotosの機能も使用できるようになります。

今回はPHImageManagerに焦点を当ててまとめていきます。

PHImageManagerとは?

参考文献:公式リファレンス:PHImageManager

PHImageManagerはPhotoKitが提供しているクラスで写真アプリ内の操作や編集をアセットを介して行うことができます。

class PHImageManager : NSObject

このクラスを使用することで写真アプリのフォトライブラリの中から画質やサイズ、取得速度などをカスタマイズして画像や動画などを取得することができます。

また取得したアセットの画像とメタデータをキャッシュしてくれるため再度アクセスする際などに迅速に結果を返すことができるようになっています。

おすすめ記事:【Swift/PhotoKit】PHPickerViewControllerで画像を取得する方法!写真アプリの操作

上記記事で解説しているitemProviderから参照する方法とは異なりアセットを介しての写真アプリ内への参照にはユーザーの承認が必要になります。そのためには「info.plist」にキーを追加します。

info.plistにNSPhotoLibraryUsageDescriptionキーを追加

PhotoKitを使用してアセットやコレクションの取得、ライブラリの更新など、アプリがPhotoKitの高度な機能を使用するためにはアプリ内からデバイスの写真アプリにアクセスできるように「info.plist」に「NSPhotoLibraryUsageDescription」キーを追加する必要があります。

【Swift UIKit】画像をカメラロールから保存/取得/削除する方法!

「info.plist」を開いたらKeyにNSPhotoLibraryUsageDescriptionと入力し、Valueには写真アプリを利用する旨を記載しておきます。入力すると自動でPrivacy - Photo Library Usage Descriptionに変換されます。

またユーザーに対して「アプリが写真アプリを利用すること」を許可してもらう必要があります。これはフォトライブラリを参照するためのメソッドを使用した場合や後述するrequestAuthorizationメソッドなどを呼び出した際にユーザーにアラートを表示して許可を促します。

【Swift】PhotoKitの使い方と写真アプリ操作方法!画像や動画の選択や保存

またこのアラートは初回呼び出し時のみ表示され、2回目以降は表示されません。ユーザーに否認されてしまった場合は設定から変更してもらう必要があります。

承認状態を取得する

ユーザーの現在の承認ステータスを取得するにはrequestAuthorizationメソッドを使用します。このメソッドが初回に呼び出された場合でも先ほどの許可申請のアラートが表示されます。

class func requestAuthorization(
    for accessLevel: PHAccessLevel,
    handler: @escaping (PHAuthorizationStatus) -> Void
)

completionHandlerから承認ステータスにアクセスできるのでその値によって処理分岐しています。ユーザーの承認ステータス次第で処理を分岐させたい場合に使用できます。

おすすめ記事:【Swift】completionHandlerとは?使い方と@escapingの意味

// ユーザーのフォト ライブラリへの読み取り/書き込みアクセスを要求します。
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
    switch status {
    case .notDetermined: break
        // ユーザーはこのアプリのアクセスを決定していません。
    case .restricted: break
        // システムがこのアプリのアクセスを制限しました。
    case .denied: break
        // ユーザーはこのアプリのアクセスを明示的に拒否しました。
    case .authorized: break
        // ユーザーは、このアプリに写真データへのアクセスを許可しました。
    case .limited: break
        // ユーザーはこのアプリに写真への限定的なアクセスを許可しました。
    @unknown  default:
        fatalError()
    }
}

アセットから画像(UIImage)を取得する

実装手順

  1. ピッカーの構成(フィルタリングなど)を定義
  2. その構成を元にPHPickerViewControllerを構築
  3. PHPickerViewControllerDelegateに準拠
  4. 選択したアセットから識別子を取得
  5. PHImageManagerのインスタンス化
  6. PHImageRequestOptionsでオプション情報の構築
  7. requestImageで画像を取得

例としてピッカーから選択した画像をアプリ内に表示させることを目標に作成していきます。まずはPhotoKitViewControllerを作成し中に必要となるUIを記述していきます。


import UIKit
import PhotosUI

class PhotoKitViewController: UIViewController {
    
    let imageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton()
        button.backgroundColor = .orange
        button.frame = CGRect(x: UIScreen.main.bounds.width/3, y: 200, width: UIScreen.main.bounds.width/3, height: 50)
        button.setTitle("画像を選択", for: .normal)
        button.addTarget(self, action: #selector(showPhotoPicker), for: .touchUpInside)
        view.addSubview(button)    

        imageView.frame =  CGRect(x: 0, y: 400, width: UIScreen.main.bounds.width, height: 300)
        view.addSubview(imageView)
    }
    
    @objc  func showPhotoPicker() {
      // ピッカーの構成(フィルタリングなど)を定義
      // その構成を元にPHPickerViewControllerを構築
    }
}

ここではボタンとそのアクション(まだ空の状態)、画像を表示させるためのUIImageViewを配置しておきます。

ボタンアクションの構築とdelegate

PHPickerViewControllerを使用した画像の選択方法については以下の記事を参考にしてください。

おすすめ記事:【Swift/PhotoKit】PHPickerViewControllerで画像を取得する方法!写真アプリの操作

ここではボタンのアクション内でピッカービューの定義とViewControllerを拡張してPHPickerViewControllerDelegateに準拠させpickerメソッドを準備しています。


@objc  func showPhotoPicker() {
    let photoLibrary = PHPhotoLibrary.shared()
    var configuration = PHPickerConfiguration(photoLibrary: photoLibrary)
    configuration.filter = PHPickerFilter.images
    configuration.selectionLimit = 1
    let picker = PHPickerViewController(configuration: configuration)
    picker.delegate = self
    present(picker, animated: true, completion: nil)
}

extension PhotoKitViewController: UINavigationControllerDelegate, PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
      // 
    }
}

またピッカーの構成部分が少し変更になっています。構成のインスタンス化時の引数にPHPhotoLibraryインスタンスを渡しています。PHPhotoLibraryオブジェクトは写真アプリが管理するアセットとコレクションのセット全体を表します。PHPhotoLibraryインスタンスを渡さないと後述するassetIdentifierが取得できません。

公式リファレンス:PHPhoto Library

let photoLibrary = PHPhotoLibrary.shared()
var configuration = PHPickerConfiguration(photoLibrary: photoLibrary)

選択したアセットから識別子を取得

pickerメソッドから取得できる結果は配列形式のPHPickerResult型です。PHPickerResultのプロパティからは選択されたアセットを識別するID(assetIdentifier:String型)とアセットを表示させるプロバイダー(itemProvider:NSItemProvider型)に参照できます。このassetIdentifierを元にアセットからデータをフェッチしていきます。


func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    let assetIds: [String] = results.compactMap(\.assetIdentifier)
    let fetchedResult:PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
    guard let firstObject = fetchedResult.firstObject else { return }

}

ここでは選択された画像のアセット識別子を取得し、String型の配列形式として格納しておき、PHAsset.fetchAssets(withLocalIdentifiers:options:)メソッドを使用してPHFetchResultを取得します。firstObjectnilかどうかで空でないかを調べています。

おすすめ記事:【Swift/PhotoKit】PHAssetとは?モデルオブジェクトの取得と操作方法!

PHImageManagerのインスタンス化

PHImageManagerを使用するにはdefaultメソッドを呼び出し共有して使用するシングルトンのインスタンスを参照します。


func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    let assetIds: [String] = results.compactMap(\.assetIdentifier)
    let fetchedResult:PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
    guard let firstObject = fetchedResult.firstObject else { return }

    let manager = PHImageManager.default()
}

インスタンスからrequestImage(for:targetSize:contentMode:options:resultHandler:)メソッドを呼び出して指定したアセットの画像(UIImage)をリクエストできます。optionsは未設定(nil)でも取得できますが、PHImageRequestOptions型で明示的に指定することでより鮮明な画質かつエラーを吐かずに取得できるので定義しておきます。

PHImageRequestOptionsでオプション情報の構築

オプションを定義するにはPHImageRequestOptionsクラスを使います。用意されている各プロパティに任意の値を渡すことでオプション情報を構築し、リクエストに反映させることができます。

class PHImageRequestOptions : NSObject {
    var isSynchronous: Bool // 同期的に処理するかどうか
    var version: PHImageRequestOptionsVersion // 要求される画像のバージョン
    var deliveryMode: PHImageRequestOptionsDeliveryMode // 要求された画質と配信の優先順位
    var resizeMode: PHImageRequestOptionsResizeMode // 要求された画像のサイズを変更する方法を指定するモード
    var normalizedCropRect: CGRect //画像のトリミング
    var isNetworkAccessAllowed: Bool // iCloudからダウンロードできるかどうか
    var progressHandler: PHAssetImageProgressHandler? // 画像をダウンロード中に写真が定期的に呼び出すブロック
}

ここでは以下のようにオプション情報を構築しておきました。


let manager = PHImageManager.default()
  
let requestOptions = PHImageRequestOptions()
requestOptions.version = PHImageRequestOptionsVersion.current
requestOptions.deliveryMode = PHImageRequestOptionsDeliveryMode.highQualityFormat
requestOptions.resizeMode = PHImageRequestOptionsResizeMode.exact
requestOptions.isSynchronous = true
requestOptions.isNetworkAccessAllowed = true

requestImageで画像を取得

オプションを定義できたらrequestImageメソッドに戻ります。引数は以下の通りです。

func requestImage(
    for asset: PHAsset,  // 対象アセット
    targetSize: CGSize, // 表示サイズ
    contentMode: PHImageContentMode, //  縦横比オプション
    options: PHImageRequestOptions?, // リクエストオプション
    resultHandler: @escaping (UIImage?, [AnyHashable : Any]?) -> Void // 完了ハンドラー
) -> PHImageRequestID

引数のresultHandlerから結果を取得でき、画像とリクエストステータスの情報を辞書型で受け取れます。forには先ほど取得したfirstObjectを、optionsには先ほど構築したオプション情報を渡します。


manager.requestImage(for: firstObject,
      targetSize: imageView.frame.size,
      contentMode: .aspectFit,
      options: requestOptions) { [weak self] (image, info) in
    DispatchQueue.main.async {
        self?.imageView.image = image
    }
}
self.dismiss(animated: true)

ビューを更新するのでメインスレッドでimageViewプロパティにセットして表示させています。

最後にピッカービューを明示的に閉じる必要があるためdismissを記述します。これを書き忘れるとピッカーが閉じれないので注意してください。

複数回呼び出されるので注意

requestImageメソッドは非同期リクエストの場合、コールバックが複数回呼び出される仕様になっているようです。最初に低画質の画像を高速に取得し、後から高画質の画像を取得するようで取得のたびにコールバックが呼び出されるようです。

参考文献:iOS PHImageManager.default().requestImage callback is called twice for the same image

全体のコード

import UIKit
import PhotosUI

class PhotoKitViewController: UIViewController {
    
    let imageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton()
        button.backgroundColor = .orange
        button.frame = CGRect(x: UIScreen.main.bounds.width/3, y: 200, width: UIScreen.main.bounds.width/3, height: 50)
        button.setTitle("画像を選択", for: .normal)
        button.addTarget(self, action: #selector(showPhotoPicker), for: .touchUpInside)
        view.addSubview(button)
        
        imageView.frame =  CGRect(x: 0, y: 400, width: UIScreen.main.bounds.width, height: 300)
        view.addSubview(imageView)
        
    }
    
    
    @objc  func showPhotoPicker() {
        let photoLibrary = PHPhotoLibrary.shared()
        var configuration = PHPickerConfiguration(photoLibrary: photoLibrary)
//                var configuration = PHPickerConfiguration()
        configuration.filter = PHPickerFilter.images
        configuration.selectionLimit = 1
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self
        present(picker, animated: true, completion: nil)
    }
}



extension PhotoKitViewController: UINavigationControllerDelegate, PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        let assetIds: [String] = results.compactMap(\.assetIdentifier)
        let fetchedResult:PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
        guard let firstObject = fetchedResult.firstObject else { return }
        
        let manager = PHImageManager.default()
        
        let requestOptions = PHImageRequestOptions()
        requestOptions.version = PHImageRequestOptionsVersion.current
        requestOptions.deliveryMode = PHImageRequestOptionsDeliveryMode.highQualityFormat
        requestOptions.resizeMode = PHImageRequestOptionsResizeMode.exact
        requestOptions.isSynchronous = true
        requestOptions.isNetworkAccessAllowed = true
        
        manager.requestImage(for: firstObject,
                                      targetSize: imageView.frame.size,
                                      contentMode: .aspectFit,
                                      options: requestOptions) { [weak self] (image, info) in
            DispatchQueue.main.async {
                self?.imageView.image = image
            }
        }
        self.dismiss(animated: true)
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index