PhotoKit で遊んでみた

作りたいアプリで Photo Album のアセットを表示する機能を実装したかったのですが、 PhotoKit の経験がなかったので、色々試してみた記録です。

一応 GitHub で公開してあります。
https://github.com/daichikuwa0618/PhotoKitPlayground

また、今回の記事は Apple 公式ドキュメント を大部分で参考にしています。

PhotoKit とは

一言でいうと、「作成したアプリと写真アプリの写真・動画との間に立って管理してくれるもの」です。
以下は公式ドキュメントからの画像ですが、アプリと写真ライブラリの間に立って、

  • 画像・動画アセットの取得
  • 画像・動画の編集

を行うようにしてくれます。

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 で定義した configurationvar にして変更可能にします。

以下のようにすることで、「画像だけを表示かつ選択上限をなくす」ことができます。

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 を閉じたときはこのメソッドが呼ばれます。

試しに resultsprint() してみます。

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: UIImageViewController に追加しておきます。

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:) メソッドの resultsPHPickerResult という型の配列になっています。
この Struct は、 assetIdentifieritemProvider を持っており、前者は写真アプリからアセットを取得する際に必要な 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: ${アクセスを求めるときの文言}

を設定します。

NSPhotoLibraryUsageDescription

これでアプリが写真ライブラリにアクセスするタイミングでダイアログが表示されるはずです。

画像を取得する

まずは 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 は本当に楽でしたね。

これを使ってまた何か機能を持ったアプリを作ってみたいです。

参考文献

  1. Apple Developer, PhotoKit
  2. Apple Developer, PHPickerViewController
  3. Developer Forums, Can you retrieve the PHAsset using the New Photo Picker?
  4. Apple Developer, PHImageManager
Built with Hugo
テーマ StackJimmy によって設計されています。