xyk blog

最近は iOS 開発の記事が多めです。

UICollectionView で複数 Section の Header と Footer を表示する

検証環境:
Xcode 12.4
Swift 5.3.2

UICollectionView で複数セクションのヘッダーとフッターを表示する方法について。

f:id:xyk:20210226145730p:plain

StoaryBoard 設定

まず StoryBoard で Root View に UICollectionView 及び UICollectionViewCell のビューを設置しておく。
UICollectionView と UICollectionViewDelegateFlowLayout は @IBOutlet でコード上の定義と紐付ける。

f:id:xyk:20210226121457p:plain ヘッダービュー、フッタービュー(UICollectionReusableView)は StoryBoard 上で設置できるのは各1つのみで複数の設置はできない。(同じビューを使い回すのは可能)
なので複数のヘッダービュー、フッタービューを表示したい場合はコード側でやるしかない(と思われる)。
今回はNibファイル(.xib)で用意し、collectionView#register メソッドで登録する。

f:id:xyk:20210226114633p:plain

ヘッダー、フッターを表示する

ヘッダー、フッターを表示させるには、ビューのサイズを指定する必要がある。
1つは、UICollectionViewFlowLayout のheaderReferenceSizeまたはfooterReferenceSizeプロパティでサイズを指定する方法、もう1つは UICollectionViewDelegate のcollectionView(_:layout:referenceSizeForHeaderInSection:)またはcollectionView(_:layout:referenceSizeForFooterInSection:)のデリゲートメソッドでサイズを指定する方法のどちらか。
このサイズに 0 を設定するとヘッダー、フッターは表示されなくなる。

コード側で指定しなくても StoryBoard 側で UICollectionView にある以下のチェックを入れるとヘッダー、フッターが自動生成されるので、これでも表示させることができる。

f:id:xyk:20210226120006p:plain

この場合は先にも書いたとおり追加できるのはヘッダー、フッター各1つのみ。
今回は複数のヘッダー、フッターを追加するのが目的なのでここは使わない。

サンプルコード

import UIKit

class ViewController: UIViewController {
    
    enum Section: Int, CaseIterable {
        case section1 = 0
        case section2
    }
    
    var items1: [String] = ["1-1", "1-2", "1-3", "1-4", "1-5", "1-6"]
    var items2: [String] = ["2-1", "2-2", "2-3", "2-4", "2-5", "2-6"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionView.register(
                UINib(nibName: "Section1HeaderView", bundle: nil),
                forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                withReuseIdentifier: "Section1HeaderView")
            collectionView.register(
                UINib(nibName: "Section1FooterView", bundle: nil),
                forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
                withReuseIdentifier: "Section1FooterView")
            collectionView.register(
                UINib(nibName: "Section2HeaderView", bundle: nil),
                forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                withReuseIdentifier: "Section2HeaderView")
            collectionView.register(
                UINib(nibName: "Section2FooterView", bundle: nil),
                forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
                withReuseIdentifier: "Section2FooterView")
            
            collectionView.dataSource = self
            collectionView.delegate = self
            collectionView.alwaysBounceVertical = true
        }
    }
    
    @IBOutlet weak var flowLayout: UICollectionViewFlowLayout! {
        didSet {
            flowLayout.estimatedItemSize = .zero // Self-Sizing off
            flowLayout.scrollDirection = .vertical
            flowLayout.sectionHeadersPinToVisibleBounds = true
            flowLayout.sectionFootersPinToVisibleBounds = false
        }
    }
}

extension ViewController: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return Section.allCases.count
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        switch Section(rawValue: section)! {
        case .section1:
            return items1.count
        case .section2:
            return items2.count
        }
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
        cell.contentView.backgroundColor = (indexPath.item % 2 == 0) ? .systemGray4 : .systemGray6
        switch Section(rawValue: indexPath.section)! {
        case .section1:
            cell.titleLabel.text = items1[indexPath.item]
        case .section2:
            cell.titleLabel.text = items2[indexPath.item]
        }
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        switch kind {
        case UICollectionView.elementKindSectionHeader:
            switch Section(rawValue: indexPath.section)! {
            case .section1:
                return collectionView.dequeueReusableSupplementaryView(
                    ofKind: kind, withReuseIdentifier: "Section1HeaderView", for: indexPath)
            case .section2:
                return collectionView.dequeueReusableSupplementaryView(
                    ofKind: kind, withReuseIdentifier: "Section2HeaderView", for: indexPath)
            }
        case UICollectionView.elementKindSectionFooter:
            switch Section(rawValue: indexPath.section)! {
            case .section1:
                return collectionView.dequeueReusableSupplementaryView(
                    ofKind: kind, withReuseIdentifier: "Section1FooterView", for: indexPath)
            case .section2:
                return collectionView.dequeueReusableSupplementaryView(
                    ofKind: kind, withReuseIdentifier: "Section2FooterView", for: indexPath)
            }
        default:
            fatalError()
        }
    }
}

extension ViewController: UICollectionViewDelegate {
    
}

extension ViewController: UICollectionViewDelegateFlowLayout {
    
    // セルのサイズ
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 90, height: 90)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 10
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 10
    }
    
    // ヘッダーのサイズ
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        switch Section(rawValue: section)! {
        case .section1:
            return CGSize(width: collectionView.frame.width, height: 60)
        case .section2:
            return CGSize(width: collectionView.frame.width, height: 60)
        }
    }
    
    // フッターのサイズ
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
        switch Section(rawValue: section)! {
        case .section1:
            return CGSize(width: collectionView.frame.width, height: 30)
        case .section2:
            return CGSize(width: collectionView.frame.width, height: 30)
        }
    }
}

class CollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var titleLabel: UILabel!
}

class Section1HeaderView: UICollectionReusableView {
    
}

class Section1FooterView: UICollectionReusableView {
    
}

class Section2HeaderView: UICollectionReusableView {
    
}

class Section2FooterView: UICollectionReusableView {
    
}