UICollectionView CompositionalLayout Part 3: Side-By-Side Cells

Continue your journey with UICollectionViewCompositionalLayout

Learn how to make rows with multiple cells.

In this tip we will add a different layout for the second section of our collection view. This layout consists of two equally sized side-by-side cells. We will also learn how to implement orthogonal scrolling to allow the cells within a section to be scrolled in the opposite direction of the collection view as a whole.

Our setup this time around is mostly the same. We are now using a Section enum to help us keep our code for the different section layouts clean and organized. The rest of the setup for the collection view is the same. You can see we use the sectionIndex variable provided to us by the sectionProvider closure to create an instance of the Section enum. We then return the sectionLayout property from this enum, it is an NSCollectionLayoutSection like we are asked to return.

func setUpCollectionView() {
    
    let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
        let section = Section(rawValue: sectionIndex)!
        return section.sectionLayout
    })

    ...
}

We can see on lines 12 and 13 that the value for the sectionLayout property is computed based on the case of the enum it is called on. Each case calls a different function to get its corresponding layout.

var sectionLayout: NSCollectionLayoutSection {
    switch self {
    case .first: return self.section1()
    case .second: return self.section2()
    }
}

The section1 function has the code for the first section, see the previous Tip in this series for more details on the setup for this layout. The section2 function has the code for the new layout we are creating in this Tip.

private func section1() -> NSCollectionLayoutSection {
            
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    item.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)

    let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100.0)), subitems: [item])

    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .paging
    
    return section
}

private func section2() -> NSCollectionLayoutSection {
    ...
}

Line 40 holds the only difference between the code in the section2 function and the section1 function. In the second section we want each cell to take up half of the width of its group, so we set the widthDimension parameter of its layout size to be .fractionalWidth(0.5) instead of .fractionalWidth(1.0). If we wanted 3 cells per group, we would use 0.333 instead of 0.5. Changing just one constant gives us significantly different layouts!

private func section1() -> NSCollectionLayoutSection {
            
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))

    ...
}

private func section2() -> NSCollectionLayoutSection {
    
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
    
    ...
}

The ability to have the collection view scroll vertically while having each section scroll horizontally is achieved by using the orthogonalScrollingBehavior property on each section. On lines 33 and 47 we set this property to have a value of .paging. To see what other options we have for this scrolling behavior, take a look at the the corresponding Tip in this series.

private func section1() -> NSCollectionLayoutSection {
            
    ...

    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .paging
    
    return section
}

private func section2() -> NSCollectionLayoutSection {
    
    ...
    
    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .paging
    
    return section
}

We have now taken another step in our ability to create layouts with UICollectionViewCompositionalLayout. To learn how to complete the final 2 layouts from our original demo, continue onto the next Tip in this series.

Full Code

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {
    
    enum Section: Int, CaseIterable {
        
        case first, second
        
        var sectionLayout: NSCollectionLayoutSection {
            switch self {
            case .first: return self.section1()
            case .second: return self.section2()
            }
        }
        
        var color: UIColor {
            switch self {
            case .first: return .red
            case .second: return .purple
            }
        }
        
        private func section1() -> NSCollectionLayoutSection {
            
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)

            let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100.0)), subitems: [item])

            let section = NSCollectionLayoutSection(group: group)
            section.orthogonalScrollingBehavior = .paging
            
            return section
        }
        
        private func section2() -> NSCollectionLayoutSection {
            
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
            
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100.0)), subitems: [item])
            
            let section = NSCollectionLayoutSection(group: group)
            section.orthogonalScrollingBehavior = .paging
            
            return section
        }
    }
    
    override func loadView() {
        let view = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 667))
        self.view = view
    
        setUpCollectionView()
    }
    
    func setUpCollectionView() {
        
        let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
            let section = Section(rawValue: sectionIndex)!
            return section.sectionLayout
        })
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        
        collectionView.backgroundColor = .white
        
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
        collectionView.dataSource = self
        
        view.addSubview(collectionView)
        collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    }
}

extension MyViewController: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return Section.allCases.count
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }
    
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.resuseIdentifier, for: indexPath) as! Cell
        
        cell.label.text = "(\(indexPath.section), \(indexPath.row))"
        cell.backgroundColor = Section(rawValue: indexPath.section)!.color
        
        return cell
    }
}

class Cell: UICollectionViewCell {
    
    static let resuseIdentifier = "Cell"

    let label: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textAlignment = .center
        return label
    }()
    
    override init(frame: CGRect) {
        
        super.init(frame: frame)
        
        addSubview(label)
        label.topAnchor.constraint(equalTo: topAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

PlaygroundPage.current.liveView = MyViewController()

* This article originally appeared in the ZipTips app.

Sponsor

earlybirdee

Get your next tech job faster. Be the first to apply to today and yesterday’s newest jobs. Only the freshest jobs are available. Older than 48 hours — poof!

Want to connect?

Contact me to talk about this article, working together or something else.