swiftuicollectionview

CollectionView compositional layout


I've been working in a collectionView that shows the cells in this layout.

|-------Featured--------|

|--Normal--| |--Normal--|

|--Normal--| |--Normal--|

But without success.

private func createLayout() -> UICollectionViewLayout {
    let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
        
        let featuredItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(250))
        let featuredItem = NSCollectionLayoutItem(layoutSize: featuredItemSize)

        let normalItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .absolute(150))
        let normalItem = NSCollectionLayoutItem(layoutSize: normalItemSize)

        let pairGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(150))
        let pairGroup = NSCollectionLayoutGroup.horizontal(layoutSize: pairGroupSize, subitems: [normalItem, normalItem])
        
        let section: NSCollectionLayoutSection
        if sectionIndex == 0 {

            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(250))
            let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [featuredItem])
            section = NSCollectionLayoutSection(group: group)
        } else {

            section = NSCollectionLayoutSection(group: pairGroup)
        }

        section.interGroupSpacing = 10
        section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        
        return section
    }
    
    return layout
}

extension HomeViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return viewModel?.products.count ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if indexPath.section == 0 && indexPath.item == 0 {
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FeaturedProductCell.identifier, for: indexPath) as? FeaturedProductCell else {
                return UICollectionViewCell()
            }
            if let product = viewModel?.products.first {
                cell.configure(product: product)
            }
            return cell
        } else {
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProductCell.identifier, for: indexPath) as? ProductCell else {
                return UICollectionViewCell()
            }

            let adjustedIndex = indexPath.item - 1
            if let product = viewModel?.products[adjustedIndex] {
                cell.configure(product: product)
            }
            return cell
        }
    }
}

In this moment, all the cells are "featured" sized. In some of others tries, I manage to display all in normal size, or Feature, normal, normal pattern. The idea is just to haver one feature cell.

Thx in advance


Solution

  • The problem is that you have set up things in the expectation of an initial section with just one cell per row and then another section with two cells per row:

    if sectionIndex == 0 {
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(250))
        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [featuredItem])
        section = NSCollectionLayoutSection(group: group)
    } else {
        section = NSCollectionLayoutSection(group: pairGroup)
    }
    

    but you are returning 1 from numberOfSections, so there is in fact only one section, the first one:

    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    

    Thus, the else clause above can never be reached; there will always be only section 0.

    So you need to return 2 instead. Other adjustments will follow naturally from there.


    Here's a demo. This is the entire code of the view controller class:

    class ViewController: UIViewController {
    
        private func createLayout() -> UICollectionViewLayout {
            let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
    
                let featuredItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(250))
                let featuredItem = NSCollectionLayoutItem(layoutSize: featuredItemSize)
    
                let normalItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .absolute(150))
                let normalItem = NSCollectionLayoutItem(layoutSize: normalItemSize)
    
                let pairGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(150))
                let pairGroup = NSCollectionLayoutGroup.horizontal(layoutSize: pairGroupSize, subitems: [normalItem, normalItem])
                pairGroup.interItemSpacing = .fixed(10)
    
                let section: NSCollectionLayoutSection
                if sectionIndex == 0 {
    
                    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(250))
                    let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [featuredItem])
                    section = NSCollectionLayoutSection(group: group)
                } else {
                    section = NSCollectionLayoutSection(group: pairGroup)
                }
    
                section.interGroupSpacing = 10
                section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
    
                return section
            }
    
            return layout
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            let collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
            self.view.addSubview(collectionView)
            collectionView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                collectionView.topAnchor.constraint(equalTo: self.view.topAnchor),
                collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
                collectionView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
                collectionView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            ])
            collectionView.dataSource = self
            collectionView.delegate = self
            collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        }
    }
    
    extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate {
        func numberOfSections(in collectionView: UICollectionView) -> Int {
            2
        }
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            switch section {
            case 0: return 1
            case 1: return 10
            default: fatalError("get real")
            }
        }
        
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
            cell.contentView.backgroundColor = .blue
            return cell
        }
    }
    

    And this is what I get:

    enter image description here

    That's close enough to what you're after. As I said, you can take it from ther to tweak as desired.