
LINEやTwitter、Pinterestなど、写真を扱う多くのアプリにはカスタマイズされた画像選択UIが実装されています。
今回はこのようなUIをPhotoKitを使って実装してみます。
デフォルトのPickerControllerはカスタマイズ性が低い
"Swift 画像 選択" などで検索すると、UIImagePickerControllerによる実装がいくつかヒットします。
何も考えずにパパッと実装したい場合には便利なのですが、UIや表示内容のカスタマイズ性が制限されるため、デザインの選択肢が狭まってしまいます。
PhotoKitでフォトライブラリにアクセスする
そこで、PhotoKitを用いて直接フォトライブラリからデータを取得してUIに設定するという方法を考えます。
ユーザにアクセスを許可してもらう
まずはUIImagePickerViewControllerと同様、Info.plistにNSPhotoLibraryUsageDescriptionを追加します。
(ここで記述した文字列がアクセス要求の際に表示されます)

次に、画像を読み込みたいタイミングで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には以下のように様々なオプションを設定できます。(引用: 公式ドキュメント)
が、今回は単純に撮影日時の降順に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. ...
したがって、mapやfilterで処理するといったことはできません。
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頂ければ幸いです。
