UICollectionView CompositionalLayout Part 1: Introduction

Learn the basics of UICollectionViewCompositionalLayout before diving in and making your own layouts

UICollectionViewCompositionalLayout was introduced at WWDC in 2019. It is a flexible way to very simply create all kinds of collection view layouts. Let’s take a high-level look at how it works.

UICollectionViewCompositionalLayout provides a simple way to make all kinds of complex layouts. It was used to make the layout for the App Store and the layout shown above.

While complex, both layouts can be set up fairly simply using UICollectionViewCompositionalLayout because of how things are structured in this new system. In UICollectionViewCompositionalLayout we use 3 levels of building blocks to construct our layouts. From smallest to largest they are: items, groups, sections. Each level holds elements of the level below it. As shown, items (purple) go in groups (green), groups go in sections (gray) and sections make our layout (blue).

Items correspond with cells in a collection view and sections are just that, sections, so no change there. Groups are new with UICollectionViewCompositionalLayout and this new intermediate level is what provides us with so much power.

Before we get into the code to actually build a layout with UICollectionViewCompositionalLayout we need to understand how sizing works. Sizes for objects in compositional layouts can be determined in three ways. They can be set with an absolute value, a relative value or with an estimated value. Sizes are set using NSCollectionLayoutSize objects that have just 2 properties - a width and a height - both of which are NSCollectionLayoutDimension'92s.

Here we see a cell (item) set using absolute values. Its width is set to 100 points and its height is set to 50 points.

We use the fractionalWidth and fractionalHeight functions to specify relative values for sizes. Each function takes a CGFloat as a parameter. This CGFloat is a number 0.0 to 1.0 that says how much of its containers corresponding dimension the item will take up. Below we have a group with an absolute width of 200 points and an absolute height of 50 points. Inside of this group we have an item with an absolute width of 200 points and a relative height of 0.8. This results in the item have a height of 40 points because 40 is equal to the fractional height value (0.8) times its containers height value (50).

If we give an item or group an estimated height, of 125 for example, the layout system will use this value as a starting point but will calculate whatever value fits best in the layout at run time.

Now that you understand how compositional layouts are built using items, groups and sections and how sizing is determined, you are ready to put this knowledge into action and build your first UICollectionViewCompositionalLayout. Check out the next tip in this series to see exactly how!

Full Code

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {
    
    enum Section: Int, CaseIterable {
        
        case first, second, third, fourth
        
        var sectionLayout: NSCollectionLayoutSection {
            switch self {
            case .first: return self.section1()
            case .second: return self.section2()
            case .third: return self.section3()
            case .fourth: return self.section4()
            }
        }
        
        var color: UIColor {
            switch self {
            case .first: return .red
            case .second: return .purple
            case .third: return .green
            case .fourth: return .orange
            }
        }
        
        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(50.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(50.0)), subitems: [item])
            
            let section = NSCollectionLayoutSection(group: group)
            section.orthogonalScrollingBehavior = .paging
            
            return section
        }
        
        private func section3() -> NSCollectionLayoutSection {
            
            let columnItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5)))
            columnItem.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)

            let stackedColumnGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1.0)), subitem: columnItem, count: 2)
            
            let tallItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1.0)))
            tallItem.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
            
            let combinedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(2/3)), subitems: [stackedColumnGroup, tallItem])
            
            let section = NSCollectionLayoutSection(group: combinedGroup)
            section.orthogonalScrollingBehavior = .paging
            
            return section
        }
        
        private func section4() -> NSCollectionLayoutSection {
            
            let rowItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1.0)))
            rowItem.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
            let rowGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1/2)), subitems: [rowItem])
            let stackedRowGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(2/3)), subitems: [rowGroup])
        
            let wideItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1/3)))
            wideItem.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)

            let combinedGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(3/4)), subitems: [stackedRowGroup, wideItem])
            
            let section = NSCollectionLayoutSection(group: combinedGroup)
            section.orthogonalScrollingBehavior = .paging
            
            return section
        }
    }
    
    override func loadView() {
        let view = UIView()
        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.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        collectionView.heightAnchor.constraint(equalToConstant: 400.0).isActive = true
        collectionView.widthAnchor.constraint(equalToConstant: 200.0).isActive = true
    }
}

extension MyViewController: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return Section.allCases.count
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 100
    }
    
    
    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.