【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
はPhotoKitが提供しているクラスで写真アプリ内の操作や編集をアセットを介して行うことができます。
class PHImageManager : NSObject
このクラスを使用することで写真アプリのフォトライブラリの中から画質やサイズ、取得速度などをカスタマイズして画像や動画などを取得することができます。
また取得したアセットの画像とメタデータをキャッシュしてくれるため再度アクセスする際などに迅速に結果を返すことができるようになっています。
おすすめ記事:【Swift/PhotoKit】PHPickerViewControllerで画像を取得する方法!写真アプリの操作
上記記事で解説しているitemProviderから参照する方法とは異なりアセットを介しての写真アプリ内への参照にはユーザーの承認が必要になります。そのためには「info.plist」にキーを追加します。
info.plistにNSPhotoLibraryUsageDescriptionキーを追加
PhotoKitを使用してアセットやコレクションの取得、ライブラリの更新など、アプリがPhotoKitの高度な機能を使用するためにはアプリ内からデバイスの写真アプリにアクセスできるように「info.plist」に「NSPhotoLibraryUsageDescription」キーを追加する必要があります。
「info.plist」を開いたらKeyにNSPhotoLibraryUsageDescription
と入力し、Valueには写真アプリを利用する旨を記載しておきます。入力すると自動でPrivacy - Photo Library Usage Description
に変換されます。
またユーザーに対して「アプリが写真アプリを利用すること」を許可してもらう必要があります。これはフォトライブラリを参照するためのメソッドを使用した場合や後述するrequestAuthorization
メソッドなどを呼び出した際にユーザーにアラートを表示して許可を促します。
またこのアラートは初回呼び出し時のみ表示され、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)を取得する
実装手順
- ピッカーの構成(フィルタリングなど)を定義
- その構成を元にPHPickerViewControllerを構築
- PHPickerViewControllerDelegateに準拠
- 選択したアセットから識別子を取得
- PHImageManagerのインスタンス化
- PHImageRequestOptionsでオプション情報の構築
- 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
が取得できません。
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
を取得します。firstObject
がnil
かどうかで空でないかを調べています。
おすすめ記事:【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)
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。