はじめに
UICollectionViewのレイアウトをUICollectionViewLayoutで作る機会があったため、その場合のHeaderとFooterの追加方法を整理しました。
完成イメージ
手順
UICollectionViewLayoutのサブクラスで生成した以下のレイアウトに対してHeaderとFooterを追加していきます。
※ 最終的なソースはこちらにあります。

Headerの追加
追加するHeader用のUICollectionReusableViewクラスのサブクラスを用意(今回はXibファイルも合わせて用意したため、collectionView側に登録も行います)
class CustomHeaderView: UICollectionReusableView {}
collectionView.register(UINib(nibName: String(describing: CustomHeaderView.self), bundle: .main),
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: String(describing: CustomHeaderView.self))
UICollectionViewDataSourceにて、Header生成のタイミングで用意したサブクラスを返します。(Footerを追加する場合もこのタイミングで返すようにします)
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader,
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: String(describing: CustomHeaderView.self),
for: indexPath) as? CustomHeaderView {
return headerView
}
return UICollectionReusableView()
}
ここまでではまだHeaderは表示されないので、UICollectionViewLayoutのサブクラス側で、HeaderのUICollectionViewLayoutAttributesを生成して、レイアウトに追加する処理を行います。
※ CellのAttributes生成のタイミングではHeaderの高さ分の表示スペースを考慮するように注意してください。
private func headerAttributes() {
guard let collectionView = collectionView else { return }
let indexPath = IndexPath(item: 0, section: 0)
let headerViewAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: indexPath)
headerViewAttribute.frame = CGRect(x: 0,
y: 0,
width: collectionView.bounds.size.width,
height: 80)
// 生成したLayoutAttributesを管理する配列にheader要素を追加
cachedAttributes.append(headerViewAttribute)
// headerの高さ分のContentSizeを追加
contentHeight = max(contentHeight, headerViewAttribute.frame.maxY)
}
Footerの追加
Headerと同じ流れでFooterも追加できます。
class CustomFooterView: UICollectionReusableView {}
collectionView.register(UINib(nibName: String(describing: CustomFooterView.self), bundle: .main),
forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
withReuseIdentifier: String(describing: CustomFooterView.self))
headerと同じタイミングでサブクラスをDataSourceに返すようにします。
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
return collectionView.dequeueReusableSupplementaryView(ofKind: kind,
withReuseIdentifier: String(describing: CustomHeaderView.self),
for: indexPath) as? CustomHeaderView ?? UICollectionReusableView()
case UICollectionView.elementKindSectionFooter:
return collectionView.dequeueReusableSupplementaryView(ofKind: kind,
withReuseIdentifier: String(describing: CustomFooterView.self),
for: indexPath) as? CustomFooterView ?? UICollectionReusableView()
default:
return UICollectionReusableView()
}
}
private func footerAttributes() {
guard let collectionView = collectionView else { return }
let indexPath = IndexPath(item: 0, section: 0)
let footerViewAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, with: indexPath)
footerViewAttribute.frame = CGRect(x: 0,
y: contentHeight, // header, cell などの要素分の高さ
width: collectionView.bounds.size.width,
height: 80)
// 生成したLayoutAttributesを管理する配列にfooter要素を追加
cachedAttributes.append(footerViewAttribute)
// footerの高さ分のContentSizeを追加
contentHeight = max(contentHeight, footerViewAttribute.frame.maxY)
}
今回はCollectionViewのsectionは0個でheaderやfooterは一つずつでしたが、section別に複数のheaderやfoooterが必要な場合は、UICollectionViewLayoutAttributesの生成時に渡すIndexPathで調整すればいいようです。
HeaderやFooterの高さをController側から渡してみる
上の例ではHeaderやFooterの高さはハードコーディングしていましたが、動的なレイアウトの変更も考慮して、Controller側から高さの値をUICollectionViewLayoutのサブクラスに渡すようにしてみます。
手順としては、UICollectionViewLayoutのサブクラス側でprotocolを用意してやり、Controller側からデリゲートで値を返すようにしてみます。
protocol CustomLayoutDelegate: class {
func headerViewHeight(_ indexPath: IndexPath) -> CGFloat
func footerViewHeight(_ indexPath: IndexPath) -> CGFloat
}
class CustomLayout: UICollectionViewLayout {
weak var delegate: CustomLayoutDelegate?
~~~~ 以下省略 ~~~~~
}
if let customLayout = collectionView.collectionViewLayout as? CustomLayout {
PhotoListViewLayout.delegate = self
}
extension ViewController: CustomLayoutDelegate {
func headerViewHeight(_ indexPath: IndexPath) -> CGFloat {
// 状況に応じてHeaderの高さを返す
return view.frame.size.height * 0.1
}
func footerViewHeight(_ indexPath: IndexPath) -> CGFloat {
// 状況に応じてFooterの高さを返す
return view.frame.size.height * 0.1
}
}
Cotroller側で返した値を、Header、FooterのUICollectionViewLayoutAttributesを生成するタイミングで受け取って高さの値に使用します。
let headerViewHeight = delegate?.headerViewHeight(indexPath) ?? 0
let footerViewHeight = delegate?.footerViewHeight(indexPath) ?? 0
これでレイアウトが更新される度にHeaderやFooterの高さを動的に変更することができそうです。👍
ソースコード
以下のリポジトリに最終版のソースを置いてあります。
https://github.com/ddd503/CollectionView-Header-Footer-Sample