UICollectionViewで画像一覧を出した時のメモ

## まえおき

UICollectionViewとPhotosフレームワークを初めて使ったので、つまづきそうだなーと思ったところのメモです。
写真appから画像一覧を取得してサムネイルを並べることを目的としています。

Xcode 11.0 GM

### 追記
* 2019/9/18 例として書いたコードのパフォーマンスを改善しました

## 画像を取得した時のつまづき

`Photos`フレームワークで取得します。

`PHAsset.fetchAssets`でフェッチした結果の`PHFetchResult`から写真の`PHAsset`を取得します。この時アクセス権の確認アラートが出現しますが、OKを押しても写真は読み込まれませんでした。

どうやらアラートはアセットのフェッチによるものではなく、アセットへのアクセス行為（PHAssetの取得部分）によるもののようです。
フェッチでもアクセス権は必要ですからフェッチは失敗しています。取得には失敗していますから、ここで許可が出ようとも画像は表示できません。

よって`PHPhotoLibrary.requestAuthorization`を使用することで、アクセス権が取得できた、もしくは既にアクセス権があるときだけフェッチを開始するようにします。

```swift
// 取得したPHAssetを格納する配列
var photoAssets: [PHAsset] = []

PHPhotoLibrary.requestAuthorization { (status) in
    switch status {
    case .authorized:
        // 許可された場合のみ読み込み開始
        loadPhotos()
    case .denied:
        // 拒否されている場合アラートを出すなり設定appへ誘導するなり
    default:
        // このほかに restricted と determined が存在する
    }
}

func loadPhotos() {
    // 取得するものをimageに指定
    // 取得したい順番などあれば options に指定する
    let assets: PHFetchResult = PHAsset.fetchAssets(with: .image, options: nil)
    // PHAssetを一つ一つ格納
    assets.enumerateObjects { [weak self] (asset, index, stop) in
        self?.photoAssets.append(assets[index])
    }
}
```

これでアクセス権を考慮して一覧を取得できました。
あとは`PHAsset`から`PHImageManager`を使って`UIImage`として取り出すだけです。


## UICollectionViewでサムネ表示した時のつまづき

### Cellのサイズが思った通りにならない

サムネが４列になるようにCellのサイズを指定しました。

```swift
func setup() {
    // Cellのレイアウト
    let layout = UICollectionViewFlowLayout()
    // 列の分割数
    let columnsCount = 4
    // CollectionViewの枠とCell達の間の隙間
    let sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    // Cellの左右のスペースの最小値
    let minimumInteritemSpacing: CGFloat = 5.0
    // 行の上下のスペースの最小値
    let minimumLineSpacing: CGFloat = 5.0
    let viewWidth = thumbnailCollectionView.frame.size.width
    // Cellのサイズ
    // sectionInsetとminimumInteritemSpacingで決めたスペースからcolumnsCountになるよう調整
    let cellWidth = floor((viewWidth - sectionInset.left * 2 - minimumInteritemSpacing * CGFloat(columnsCount - 1)) / CGFloat(columnsCount))
    layout.itemSize = CGSize(width: cellWidth, height: cellWidth)
    layout.minimumInteritemSpacing = minimumInteritemSpacing
    layout.minimumLineSpacing = minimumLineSpacing
    layout.sectionInset = sectionInset
    thumbnailCollectionView.collectionViewLayout = layout
}
```

これを`viewDidLoad()`で呼んでしまっていたのが原因でした。thumbnailCollectionViewのサイズが決まっていないためです。
~~`viewDidLayoutSubviews()`で呼ぶようにしたところうまくいきました。~~
**追記修正**
このコード通りに実装して触っていたところ、なーんかスクロールが重たい…というかカクカクする？
先輩に質問したところ、
「`viewDidLayoutSubviews()`がたくさん呼ばれてるからかも。ブレークポイントはって調べてみては？」
とのアドバイスをいただき調査したところ…

めっちゃよばれる〜

かくついていたのはスクロールするたびに`UICollectionViewFlowLayout()`のインスタンスを生成していたことが原因のようでした。
Cellや間隔のレイアウトを決めるのは一度で良いため、フラグで制御することにしました。サクサクになった〜

本当は「Viewのサイズが決まった後に一度だけ呼ばれる」都合の良いメソッドに記述したいのですが…

### 素早くスクロールした時にCellの挙動がおかしい

iOS13でスクロールバーを摘んで高速スクロール？が可能になりました。
普通にスクロールする分には問題ないのですが…高速でスクロールした後、なにやらCellの中身が目まぐるしく変化しています。紙芝居みたい。

Cellは使い回しますから、初期化がができていないのかな？と思い次の画像を読む前に前画像を`nil`にする処理を入れてみましたが…変わらず

原因は高速スクロールによって、`PHImageManager`による画像生成が追いていなかったことでした。
前の画像の生成が終わる前に次の画像が…ということを繰り返し、そのCellに対する生成タスクが溜まっていき、スクロールが止まった時にタスクが順次処理されて順に画像が表示された、ということでした。

`PHImageManager`には画像生成をキャンセルする`cancelImageRequest`メソッドがあるので、次の画像を生成する前にキャンセルすることで解決しました。

```ThumbnailListViewController.swift
extension ThumbnailListViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return photoAssets.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = thumbnailCollectionView.dequeueReusableCell(withReuseIdentifier: "ThumbnailCell", for: indexPath) as? ThumbnailCell else {
            return UICollectionViewCell()
        }
        // cellを使い回す前に前のrequestをキャンセルして画像を削除
        if let requestId = cell.requestId {
            imageManager.cancelImageRequest(requestId)
        }
        cell.thumbnailImageView.image = nil
        let item = photoAssets[indexPath.row]
        cell.requestId = imageManager.requestImage(for: item,
                                  targetSize: CGSize(width: 480, height: 480),
                                  contentMode: .aspectFit,
                                  options: nil) { (image, info) in
                                    cell.thumbnailImageView.image = image
        }
        return cell
    }
}
```
```ThumbnailCell.swift
class ThumbnailCell: UICollectionViewCell {
    // サムネを表示するImageView
    @IBOutlet weak var thumbnailImageView: UIImageView!
    // Cellで入れる画像のPHImageRequestID
    public var requestId: PHImageRequestID?
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }
}
```

## まとめ


iOSたのちい