UICollectionView のヘッダーの内容が再描画されない

Cell と Header で構成される UICollectionViewController でコード(delegate 経由ではない)からヘッダーの内容を変更しようとすると直前の値が表示上に残って、その上に上書きされ値が重なって表示されてしまう問題に直面。

こんな感じ。

わかりづらいですが、Number of selected: の後ろの数値と、Deselect all が Select all と重なっている状態。

そして、ひとまずの前進に至るまでに半日消費。

複数日かからなくなっただけ成長しているのだろうか;;。(まるで成長していない)

原因としては、コードからヘッダーを参照するための呼び出し方法が適切ではなかった模様。

let reusableView = collection.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "collectionHeader", for: IndexPath(row: 0, section: 0))

問題のあるコード。(とはいえ参照するだけなら上記でも良いかと思います。多分)

dequeueReusableSupplementaryView で UICollectionElementKindSectionHeader を指定して UICollectionReusableView を取得して、そのあとのコードで取得した reusableView 経由で直接ヘッダー上のコントロールの値を変更していた。ら、UICollectionView の Cell などはキャッシュされるとかなんとからしく再描画されない。

(試してないけど) Cell の場合は UICollectionViewCell を継承してカスタムクラスを用意して、その中で prepareForReuse() を override すればゴミ表示問題は解決するとかの書き込みは見つけた。

ので、じゃぁヘッダーも一緒だろう、と思い UICollectionReusableView を継承したカスタムクラスから prepareForReuse() を override したけれど再描画されない。さらには prepareForReuse が呼ばれない(viewの初回表示時は呼ばれる)。dequeueReusableSupplementaryView を呼んだぐらいでは呼ばれない模様。ので明示的に呼んだけどやはり再描画はされない。

公式のヘルプを行ったり来たりして以下の記述を見つけた。

UICollectionViewLayout (公式)

  • Cells are the main elements positioned by the layout. Each cell represents a single data item in the collection. A collection view can have a single group of cells or it can divide those cells into multiple sections. The layout object’s main job is to arrange the cells in the collection view’s content area.
  • Supplementary views present data but are different than cells. Unlike cells, supplementary views cannot be selected by the user. Instead, you use supplementary views to implement things like header and footer views for a given section or for the entire collection view. Supplementary views are optional and their use and placement is defined by the layout object.

Supplementary view は Reusable View を元に構成されているっぽい(多分)し、Cell (UICollectionViewCell) は UICollectionReusableView を継承しているので、まあ当然だけど Cell にできてヘッダーにできないことはいくつかある。ヘッダーの IndexPath が求められれば reloadItems で更新できるかもしれないと思いつつヘッダーの IndexPath の取得方法はわからなかった・・・;。Cell の場合は reloadData とか reload 系で解決するけど、IndexPath が特定できればヘッダーもこれが最適解なのかもしれない(多分無理そう)。

ちなみに reloadSections をすると全体が再描画されて、望む結果にはならなかった(ちらつくし)。Cell で全体の更新の際に使う reloaData も、どこかの記事にパフォーマンスに影響あるから呼ぶのは必要最小限にした方が良いみたいな書き込みもあるし。CollectionViewController の描画系が最適化されたことによってこの辺りはより適切にコードを書かなければいけない系、かなぁ?

前置き長いけど一歩前進した方法は以下のようにヘッダーを参照する方法を変えると期待通りに変更できる模様。

let views = collection.visibleSupplementaryViews(ofKind: UICollectionElementKindSectionHeader)  // ヘッダーの一覧。フッターの場合は kind をフッター(UICollectionElementKindSectionFooter)にする。多分
for i in 0..<views.count {
    if let reusableView = view[i] {
        // 複数ヘッダーがある場合は reuseIdentifier で要識別
        // reusableView.subviews でヘッダー内のコントロールが参照できるのでそこで値を変更
    }
}

# 再利用品(reusable)の dequeue じゃないから効果(キャッシュされない)があるのだろうか・・・

これでひとまずは再描画まで行き着いた。が、ヘッダー上のボタンの再描画時にちらつくので、もしかすると適切な方法ではないかもしれない。あるいは layout の方でもっと適切な解決策があるのかもしれない。

先は長い・・・

ちらつくのは別の問題だった。以下で解決。

UIView.performWithoutAnimation {
    button.setTitle(title, for: .normal)
    button.layoutIfNeeded()
}

参考サイト:

UIButtonのタイトル文字をチラつかせず変更する + 便利プロパティ化

フォローする