設定 collection view section header / footer(supplementary view)的各種方法

使用 collection view 呈現畫面時,除了用一個個 cell 顯示資料,我們也可以添加 section header / footer(supplementary view),將畫面做一些分類。如下圖所示,第一個 Section 顯示角色清單,section header 顯示 Characters。第二個 Section 顯示項目清單,section header 顯示 Items。

接下來我們將學習如何在 collection view 加入 section header / footer,包含以下幾點的比較:

1. 從 storyboard,xib 或程式設計 header。

2. compositional layout & flow layout。

3. UICollectionViewDiffableDataSource。

設計 header / footer 畫面

方法 1: 從 storyboard 設計

點選 collection view,勾選 Accessories 的 Section Header / Section Footer。值得注意的,此方法只能設計一個 header,若有多個 section,需要搭配不同的 header,則須使用其它兩個方法。

勾選 Section Header 後,collection view 下長出 Collection Reusable View。

此時我們可以開始設計 header,比方調整它的高度和顏色,加入顯示文字的 label。

設定 Collection Reusable View 的 Reuse Identifier,在此我們輸入 Header。

新增繼承 UICollectionReusableView 的類別 HeaderView,將 Collection Reusable View 的 Class 設為 HeaderView,然後連結 label 的 outlet。

import UIKitclass HeaderView: UICollectionReusableView {

@IBOutlet weak var label: UILabel!
}

方法 2: 從 xib 設計

從 HeaderView.xib 設計 header 畫面,搭配繼承 UICollectionReusableView 的類別 HeaderView。

從 collection view 呼叫 register(_:forSupplementaryViewOfKind:withReuseIdentifier:) 註冊 header 的 id,指定 id 為 Header,到時候 collection view 將透過 id Header 產生 HeaderView。

override func viewDidLoad() {
super.viewDidLoad()

collectionView.register(UINib(nibName: "HeaderView", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header")

方法 3: 從程式設計

新增繼承 UICollectionReusableView 的類別 HeaderView,在類別裡設計畫面。

class HeaderView: UICollectionReusableView {

let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 23)
label.textAlignment = .center
label.backgroundColor = .systemYellow
return label
}()

override init(frame: CGRect) {
super.init(frame: frame)

backgroundColor = .systemTeal
addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
label.topAnchor.constraint(equalTo: topAnchor, constant: 10),
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10),
])
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

從程式設定 auto layout 還有其它的寫法,有興趣的朋友也可以參考以下連結。

https://cutt.ly/1WyHhIr

從 collection view 呼叫 register(_:forSupplementaryViewOfKind:withReuseIdentifier:) 註冊 header 的 id,指定 id 為 Header,到時候 collection view 將透過 id Header 產生 HeaderView。

override func viewDidLoad() {
super.viewDidLoad()

collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header")

定義 UICollectionViewDataSource 回傳 header / footer 的 function

定義 collectionView(_:viewForSupplementaryElementOfKind:at:),在 function 裡產生 header view 回傳。

override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! HeaderView
headerView.label.text = "可愛的小王子\(indexPath.section)"
return headerView
}

設定 header / footer 的 size

依據 layout 採用 compositional layout 或 flow layout,有兩種不同的做法。

compositional layout

生成 NSCollectionLayoutBoundarySupplementaryItem 時指定 header 的 size & 位置,然後設定 section 的 boundarySupplementaryItems。

func generateLayout() -> UICollectionViewLayout {
let fraction: CGFloat = 1 / 3
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(fraction), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(fraction))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

let section = NSCollectionLayoutSection(group: group)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(100))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]

let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}

flow layout

若是從 storyboard 設計 header,collection view 的 Header Size 即為它的高度。

若是從 xib 或程式設計 header,則須透過 UICollectionViewFlowLayout 的 headerReferenceSize 設定高度。在此 header 寬度將等於 collection view 的寬度,所以 width 可以傳 0。

func configureCellSize() {
let itemSpace: CGFloat = 3
let columnCount: CGFloat = 3

let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout
let width = floor((collectionView.bounds.width - itemSpace * (columnCount-1)) / columnCount)
flowLayout?.itemSize = CGSize(width: width, height: width)
flowLayout?.estimatedItemSize = .zero
flowLayout?.minimumInteritemSpacing = itemSpace
flowLayout?.minimumLineSpacing = itemSpace
flowLayout?.headerReferenceSize = CGSize(width: 0, height: 100)
}

