Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a UICollectionView global header and/or footer?

I’ve been trying to create a UICollectionView header that would stick on top of my collection view. I’m using UICollectionViewCompositionalLayout.

I’ve tried multiple approaches: using a cell, using a section header and try to mess with insets and offsets to position it correctly relative to my content… And even adding a view on top of the collection view that would listen to the collection view’s scroll view’s contentOffset to position itself at the right place. But none of these approaches are satisfying. They all feel like a hack.

I’ve been doing some research and apparently you’d have to sublcass UICollectionViewLayout which is super tedious and seems overkill to just have a header, but one that is global to the whole collection view.

like image 301
Pomme2Poule Avatar asked Sep 17 '25 14:09

Pomme2Poule


1 Answers

TL;DR

UICollectionViewCompositionalLayout has a configuration property which you can set by creating an UICollectionViewCompositionalLayoutConfiguration object. This object has some really nice and useful functionality such as the boundarySupplementaryItems property.

From the docs:

An array of the supplementary items that are associated with the boundary edges of the entire layout, such as global headers and footers.

Bingo. Set this property and do the necessary wiring in your datasource and you should have your global header.

Code Example

Here, I'm declaring a global header in my layout. The header is a segmented control inside a visual effect view, but yours can be any subclass of UICollectionReusableView.

enum SectionLayoutKind: Int, CaseIterable {
    case description
}

private var collectionView: UICollectionView! = nil

override func viewDidLoad() {
    super.viewDidLoad()

    collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
}

static func descriptionSection() -> NSCollectionLayoutSection {
    // Instantiate and return a `NSCollectionLayoutSection` object.
}

func createLayout() -> UICollectionViewLayout {
    let layout = UICollectionViewCompositionalLayout {
        (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
        // Create your section
        // add supplementaries such as header and footers that are relative to the section…
        guard let layoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil }
        
        let section: NSCollectionLayoutSection
        switch layoutKind {
        case .description:
            section = Self.descriptionSection()
        }
        
        return section
    }
    
    /*
     ✨ Magic starts HERE:
    */
    let globalHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(44))
    let globalHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: globalHeaderSize, elementKind: Constants.HeaderKind.globalSegmentedControl, alignment: .top)
    // Set true or false depending on the desired behavior
    globalHeader.pinToVisibleBounds = true
    
    let config = UICollectionViewCompositionalLayoutConfiguration()
    /*
     If you want to do spacing between sections.
     That's another big thing this config object does.
     If you try to define section spacing at the section level with insets,
     the spacing is between the items and the standard headers.
    */
    config.interSectionSpacing = 20
    config.boundarySupplementaryItems = [globalHeader]
    layout.configuration = config
    /*
     End of magic. ✨
    */
    
    return layout
}

struct Constants {
    struct HeaderKind {
        static let space = "SpaceCollectionReusableView"
        static let globalSegmentedControl = "segmentedControlHeader"
    }
}

Supplementary code for the data source part:

let globalHeaderRegistration = UICollectionView.SupplementaryRegistration<SegmentedControlReusableView>(elementKind: Constants.HeaderKind.globalSegmentedControl) { (header, elementKind, indexPath) in
    // Opportunity to further configure the header
    header.segmentedControl.addTarget(self, action: #selector(self.onSegmentedControlValueChanged(_:)), for: .valueChanged)
}

dataSource.supplementaryViewProvider = { (view, kind, indexPath) in
    if kind == Constants.HeaderKind.globalSegmentedControl {
        return self.collectionView.dequeueConfiguredReusableSupplementary(using: globalHeaderRegistration, for: indexPath)
    } else {
        // return another registration object
    }
}
like image 137
Pomme2Poule Avatar answered Sep 20 '25 05:09

Pomme2Poule