swiftuicollectionviewuikitflowlayoutdecoration

Customizing corner radius & colors for UICollectionView sections in Compositional Layout?


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


Solution

  • 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

    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