iosuicollectionviewuicollectionviewcompositionallayoutuicollectionreusableview

Set correct height to section background view in horizontal collectionview


I am trying out the Modern collectionviews with the below code.

The only problem in this code is the section background height is filling from the last item . If I keep the collection view scrolling to vertical that works correctly.

class SectionDecorationViewController: UIViewController {

    static let sectionBackgroundDecorationElementKind = "section-background-element-kind"

    var currentSnapshot: NSDiffableDataSourceSnapshot<Int, Int>! = nil
    var dataSource: UICollectionViewDiffableDataSource<Int, Int>! = nil
    var collectionView: UICollectionView! = nil
    var currentSelection: Bool = false

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Section Background Decoration View"
        configureHierarchy()
        configureDataSource()
    }
}

extension SectionDecorationViewController {
    /// - Tag: Background
    func createLayout() -> UICollectionViewLayout {

        let sectionProvider = { [weak self] (sectionIndex: Int,
                                 layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in



            guard let self = self else { return nil }

            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                  heightDimension: .absolute(44))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)

            // Get the number of items in the section
            let itemCount = self.currentSnapshot.numberOfItems(inSection: sectionIndex)

            // Custom group height based on the number of items
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                   heightDimension: .absolute(CGFloat(itemCount) * 44))
            let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])

            let section = NSCollectionLayoutSection(group: group)
            section.orthogonalScrollingBehavior = .paging
            section.interGroupSpacing = 0 // Remove inter-group spacing
            section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) // Remove content insets

            // Add the background decoration item
            let sectionBackgroundDecoration = NSCollectionLayoutDecorationItem.background(
                elementKind: SectionDecorationViewController.sectionBackgroundDecorationElementKind)
            section.decorationItems = [sectionBackgroundDecoration]
            return section
        }
        
        let config = UICollectionViewCompositionalLayoutConfiguration()
        config.scrollDirection = .horizontal
        config.interSectionSpacing = 0
        let layout = UICollectionViewCompositionalLayout(
            sectionProvider: sectionProvider, configuration: config)
        layout.register(
            SectionBackgroundDecorationView.self,
            forDecorationViewOfKind: SectionDecorationViewController.sectionBackgroundDecorationElementKind)
        return layout
    }
}

extension SectionDecorationViewController {
    func configureHierarchy() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .systemBackground
    
        view.addSubview(collectionView)
        collectionView.delegate = self
    }
    func configureDataSource() {
        
        let cellRegistration = UICollectionView.CellRegistration<ListCell, Int> { (cell, indexPath, identifier) in
            // Populate the cell with our item description.

            cell.label.text = "\(indexPath.section),\(indexPath.item)"

        }
        
        dataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
            // Return the cell.
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
        }

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

enter image description here

enter image description here


Solution

  • Based on this answer - https://stackoverflow.com/a/77297540/6257435 - with just a few modifications, and stripped-down to emphasize group backgrounds when .scrollDirection = .horizontal...


    Simple Cell class - single label, centered:

    class SimpleCell: UICollectionViewCell {
        
        let theLabel: UILabel = {
            let v = UILabel()
            v.textAlignment = .center
            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: 0.0),
                theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            ])
            
            contentView.layer.borderColor = UIColor(white: 0.5, alpha: 1.0).cgColor
            contentView.layer.borderWidth = 1.0
            contentView.layer.cornerRadius = 6
            contentView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        }
        
    }
    

    Section Background View class:

    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),
            ])
            
            bkgView.backgroundColor = .init(red: 0.5, green: 0.75, blue: 1.0, alpha: 1.0)
            bkgView.layer.cornerRadius = 8.0
            bkgView.layer.borderColor = UIColor.black.cgColor
            bkgView.layer.borderWidth = 1.0
        }
        
    }
    

    Example Controller class:

    class HorizontalGroupsVC: 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.heightAnchor.constraint(equalToConstant: 120.0),
            ])
            
            configureDataSource()
            
            collectionView.delegate = self
            
            view.backgroundColor = .systemYellow
            collectionView.backgroundColor = .clear
        }
        
        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: .estimated(80.0),
                                                   heightDimension: .fractionalHeight(1.0))
            
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
            
            let section = NSCollectionLayoutSection(group: group)
            section.interGroupSpacing = 8
            section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
            
            section.decorationItems = [
                NSCollectionLayoutDecorationItem.background(elementKind: HorizontalGroupsVC.backgroundElementKind)
            ]
            
            let config = UICollectionViewCompositionalLayoutConfiguration()
            config.interSectionSpacing = 12
            
            config.scrollDirection = .horizontal
            
            let layout = UICollectionViewCompositionalLayout(section: section, configuration: config)
            
            layout.register(SectionBackgroundView.self, forDecorationViewOfKind: HorizontalGroupsVC.backgroundElementKind)
            
            return layout
        }
        
        func configureDataSource() {
            
            let cellRegistration = UICollectionView.CellRegistration<SimpleCell, Int> { (cell, indexPath, identifier) in
                cell.theLabel.text = "\(indexPath)"
            }
            
            dataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: collectionView) {
                (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
                return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
            }
            
            // initial data
            let itemsPerSection: [Int] = [3, 2, 1, 5, 3, 4, 2, 3, 4]
            let sections = Array(0..<itemsPerSection.count)
            var snapshot = NSDiffableDataSourceSnapshot<Int, Int>()
            var itemOffset = 0
            var i: Int = 0
            
            sections.forEach {
                snapshot.appendSections([$0])
                let n = itemsPerSection[i % itemsPerSection.count]
                snapshot.appendItems(Array(itemOffset..<itemOffset + n))
                itemOffset += n
                i += 1
            }
            
            dataSource.apply(snapshot, animatingDifferences: false)
        }
        
    }
    

    Output:

    enter image description here