まえおき
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
メソッドがあるので、次の画像を生成する前にキャンセルすることで解決しました。
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
}
}
class ThumbnailCell: UICollectionViewCell {
// サムネを表示するImageView
@IBOutlet weak var thumbnailImageView: UIImageView!
// Cellで入れる画像のPHImageRequestID
public var requestId: PHImageRequestID?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
まとめ
iOSたのちい