2

I am working on a project where I am using UICollectionView with a compositional layout. I am trying to add corner radius to the section headers of my UICollectionView. I am using UICollectionViewCompositionalLayout to create sections, and I want each section header to have a different corner radius, color, and design.

Here is an example of my code:

    // Creating the compositional layout
    let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
        // configuring sections and items
    }

    // Registering section header
    let headerRegistration = UICollectionView.SupplementaryRegistration
        <HeaderCollectionViewCell>(elementKind: UICollectionView.elementKindSectionHeader) {
        supplementaryView, string, indexPath in
        // configuring header view
    }

    collectionView.collectionViewLayout = layout
    collectionView.register(HeaderCollectionViewCell.self, 
        forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, 
        withReuseIdentifier: "HeaderCollectionViewCell")

How can I add different corner radius, colors, and designs for each section header of my UICollectionView using UICollectionViewCompositionalLayout? Any help or guidance on this issue would be greatly appreciated. Thank you!

I've attempted to decorate it by creating decorators within sections, but it seems that I can only change the colors and not the corner radius. Moreover, the colors I've added don't adhere to the section constraints. For instance, if I add horizontal padding to a section, the section color overflows beyond that padding and expands to the width of the screen

1 Answer 1

3

You can do that in your registration block.

For example:

    let headerRegistration = UICollectionView.SupplementaryRegistration
    <TitleSupplementaryView>(elementKind: CompColumnsVC.sectionHeaderElementKind) {
        (supplementaryView, string, indexPath) in
        
        supplementaryView.label.text = "\(string) for section \(indexPath.section)"
        
        // default background color / corner radius /
        //  text color / border color / border width
        supplementaryView.backgroundColor = .lightGray
        supplementaryView.layer.cornerRadius = 0.0
        
        supplementaryView.layer.borderColor = UIColor.black.cgColor
        supplementaryView.layer.borderWidth = 1.0
        supplementaryView.label.textColor = .black
        
        // specific background color / corner radius /
        //  text color / border color / border width
        //  for sections 0, 1, 2 (all the rest use default
        switch indexPath.section {
        case 0:
            supplementaryView.backgroundColor = .cyan
            supplementaryView.layer.cornerRadius = 6.0

        case 1:
            supplementaryView.backgroundColor = .systemBlue
            supplementaryView.label.textColor = .white
            supplementaryView.layer.cornerRadius = 12.0

        case 2:
            supplementaryView.backgroundColor = .yellow
            supplementaryView.layer.cornerRadius = 16.0
            supplementaryView.layer.borderWidth = 0.0
            supplementaryView.layer.borderColor = UIColor.red.cgColor

        default:
            ()
        }
        
    }

gives me this result:

enter image description here

Here's a complete example, based on

  • Compositional Layout ->
  • Advanced Layouts ->
  • Supplementary Views ->
  • Section Headers/Footers

from Apple's Implementing Modern Collection Views sample app:


Simple single-label collection view cell:

class SimpleCell: UICollectionViewCell {
    
    let theLabel: UILabel = {
        let v = UILabel()
        return v
    }()
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        theLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(theLabel)
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            theLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
            theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
            theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0),
        ])
        
        contentView.layer.borderColor = UIColor(white: 0.9, alpha: 1.0).cgColor
        contentView.layer.borderWidth = 1.0
    }
}

Reusable view for section headers:

class TitleSupplementaryView: UICollectionReusableView {
    
    let label = UILabel()
    let bkgView = UIView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    func commonInit() {
        
        label.translatesAutoresizingMaskIntoConstraints = false
        bkgView.translatesAutoresizingMaskIntoConstraints = false
        bkgView.addSubview(label)
        addSubview(bkgView)
        
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: bkgView.topAnchor, constant: 8.0),
            label.leadingAnchor.constraint(equalTo: bkgView.leadingAnchor, constant: 8.0),
            label.trailingAnchor.constraint(equalTo: bkgView.trailingAnchor, constant: -8.0),
            label.bottomAnchor.constraint(equalTo: bkgView.bottomAnchor, constant: -8.0),
            
            bkgView.topAnchor.constraint(equalTo: topAnchor, constant: 4.0),
            bkgView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
            bkgView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
            bkgView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4.0),
        ])

        label.font = .systemFont(ofSize: 14.0, weight: .light)

        bkgView.backgroundColor = .clear

    }
}

Example view controller class:

class CustomizeHeadersVC: UIViewController, UICollectionViewDelegate {
    
    var collectionView: UICollectionView!
    
