6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-05-02

はじめに

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

完成イメージ

Header Footer

手順

UICollectionViewLayoutのサブクラスで生成した以下のレイアウトに対してHeaderとFooterを追加していきます。
※ 最終的なソースはこちらにあります。

Start

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を生成して、レイアウトに追加する処理を行います。

※ 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は表示できました。
Header

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の表示もできました。
Footer

今回は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

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?