UICollectionView で複数 Section の Header と Footer を表示する
検証環境:
Xcode 12.4
Swift 5.3.2
UICollectionView で複数セクションのヘッダーとフッターを表示する方法について。
StoaryBoard 設定
まず StoryBoard で Root View に UICollectionView 及び UICollectionViewCell のビューを設置しておく。
UICollectionView と UICollectionViewDelegateFlowLayout は @IBOutlet でコード上の定義と紐付ける。
ヘッダービュー、フッタービュー(UICollectionReusableView)は StoryBoard 上で設置できるのは各1つのみで複数の設置はできない。(同じビューを使い回すのは可能)
なので複数のヘッダービュー、フッタービューを表示したい場合はコード側でやるしかない(と思われる)。
今回はNibファイル(.xib)で用意し、collectionView#register メソッドで登録する。
ヘッダー、フッターを表示する
ヘッダー、フッターを表示させるには、ビューのサイズを指定する必要がある。
1つは、UICollectionViewFlowLayout のheaderReferenceSize
またはfooterReferenceSize
プロパティでサイズを指定する方法、もう1つは UICollectionViewDelegate のcollectionView(_:layout:referenceSizeForHeaderInSection:)
またはcollectionView(_:layout:referenceSizeForFooterInSection:)
のデリゲートメソッドでサイズを指定する方法のどちらか。
このサイズに 0 を設定するとヘッダー、フッターは表示されなくなる。
コード側で指定しなくても StoryBoard 側で UICollectionView にある以下のチェックを入れるとヘッダー、フッターが自動生成されるので、これでも表示させることができる。
この場合は先にも書いたとおり追加できるのはヘッダー、フッター各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 { }