    var dataSource: UICollectionViewDiffableDataSource<Int, Int>! = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(collectionView)
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
            collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
            collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
        ])
        
        configureDataSource()

        collectionView.delegate = self

    }
    
    static let sectionHeaderElementKind = "section-header-element-kind"

    func createLayout() -> UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                              heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                               heightDimension: .absolute(44))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing = 3
        section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 6, trailing: 0)
        
        let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                      heightDimension: .estimated(44))
        let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: headerFooterSize,
            elementKind: CustomizeHeadersVC.sectionHeaderElementKind, alignment: .top)
        
        section.boundarySupplementaryItems = [sectionHeader]
        
        let layout = UICollectionViewCompositionalLayout(section: section)
        return layout
    }

    func configureDataSource() {
        
        let cellRegistration = UICollectionView.CellRegistration<SimpleCell, Int> { (cell, indexPath, identifier) in
            // Populate the cell with our item description.
            cell.theLabel.text = "\(indexPath)" // "\(indexPath.section),\(indexPath.item)"
        }
        
        let headerRegistration = UICollectionView.SupplementaryRegistration
        <TitleSupplementaryView>(elementKind: CustomizeHeadersVC.sectionHeaderElementKind) {
            (supplementaryView, string, indexPath) in
            
            supplementaryView.label.text = "Section Header for section \(indexPath.section)"
            
            // default background color / corner radius /
            //  text color / border color / border width
            supplementaryView.bkgView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            supplementaryView.bkgView.layer.cornerRadius = 0.0
            
            supplementaryView.bkgView.layer.borderColor = UIColor.black.cgColor
            supplementaryView.bkgView.layer.borderWidth = 1.0
            supplementaryView.label.textColor = .black
            
            // specific background color / corner radius /
            //  text color / border color / border width
            //  for sections ... cycle through 4 "styles"
            switch indexPath.section % 4 {
            case 0:
                supplementaryView.bkgView.backgroundColor = .cyan
                supplementaryView.bkgView.layer.cornerRadius = 6.0
                
            case 1:
                supplementaryView.bkgView.backgroundColor = .systemBlue
                supplementaryView.label.textColor = .white
                supplementaryView.bkgView.layer.cornerRadius = 12.0
                supplementaryView.bkgView.layer.borderWidth = 2.0
                
            case 2:
                supplementaryView.bkgView.backgroundColor = .yellow
                supplementaryView.bkgView.layer.cornerRadius = 16.0
                supplementaryView.bkgView.layer.borderWidth = 0.0
                supplementaryView.bkgView.layer.borderColor = UIColor.red.cgColor
                
            default:
                ()
            }
            
        }
        
        dataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
        }
        
        dataSource.supplementaryViewProvider = { (view, kind, index) in
            return self.collectionView.dequeueConfiguredReusableSupplementary(
                using: headerRegistration, for: index)
        }

        // initial data
        let itemsPerSection = 3
        let sections = Array(0..<25)
        var snapshot = NSDiffableDataSourceSnapshot<Int, Int>()
        var itemOffset = 0
        sections.forEach {
            snapshot.appendSections([$0])
            snapshot.appendItems(Array(itemOffset..<itemOffset + itemsPerSection))
            itemOffset += itemsPerSection
        }
        dataSource.apply(snapshot, animatingDifferences: false)
    }

}

Output - we cycle through 4 different section header "styles" (background and text colors, borders, corner radii, etc):

enter image description here


Edit - after comments...

Slight modifications to above code to also show a "section background" decoration view...

Section background view:

class SectionBackgroundView: UICollectionReusableView {
    
    static let reuseIdentifier: String = "SectionBackgroundView"
    
    let bkgView = UIView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    func commonInit() {
        backgroundColor = .clear
        
        bkgView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(bkgView)
        
        NSLayoutConstraint.activate([
            bkgView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
            bkgView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
            bkgView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
            bkgView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
        ])
        
    }
    
}

Example view controller class:

class CustomizeHeadersVC: UIViewController, UICollectionViewDelegate {
    
    var collectionView: UICollectionView!
    