view controller 的完整程式

compositional layout

import UIKitclass PrinceCollectionViewController: UICollectionViewController {


func generateLayout() -> UICollectionViewLayout {
let fraction: CGFloat = 1 / 3
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(fraction), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(fraction))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

let section = NSCollectionLayoutSection(group: group)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(100))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
override func viewDidLoad() {
super.viewDidLoad()

collectionView.collectionViewLayout = generateLayout()
     // 若 header 是從程式或 xib 設計,記得呼叫 collection view 的 register 註冊 header 的 id

}


// MARK: UICollectionViewDataSource

override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! HeaderView
headerView.label.text = "可愛的小王子\(indexPath.section)"
return headerView
}

override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 4
}


override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(PrinceCollectionViewCell.self)", for: indexPath) as! PrinceCollectionViewCell

let index = indexPath.section * 5 + indexPath.item
cell.imageView.image = UIImage(named: "pic\(index)")
return cell
}

}

flow layout

import UIKitclass PrinceCollectionViewController: UICollectionViewController {

func configureCellSize() {
let itemSpace: CGFloat = 3
let columnCount: CGFloat = 3

let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout
let width = floor((collectionView.bounds.width - itemSpace * (columnCount-1)) / columnCount)
flowLayout?.itemSize = CGSize(width: width, height: width)
flowLayout?.estimatedItemSize = .zero
flowLayout?.minimumInteritemSpacing = itemSpace
flowLayout?.minimumLineSpacing = itemSpace
flowLayout?.headerReferenceSize = CGSize(width: 0, height: 100)
     // 若是從 xib 或程式設計 header,則須透過 UICollectionViewFlowLayout 的 headerReferenceSize 設定高度 }


override func viewDidLoad() {
super.viewDidLoad()

configureCellSize()
// 若是從 xib 或程式設計 header,則須呼叫 collection view 的 register 註冊 header id

}

// MARK: UICollectionViewDataSource

override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! HeaderView
headerView.label.text = "可愛的小王子\(indexPath.section)"
return headerView
}

override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 4
}


override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(PrinceCollectionViewCell.self)", for: indexPath) as! PrinceCollectionViewCell

let index = indexPath.section * 5 + indexPath.item
cell.imageView.image = UIImage(named: "pic\(index)")
return cell
}
}

One more thing,UICollectionViewDiffableDataSource

若是使用 UICollectionViewDiffableDataSource 當 collection view 的 data source,則要透過 supplementaryViewProvider 設定 section header / footer,比方以下例子。

import UIKitclass PrinceCollectionViewController: UICollectionViewController {

func configureCellSize() {
let itemSpace: CGFloat = 3
let columnCount: CGFloat = 3

let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout
let width = floor((collectionView.bounds.width - itemSpace * (columnCount-1)) / columnCount)
flowLayout?.itemSize = CGSize(width: width, height: width)
flowLayout?.estimatedItemSize = .zero
flowLayout?.minimumInteritemSpacing = itemSpace
flowLayout?.minimumLineSpacing = itemSpace

// 若是從 xib 或程式設計 header,則須透過 UICollectionViewFlowLayout 的 headerReferenceSize 設定高度
}


var dataSource: UICollectionViewDiffableDataSource<Int, Int>!

func createDataSource() {
dataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(PrinceCollectionViewCell.self)", for: indexPath) as! PrinceCollectionViewCell
let index = indexPath.section * 5 + indexPath.item
cell.imageView.image = UIImage(named: "pic\(index)")
return cell
})

dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! HeaderView
headerView.label.text = "可愛的小王子\(indexPath.section)"
return headerView
}



}

override func viewDidLoad() {
super.viewDidLoad()

configureCellSize()
// 若是從 xib 或程式設計 header,則須呼叫 collection view 的 register 註冊 header id
createDataSource()
var snapshot = NSDiffableDataSourceSnapshot<Int, Int>()
for i in 0...3 {
snapshot.appendSections([i])
let number = 5 * i
snapshot.appendItems(Array(number...number+4))
}
dataSource.apply(snapshot, animatingDifferences: false)

}

}

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS / Flutter App 開發教室

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com