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

![Custom UIs](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492914/4f0799d4-4680-89ca-782a-905a4415263b.jpeg)
LINEやTwitter、Pinterestなど、写真を扱う多くのアプリには**カスタマイズされた画像選択UI**が実装されています。
今回はこのようなUIを`PhotoKit`を使って実装してみます。

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

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

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

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

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

まずはUIImagePickerViewControllerと同様、`Info.plist`に`NSPhotoLibraryUsageDescription`を追加します。
(ここで記述した文字列がアクセス要求の際に表示されます)
![スクリーンショット 2021-01-19 11.27.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492914/320c6869-c577-35eb-fa87-2e0f9e92c3cc.png)

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

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


```swift
// フォトライブラリへのアクセスを要求する
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:)`を呼び出すと、このようなプロンプトが表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492914/26ace5ac-28d9-2fbf-3faf-4c2b7a599e94.png" width="300">
ここで「**写真を選択…**」をタップすると `.limited`、
「**すべての写真へのアクセスを許可**」をタップすると`.authorized`、
「**許可しない**」をタップすると`.denied`が返ります。

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

フォトライブラリの内容を取得するには`fetchAssets(with: PHFetchOptions)`を使用します。
`PHFetchOptions`には以下のように様々なオプションを設定できます。(引用: [公式ドキュメント](https://developer.apple.com/documentation/photokit/phfetchoptions#1965657))

![スクリーンショット 2021-01-19 16.15.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492914/c325dbc5-0e3d-8dd4-fef3-76f335cef7e2.png)


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

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

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

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

しかし[公式ドキュメント](https://developer.apple.com/documentation/photokit/phfetchresult)によると、配列のように**全ての値がその瞬間用意されているわけではない**ようです。

> ... 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`に変換できます。

```swift
// (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
        }
    }
}
```

[公式ドキュメント](https://developer.apple.com/documentation/photokit/phimagemanager/1616964-requestimage)によると、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に反映して…

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492914/bc2629ac-d714-0423-fc41-3643440e6d97.gif" width="300">

完成です!

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

## 最後に

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

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

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

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