作りたいアプリで Photo Album のアセットを表示する機能を実装したかったのですが、 PhotoKit の経験がなかったので、色々試してみた記録です。
一応 GitHub で公開してあります。
https://github.com/daichikuwa0618/PhotoKitPlayground
また、今回の記事は Apple 公式ドキュメント を大部分で参考にしています。
PhotoKit とは
一言でいうと、「作成したアプリと写真アプリの写真・動画との間に立って管理してくれるもの」です。
以下は公式ドキュメントからの画像ですが、アプリと写真ライブラリの間に立って、
- 画像・動画アセットの取得
- 画像・動画の編集
を行うようにしてくれます。
ライブラリにアクセスするために PHPickerViewController
を使う
iOS 14 から追加された PHPickerViewController
を使って、今回はライブラリにアクセスしてみます。PHPickerViewController
を使うときは import PhotoUI
をするのを忘れないでください。
(ずっと import PhotoKit
だと思い込んでてハマった)
viewDidLoad
に以下のように記述するだけで PHPickerViewController
が立ち上がります。
override func viewDidLoad() {
super.viewDidLoad()
let configuration = PHPickerConfiguration()
let picker = PHPickerViewController(configuration: configuration)
present(picker, animated: true)
}
PHPickerConfiguration
をいじってみる
このままだと選択もできないと思います。
色々いじって見ましょう。
先程 let
で定義した configuration
を var
にして変更可能にします。
以下のようにすることで、「画像だけを表示かつ選択上限をなくす」ことができます。
override func viewDidLoad() {
super.viewDidLoad()
var configuration = PHPickerConfiguration()
configuration.filter = .images // 追加: 画像だけ選択可能
configuration.selectionLimit = 0 // 追加: 選択上限を無くす (デフォルトは 1)
let picker = PHPickerViewController(configuration: configuration)
present(picker, animated: true)
}
これでビルドすると、無制限に画像が選択できるようになったと思います。
PHPickerViewControllerDelegate
を実装する
PHPickerViewController
を閉じたときと選択が完了したときの動作を実装します。
まずは以下のようにして ViewController を PHPickerViewControllerDelegate
Protocol に準拠させていきます。
extension ViewController: PHPickerViewControllerDelegate { }
また、 delegate
に ViewController 自身を指定するのを忘れないでください。
override func viewDidLoad() {
super.viewDidLoad()
let picker = generatePhotoPicker()
picker.delegate = self // delegate を指定する
present(picker, animated: true)
}
/// PHPickerViewController を生成するメソッド
private func generatePhotoPicker() -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 0
let picker = PHPickerViewController(configuration: configuration)
return picker
}
この Protocol に準拠するオブジェクトは picker(_:didFinishPicking:)
メソッドの実装が求められます。
選択完了時や Picker を閉じたときはこのメソッドが呼ばれます。
試しに results
を print()
してみます。
extension ViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
print(results)
}
}
この状態で iOS Simulator で実行 → 写真を選択して “Add” を押下してみると、選択した画像の情報がコンソールに出力されたと思います。
また、 “Cancel” を押下すれば、空の配列が返ってきていることが分かります。
また、このままでは “Cancel” を押しても “Add” を押しても PHPickerViewController
が閉じられないので、 picker()
の引数にある picker
オブジェクトを dismiss
してあげます。
extension ViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true) // 追加: Cancel または Add ボタン押下で Picker が閉じられるようになる。
print(results)
}
}
これで Picker を閉じるのと結果を受け取るのを同時に行うことができるようになりました。
選択した画像を表示してみる
大抵の場合、選択された画像を UIImage
なりに表示したいニーズがほとんどだと思うので、やってみます。
まず、以下のようにして imageView: UIImage
を ViewController
に追加しておきます。
class ViewController: UIViewController {
private var imageView: UIImageView = UIImageView() // 追加
override func viewDidLoad() {
super.viewDidLoad()
setupImageView() // 追加
let picker = generatePhotoPicker()
picker.delegate = self
present(picker, animated: true)
}
private func generatePhotoPicker() -> PHPickerViewController { ... }
// このメソッドを追加
private func setupImageView() {
view.addSubview(imageView)
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(systemName: "photo.on.rectangle.angled")
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalTo: view.widthAnchor),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
これで起動して Picker を閉じてみると、 photo.on.rectangle.angled
の画像が画面中央に表示されると思います。
あとはこの imageView
に選択された画像を割り当てます。
picker(_:didFinishPicking:)
メソッドの results
は PHPickerResult
という型の配列になっています。
この Struct は、 assetIdentifier
と itemProvider
を持っており、前者は写真アプリからアセットを取得する際に必要な ID, 後者はデータが格納されたオブジェクトになっています。
この辺りは自分も完全に理解はしていないので、詳しくは PHPickerResult の公式ドキュメント を参考にしてみてください。
今回は itemProvider
から UIImage
を取り出し、表示させてみます。
先にコードを書いてみると、以下のようにすることで、選択された画像 (の 1 枚目) を imageView
に表示されることができます。
extension ViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let firstItemProvider = results.first?.itemProvider else { return }
if firstItemProvider.canLoadObject(ofClass: UIImage.self) {
firstItemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in
guard let self = self else { return }
DispatchQueue.main.async {
self.imageView.image = image as? UIImage
}
}
} else {
print("対応していない画像フォーマットです。")
}
}
}
canLoadObject(ofClass:)
,loadObject(ofClass:completionHandler:)
は何者なのか- なんで
DispatchQueue.main.async
が出てくるのか
辺りが気になるかと思います。
まず、 canLoadObject(ofClass:)
ですが、 ofClass
引数で与えられたクラスでデータを読み込めるかどうかを Bool
で返すメソッドです。
これが true
であれば UIImage
Class として読み込めるので loadObject
によってデータを UIImage
型で読み込んでいます。
ちなみに loadObject
メソッドで行われる読み込み処理は非同期処理です。
なのでクロージャーとして読み込み完了後の処理を渡してあげます。
また、 imageView.image
へのアクセスは View の変更であり、メインスレッドで実行されるべきなので、 DispatchQueue.main.async
によってメインスレッドで実行するようにしてます。
これで実行すると、選択した画像の最初の画像が imageView
に表示できたと思います。
assetIdentifier
によって画像を取得する
先程は itemProvider
によって画像を取得、表示しましたが、PHPickerResult
が持つもう一つのオブジェクトである assetIdentifier
でもやってみます。
Info.plist でライブラリにアクセスすることを宣言する
assetIdentifier
によって画像を取得するときは、これを ID として画像アセット自体はライブラリから直接アクセスして取得することになります。
従ってプライバシーに配慮する必要があり、 Info.plist
にて設定が必要となります。
設定するのは NSPhotoLibraryUsageDescription
で、 Info.plist
にて、
- Key:
Privacy - Photo Library Usage Description
- Value: ${アクセスを求めるときの文言}
を設定します。
これでアプリが写真ライブラリにアクセスするタイミングでダイアログが表示されるはずです。
画像を取得する
まずは PHPickerConfiguration
の引数に shared()
インスタンスを渡すことで可能になります。
理由としては、 PHPickerConfiguration()
インスタンスは assetIdentifier
を返してくれないからです。 (参考)
private func generatePhotoPicker() -> PHPickerViewController {
let photoLibrary: PHPhotoLibrary = .shared() // 追加
var configuration = PHPickerConfiguration(photoLibrary: photoLibrary) // 引数を追加
configuration.filter = .images
configuration.selectionLimit = 0
let picker = PHPickerViewController(configuration: configuration)
return picker
}
次に、 assetIdentifier
から画像を取得するには、 fetchAssets(withLocalIdentifiers:options:)
を使います。
また、それによって取得した PHAsset
から画像を取得するには PHImageManager
Class の requestImage(for:targetSize:contentMode:options:resultHandler:)
を使用します。
以上を picker
メソッドに実装してみます。
extension ViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
let ids: [String] = results.compactMap(\.assetIdentifier)
let fetchedResult = PHAsset.fetchAssets(withLocalIdentifiers: ids, options: nil)
guard let firstObject = fetchedResult.firstObject else { return }
PHImageManager().requestImage(for: firstObject,
targetSize: imageView.frame.size,
contentMode: .aspectFit,
options: nil) { [weak self] (image, info) in
DispatchQueue.main.async {
self?.imageView.image = image
}
}
}
}
実行 → 写真選択 → Add を押すと、 Picker が閉じたタイミングで許諾ダイアログが出るかと思います。
これを許可してあげると、、、
無事に写真ライブラリから画像を取得して表示させることができました!!!
または、以下のように requestAuthorization(for:handler:)
メソッドによって明示的なタイミングで許諾を求めることが可能です。
private func requestPhotoLibraryAccess() {
PHPhotoLibrary.requestAuthorization(for: .addOnly) { [weak self] status in
guard let self = self else { return }
switch status {
case .notDetermined:
self.showErrorAlert("ライブラリへのアクセスが選択されていません。")
case .restricted:
self.showErrorAlert("ライブラリへのアクセスが制限されています。")
case .denied:
self.showErrorAlert("ライブラリへのアクセスが許可されていません。")
case .authorized:
self.showPhotoPicker()
case .limited:
self.showPhotoPicker()
@unknown default:
self.showErrorAlert("ライブラリへのアクセスが不明です。")
assertionFailure("unknown permission")
}
}
}
おわり
以上で「PhotoKit で遊んでみた」としては終わりにします。
気が向いたら SwiftUI でも作ってみたいです。
今回作ったアプリの動くものは GitHub に置いておきます。
思った以上に簡単にアクセスできるんだなといった印象で、特に PHPickerViewController
は本当に楽でしたね。
これを使ってまた何か機能を持ったアプリを作ってみたいです。