    var dataSource: UICollectionViewDiffableDataSource<Int, Int>! = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(collectionView)
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
            collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
            collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
        ])
        
        configureDataSource()

        collectionView.delegate = self

    }
    
    func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {

        if elementKind == CustomizeHeadersVC.backgroundElementKind,
           let v = view as? SectionBackgroundView
        {

            // default background color / corner radius /
            //  text color / border color / border width
            v.bkgView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            v.bkgView.layer.cornerRadius = 0.0
            
            v.bkgView.layer.borderColor = UIColor.black.cgColor
            v.bkgView.layer.borderWidth = 1.0
            
            // specific background color / corner radius /
            //  text color / border color / border width
            //  for sections ... cycle through 4 "styles"
            switch indexPath.section % 4 {
            case 0:
                v.bkgView.backgroundColor = .cyan
                v.bkgView.layer.cornerRadius = 6.0
                
            case 1:
                v.bkgView.backgroundColor = .systemBlue
                v.bkgView.layer.cornerRadius = 12.0
                v.bkgView.layer.borderWidth = 2.0
                
            case 2:
                v.bkgView.backgroundColor = .systemYellow
                v.bkgView.layer.cornerRadius = 16.0
                v.bkgView.layer.borderWidth = 0.0
                v.bkgView.layer.borderColor = UIColor.red.cgColor
                
            default:
                ()
            }

            if let bc = v.bkgView.backgroundColor {
                v.bkgView.backgroundColor = bc.withAlphaComponent(0.25)
            }
        }
    }
    
    static let sectionHeaderElementKind = "section-header-element-kind"
    static let backgroundElementKind = "background-element-kind"

    func createLayout() -> UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                              heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                               heightDimension: .absolute(44))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing = 3
        section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 6, trailing: 0)
        section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 20, bottom: 16, trailing: 20)

        let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                      heightDimension: .estimated(44))
        let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: headerFooterSize,
            elementKind: CustomizeHeadersVC.sectionHeaderElementKind, alignment: .top)
        
        section.boundarySupplementaryItems = [sectionHeader]
        
        section.decorationItems = [
            NSCollectionLayoutDecorationItem.background(elementKind: CustomizeHeadersVC.backgroundElementKind)
        ]

        let config = UICollectionViewCompositionalLayoutConfiguration()
        config.interSectionSpacing = 12 // section spacing

        let layout = UICollectionViewCompositionalLayout(section: section, configuration: config)

        layout.register(SectionBackgroundView.self, forDecorationViewOfKind: CustomizeHeadersVC.backgroundElementKind)

        return layout
    }

    func configureDataSource() {
        
        let cellRegistration = UICollectionView.CellRegistration<SimpleCell, Int> { (cell, indexPath, identifier) in
            // Populate the cell with our item description.
            cell.theLabel.text = "\(indexPath)" // "\(indexPath.section),\(indexPath.item)"
        }
        
        let bkgRegistration = UICollectionView.SupplementaryRegistration
        <SectionBackgroundView>(elementKind: CustomizeHeadersVC.backgroundElementKind) {
            (supplementaryView, string, indexPath) in
            
            supplementaryView.bkgView.backgroundColor = .green
        }
        
        let headerRegistration = UICollectionView.SupplementaryRegistration
        <TitleSupplementaryView>(elementKind: CustomizeHeadersVC.sectionHeaderElementKind) {
            (supplementaryView, string, indexPath) in
            
            supplementaryView.label.text = "Section Header for section \(indexPath.section)"
            
            // default background color / corner radius /
            //  text color / border color / border width
            supplementaryView.bkgView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            supplementaryView.bkgView.layer.cornerRadius = 0.0
            
            supplementaryView.bkgView.layer.borderColor = UIColor.black.cgColor
            supplementaryView.bkgView.layer.borderWidth = 1.0
            supplementaryView.label.textColor = .black
            
            // specific background color / corner radius /
            //  text color / border color / border width
            //  for sections ... cycle through 4 "styles"
            switch indexPath.section % 4 {
            case 0:
                supplementaryView.bkgView.backgroundColor = .cyan
                supplementaryView.bkgView.layer.cornerRadius = 6.0
                
            case 1:
                supplementaryView.bkgView.backgroundColor = .systemBlue
                supplementaryView.label.textColor = .white
                supplementaryView.bkgView.layer.cornerRadius = 12.0
                supplementaryView.bkgView.layer.borderWidth = 2.0
                
            case 2:
                supplementaryView.bkgView.backgroundColor = .systemYellow
                supplementaryView.bkgView.layer.cornerRadius = 16.0
                supplementaryView.bkgView.layer.borderWidth = 0.0
                supplementaryView.bkgView.layer.borderColor = UIColor.red.cgColor
                
            default:
                ()
            }
            
        }
        
        dataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
        }
        
        dataSource.supplementaryViewProvider = { (view, kind, index) in
            return self.collectionView.dequeueConfiguredReusableSupplementary(
                using: headerRegistration, for: index)
        }

        // initial data
        let itemsPerSection = 3
        let sections = Array(0..<25)
        var snapshot = NSDiffableDataSourceSnapshot<Int, Int>()
        var itemOffset = 0
        sections.forEach {
            snapshot.appendSections([$0])
            snapshot.appendItems(Array(itemOffset..<itemOffset + itemsPerSection))
            itemOffset += itemsPerSection
        }
        dataSource.apply(snapshot, animatingDifferences: false)
    }

}

Result:

enter image description here

6
  • Thanks for sharing, I want to know how do I decorate my group as well, So that I can have each and every section element different background style. Commented Oct 16, 2023 at 14:16
  • OK - that’s a completely different question though.
    – DonMag
    Commented Oct 16, 2023 at 14:28
  • @Shivadityakr - quick search for swift "UICollectionViewCompositionalLayout" section border comes up with lots of results.
    – DonMag
    Commented Oct 16, 2023 at 16:03
  • @Shivadityakr - is this closer to what you want? i.sstatic.net/6StP7.png
    – DonMag
    Commented Oct 18, 2023 at 13:35
  • Hey that i wanted to, thanks man Commented Oct 19, 2023 at 13:48

Not the answer you're looking for? Browse other questions tagged or ask your own question.