Help us understand the problem. What is going on with this article?

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

まえおき

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

Xcode 11.0 GM

追記

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

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

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

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

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

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

// 取得した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のサイズが思った通りにならない

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

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

まとめ

これらのコードをはgitに置いてあります。
よろしければ参考にしてください。

iOSたのちい

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away