LoginSignup
5
5

More than 3 years have passed since last update.

【Swift】PhotoKitで画像選択UIを作る

Last updated at Posted at 2021-01-19

Custom UIs
LINEやTwitter、Pinterestなど、写真を扱う多くのアプリにはカスタマイズされた画像選択UIが実装されています。
今回はこのようなUIをPhotoKitを使って実装してみます。

デフォルトのPickerControllerはカスタマイズ性が低い

"Swift 画像 選択" などで検索すると、UIImagePickerControllerによる実装がいくつかヒットします。
何も考えずにパパッと実装したい場合には便利なのですが、UIや表示内容のカスタマイズ性が制限されるため、デザインの選択肢が狭まってしまいます。

PhotoKitでフォトライブラリにアクセスする

そこで、PhotoKitを用いて直接フォトライブラリからデータを取得してUIに設定するという方法を考えます。

ユーザにアクセスを許可してもらう

まずはUIImagePickerViewControllerと同様、Info.plistNSPhotoLibraryUsageDescriptionを追加します。
(ここで記述した文字列がアクセス要求の際に表示されます)
スクリーンショット 2021-01-19 11.27.00.png

次に、画像を読み込みたいタイミングでauthorizationStatus(for:)を呼び出し、フォトライブラリへのアクセスが許可されているかを確認します。

アプリの要求する許可が与えられていない場合は、requestAuthorization(for:handler:)でユーザに許可を促します。

// フォトライブラリへのアクセスを要求する
func requireAuthToPhotoLib(){
    let currentStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)

    // 許可されている?
    if [.authorized, .limited].contains(currentStatus){
        print("User has already granted access to the library.")
        return
    }

    // 許可をリクエスト
    PHPhotoLibrary.requestAuthorization(for: .readWrite) { (status) in
        switch status {
        case .authorized: // 「すべての写真へのアクセスを許可」
            print("Authorized!")

        case .denied: // 「許可しない」
            print("Denied")

        case .limited: // 「写真を選択」
            print("Limited access")

        case .notDetermined: // ユーザがどうするか決めてない
            print("Not determined")

        case .restricted: // ユーザはフォトライブラリへのアクセスを許可できない
            print("Restricted")

        @unknown default:
            fatalError("!?")
        }
    }
}

requestAuthorization(for:handler:)を呼び出すと、このようなプロンプトが表示されます。

ここで「写真を選択…」をタップすると .limited
すべての写真へのアクセスを許可」をタップすると.authorized
許可しない」をタップすると.deniedが返ります。

フォトライブラリからデータを取得

フォトライブラリの内容を取得するにはfetchAssets(with: PHFetchOptions)を使用します。
PHFetchOptionsには以下のように様々なオプションを設定できます。(引用: 公式ドキュメント)

スクリーンショット 2021-01-19 16.15.27.png

が、今回は単純に撮影日時の降順に10枚取得するように設定しました。

let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
fetchOptions.fetchLimit = 10
let fetchResult = PHAsset.fetchAssets(with: fetchOptions)

fetchAssetsの戻り値PHFetchResultは配列のように添字で参照でき、.countでサイズの取得も可能です。

fetchResult[0].mediaType == .image // true
fetchResult.count // 10

しかし公式ドキュメントによると、配列のように全ての値がその瞬間用意されているわけではないようです。

... Unlike an NSArray object, however, a PHFetchResult object dynamically loads its contents from the Photos library as needed, providing optimal performance even when handling a large number of results. ...

したがって、mapfilterで処理するといったことはできません

PHAssetからサムネイル画像を生成

次に、取得したPHFetchResultよりPHAssetを取り出し、サムネイルを生成します。
PHCachingImageManager.requestImage(for:)を使用することでUIImageに変換できます。

// (UICollectionViewにUIImageViewを載せて表示させる想定)
let cell: CollectionViewCell = .......

DispatchQueue.global().async {
    PHCachingImageManager().requestImage(
        for: asset,
        targetSize: self.layout.itemSize,
        contentMode: .aspectFill,
        options: nil
    ) { (image, nil) in

        // 画像の準備が完了したときに呼び出される
        DispatchQueue.main.async {
            cell.image = image
        }
    }
}

公式ドキュメントによると、handlerは複数回呼び出されることがあるようです。

For an asynchronous request, Photos may call your result handler block more than once.
Photos first calls the block to provide a low-quality image suitable for displaying temporarily while it prepares a high-quality image.
(If low-quality image data is immediately available, the first call may occur before the method returns.)
When the high-quality image is ready, Photos calls your result handler again to provide it.

画像の表示にやたら時間がかかるという状況を回避するために、handlerにはあらかじめキャッシュしておいたデータを先に渡す仕組みになっています。
handlerの中でUIを更新することで、ユーザの待機時間を最小限に抑えることができます。

UIに反映

最後に、生成したUIImageをUIImageViewに反映して…

完成です!

(UIについては様々な実装方法があると思うので、ここでは割愛します。コードの詳細はGitHubを参照してください。)

最後に

ここまで読んでいただきありがとうございました。

PhotoKitはかなり古くからあるフレームワークらしいのですが、今回調べて初めて知りました。
なかでもrequestImage()の美しさには驚きました。handlerが複数回呼ばれるのに合わせてUIImageViewが更新されていくのは非常にそれっぽさがありますね(個人的に)。

WWDC2020にて PHPickerViewController が紹介されていたようなので、そちらもまた触ってみようと思います。

ソースコードはGitHubにて公開しておりますので、「この実装ダメだよ!」とか「こっちのクラス(メソッド)使ったほうがいいよ!」等ございましたらコメントまたはPR頂ければ幸いです。

5
5
0

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
5
5