【swift】reloadDataするとCollectionViewのcontentOffsetがバグる件の対応

こんにちは。

Self-sizingセルを活用してCollectionViewを作成すると、スクロールした時にもうreloadDataを利用するとoffsetがジャンプしてしまうバグに悩まされました。
その対応、備忘録です。

CollectionVIew.contentOffset.yがジャンプする

上記のリロードをすると、

  • 画面先頭へ意図しないジャンプCGPoint(x: 0.0, y: 0.0)
  • 先頭までは行かないがy座標がズレる

という問題がありました。

これが発生していた条件として

  • CollectionViewFlowLayoutの利用
  • Cellをself-sizing / Dynamic height / サイズ可変等を利用

すると発生していました。

解決策

結論として、estimatedSizeの計算がしっかりされておらず座標ずれが発生していたようです。
そのため、CollectionViewFlowLayout.estimatedItemSize = .automaticをやめて、面倒ですがsizeForItemAtでサイズを計算して返してあげることで解消できました。

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    // 直接計算してreturnする
}

この結論に至るために試してみたことは以下です。

試したこと

setContentOffset()を利用し強制的に座標を戻す

一見するとこれで行けそうですし、割とこれ系のバグのワークアラウンドとしてどこでも紹介されてたりします。
が、これを行っても効果はありませんでした。

let offset = collectionView.contentOffset
collectionView.reloadData()
collectionView.setContentOffset(offset, animated: false)

しっかりとした原因究明はできなかったのですが、デバッグしてみるとsetContentOffsetがうまく効いていないようでした。

performBatchUpdatesやDispatchQueue.mainの利用

こちらも特に改善はみられませんでした、setContentOffsetを強制的にメインスレッド実行したりperformBatchUpdatesで実行してもだめ、、、

let offset = collectionView.contentOffset
collectionView.reloadData()

DispatchQueue.main.async {
    self.collectionView.setContentOffset(offset, animated: false)
}

collectionView.performBatchUpdates(nil) { [weak self] _ in
    self?.collectionView.setContentOffset(offset, animated: false)
}

reloadSections()の利用

これが一番期待できそうでしたが、reloadSections, reloadItemsを利用すると座標ズレは解消できましたがヘッダー・フッターの位置がバグってしまうバグに今度は遭遇。
このバグを誤魔化す術が見つからなかったので利用を断念。
それとcontentOffsetズレを解消されるのですが、reloadを欠けた時に一瞬再構成?のアニメーションというかグラデーションが入るので、う〜んという感じ。

同様にメインスレッドで調整してもダメでした。

セルの再構成

Autolayoutのwarningが出ていたり、割と複雑な構成のセルを利用していたこともあり、セルを0から再構成することを試みましたが効果なし。

compositionalLayout + DiffableDataSourceの利用

iOS13から利用できる二つを試してみましたが座標ズレには効果なし。

そもそも、私のプロジェクトではAPIレスポンスに応じてセルのサイズが変わるというケースで、このような動的なデータによるレイアウト作成はあまりCompositionalLayoutには向かないかもしれないことに気付きました。
compositionalLayoutはItemやGroupという概念を利用してパターン化したり全体的なSection構成をしたりするのですが、作成の段階でサイズを決めるので固定サイズをレイアウトする際は非常に使いやすいです。
ですが上記のようなケースの場合、データを渡してあげて、それをベースにパターンを作成して構成しないと行けないので少々コストがかかります。
それが例えば追加取得や無限スクロールがある場合だと、その度にデータを渡して再構成しなければいけないので、あまり向かいないなぁと。
セルやセクションの個数やレイアウトパターンが決まっている場合は断然CompositionalLayoutの方がFlowLayoutよりいいですね。

compositionalLayout + DiffableDataSourceの使い方はこちらで説明しています。

【Swift】iOS13のCollectionView、CompositionalLayoutsでモダンにレイアウトする
【Swift】Compositinal Layouts を複数の型に適応させる方法
【Swift】CompotionalLayouts でセクションごとのcollectionView/cellにアクセスする方法

結論

上記のような方法を試してみてダメだったので、この不具合が出たらestimatedSizeに着目してみてください。
estimatedSizeがしっかり適用される方法があればself-sizingでもいけそうですが、検証がまだ行き届いていません。
なので手っ取り早くサイズ計算して返してあげると改善されました。