IndexPathに気をつけろ!

はじめに

UICollectionViewでレイアウト計算を行う際にIndexPathを自作して問題が起きました。

今回実現したかったことは、UICollectionViewをスクロールして対象のcellが画面内に100%表示されているかを判定することです。
CollectionViewのcellが画面に表示するかの判定処理は以下です。

現在画面に表示されている部分のCollectionViewの座標はcollectionView.boundsで取れるのでそこで取っています。今回の要件的にY座標でスクロールはしないものだったのでX座標だけで判定しています。

private func isPerfectVisibleCell(cellRect: CGRect) -> Bool {
    let collectionViewRect = collectionView.bounds

    let collectionViewMaxX = collectionViewRect.maxX
    let collectionViewMinX = collectionViewRect.minX
    let cellMaxX = cellRect.maxX
    let cellMinX = cellRect.minX

    return cellMinX > collectionViewMinX && cellMaxX < collectionViewMaxX
}

問題の箇所

実際に処理行う箇所ですか以下のようなコードでcollectionViewのlayoutAttributesForItemに自作のIndexPathを渡してセルのCGRectを取得しようとしていました。

一見何も問題ないように見えますが、cellRectで取得できるCGRectの値が正しいものではありませんでした。

for i in 0..<viewModel.cellViewModel.count {
    let indexPath = IndexPath(row: i, section: 0)
    guard let cellRect: CGRect = collectionView.layoutAttributesForItem(at: indexPath)?.frame
    else {
        return
    }
    
    if isPerfectVisibleCell(cellRect: cellRect) {
        // やりたい処理処理
    }
}

ここからはあくまで推測でしかないのですが、collectionView.layoutAttributesForItemで引数に渡すIndexPathは中身のrowのcountを使ってcellを見つけるのではなく、collectionViewの内部に保持しているcellのアドレスをIndexPathを使って参照して持ってきているんじゃないかと思っています。

解決方法

cellを作成する際に使用するIndexPathを変数として保持しておいてそちらを使ってcollectionView.layoutAttributesForItemを呼び出すことで正しいcellの座標とサイズを取得することができました。

indexPathList.forEach { indexPath in
    guard let cellRect: CGRect = collectionView.layoutAttributesForItem(at: indexPath)?.frame
    else {
        return
    }
    
    if isPerfectVisibleCell(cellRect: cellRect) {
        // やりたい処理処理
    }
}

※cellの生成は画面回転やreload時にも呼ばれてしまうので、変数として保持しているIndexPathにすでに保存してあるIndexPathはappendしない処理を書いておかないと重複して保持してしまうので注意!!