How to create sticky headers with Compositional Layout

It is actually quite easy, but you need to know where to activate this behavior. Let's see how to enable pinned headers.

Published: Feb. 9, 2021
App Store

If you worked with TableView and recently switched to CollectionView, you may miss the automatic sticky section headers that TableView provided in the default configuration. These sticky / pinned headers are useful when presenting longer lists, because they never go out of view and user can always see what section are they currently viewing.

This post is going to be short, because creating this sort of headers is very straightforward with Compositional Layout.

If you want your header to be sticky, you need to set this on the header element itself. It is the pinToVisibleBounds property.

So in practice it looks like this:

let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
let headerElement = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)

// this activates the "sticky" behavior
headerElement.pinToVisibleBounds = true

And done! Quite nice, right? There are few possible gotchas you need to watch out for. For one, make sure the header has solid background color, otherwise user will think there is a bug when your header covers the items. And also you cannot have top inset set on the collection view with the contentInset property.

Compositional Layout - Sticky headers

Header views setup recap

Let's also briefly recap all the pieces you need to display headers (sticky or not) with Compositional Layout. Above we have defined the headerElement. The next step is to assign it to the section definition:

section.boundarySupplementaryItems = [headerElement]

The section is the NSCollectionLayoutSection type.

Next you need a method (or inline closure) that will be responsible for dequeuing the correct header element. Basic example looks like this:

func supplementary(collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView? {
    let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: StickyHeaderView.reuseIdentifier, for: indexPath) as! StickyHeaderView

    header.configure(with: "Sticky header \(indexPath.section + 1)")

    return header

And the last step is to configure the Diffable Data Source:

datasource.supplementaryViewProvider = { [unowned self] collectionView, kind, indexPath in
    return self.supplementary(collectionView: collectionView, kind: kind, indexPath: indexPath)

I have written previously about using these methods instead of closures and also about useful extensions which you can then use to register the header class with your collection view like this:

collectionView.register(header: StickyHeaderView.self)

And thats all the steps required. The complete code is available on GitHub.

Filip Němeček profile photo


Filip Němeček

iOS blogger and developer with interest in Python/Django. Want to see most recent projects? 👀

iOS blogger and developer with interest in Python/Django. Want to see most recent projects? 👀