【Swift5】UICollectionViewLayoutのサブクラスでHeaderとFooterを生成する

# はじめに
UICollectionViewのレイアウトをUICollectionViewLayoutで作る機会があったため、その場合のHeaderとFooterの追加方法を整理しました。

## 完成イメージ
<img width="200" alt="Header" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254298/edffa718-0f10-e13a-7846-ac31842bae8a.png"> <img width="200" alt="Footer" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254298/434e0655-787b-fe19-5073-99efcf1be6c6.png">

# 手順
UICollectionViewLayoutのサブクラスで生成した以下のレイアウトに対してHeaderとFooterを追加していきます。<br>※ 最終的なソースは[こちら](https://github.com/ddd503/CollectionView-Header-Footer-Sample)にあります。

<img width="180" alt="Start" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254298/2df6be59-5733-69e5-6c92-bba560648f74.png">

## Headerの追加
追加するHeader用のUICollectionReusableViewクラスのサブクラスを用意（今回はXibファイルも合わせて用意したため、collectionView側に登録も行います）

```CustomHeaderView.swift
class CustomHeaderView: UICollectionReusableView {}
```

```ViewController.swift
collectionView.register(UINib(nibName: String(describing: CustomHeaderView.self), bundle: .main),
                                forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                                withReuseIdentifier: String(describing: CustomHeaderView.self))
```


UICollectionViewDataSourceにて、Header生成のタイミングで用意したサブクラスを返します。（Footerを追加する場合もこのタイミングで返すようにします）

```ViewController.swift
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を生成して、レイアウトに追加する処理を行います。<br>
※ CellのAttributes生成のタイミングではHeaderの高さ分の表示スペースを考慮するように注意してください。

```CustomLayout.swift 
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)
}

```

これでHeaderは表示できました。
<img width="180" alt="Header" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254298/edffa718-0f10-e13a-7846-ac31842bae8a.png">

## Footerの追加
Headerと同じ流れでFooterも追加できます。

```CustomFooterView.swift 
class CustomFooterView: UICollectionReusableView {}
```

```ViewController.swift
collectionView.register(UINib(nibName: String(describing: CustomFooterView.self), bundle: .main),
                        forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
                        withReuseIdentifier: String(describing: CustomFooterView.self))
```

headerと同じタイミングでサブクラスをDataSourceに返すようにします。

```ViewController.swift
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()
        }
    }
```

```CustomLayout.swift
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)
}

```

これでFooterの表示もできました。
<img width="180" alt="Footer" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254298/434e0655-787b-fe19-5073-99efcf1be6c6.png">

今回はCollectionViewのsectionは0個でheaderやfooterは一つずつでしたが、section別に複数のheaderやfoooterが必要な場合は、UICollectionViewLayoutAttributesの生成時に渡すIndexPathで調整すればいいようです。

## HeaderやFooterの高さをController側から渡してみる
上の例ではHeaderやFooterの高さはハードコーディングしていましたが、動的なレイアウトの変更も考慮して、Controller側から高さの値をUICollectionViewLayoutのサブクラスに渡すようにしてみます。

手順としては、UICollectionViewLayoutのサブクラス側でprotocolを用意してやり、Controller側からデリゲートで値を返すようにしてみます。

```CustomLayout.swift
protocol CustomLayoutDelegate: class {
    func headerViewHeight(_ indexPath: IndexPath) -> CGFloat
    func footerViewHeight(_ indexPath: IndexPath) -> CGFloat
}
```

```CustomLayout.swift
class CustomLayout: UICollectionViewLayout {

    weak var delegate: CustomLayoutDelegate?

    ~~~~ 以下省略 ~~~~~
}
```

```ViewController.swift
if let customLayout = collectionView.collectionViewLayout as? CustomLayout {
            PhotoListViewLayout.delegate = self
        }
```

```ViewController.swift
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を生成するタイミングで受け取って高さの値に使用します。

```CustomLayout.swift
 let headerViewHeight = delegate?.headerViewHeight(indexPath) ?? 0
 let footerViewHeight = delegate?.footerViewHeight(indexPath) ?? 0
```

これでレイアウトが更新される度にHeaderやFooterの高さを動的に変更することができそうです。👍

## ソースコード
以下のリポジトリに最終版のソースを置いてあります。
https://github.com/ddd503/CollectionView-Header-Footer-Sample