UICollectionView CompositionalLayout Part 4: Advanced Layouts

Learn how to make 2 complex layouts with simple code

We will create the last two layouts in this series.

The layouts for our final two sections are both significantly more complex than the first two, but only visually. As we will see, both of these layouts can be constructed in less than 15 lines of code each.

Since we are adding two new sections to our collection view, each with their own layout, we will add two new cases to our Section enum. With these two cases we need to update our sectionLayout and color properties as well as add two new functions that create their respective NSCollectionLayoutSections. The rest of our setup is the same.

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(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
        }

        ...
}

Let’s take a closer look at what we are trying to accomplish with our third section. We have three columns of cells. The first and third columns have two equally sized cells stacked on each other and the second column is just one big cell. While this layout may look intimidating at first, we will see that by taking advantage of our ability to nest groups inside of groups, it really isn’t much more complicated than what we have already done.

We start, like normal, by defining an item, here called columnItem. We then create a vertical group that stacks one columnItem on top of another.

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)
    ...
}

Next we need to work on the middle, tall item. This will just be a normal item with a fraction height value of 1.0 instead of the 0.5 we used for the stacked items.

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)
    
    ...
}

We then make a new group composed of the stacked group - stackedColumnGroup - and this new item - tallItem - which we call combinedGroup. The combinedGroup will layout each of the subitems we give it in order, looping around if need be, until it runs out of space in its container. So it will first layout a stackedColumnGroup, then a tallItem, then another stackedColumnGroup before running out of space when it will then start over for the next group.

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])
    
    ...
}

Our first of these complex layouts is finished by creating a section from our combined grouping and retuning that 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
}

The final layout is another composed layout that takes advantages of being able to nest groups. We first make rowItems that are to span one-third of the screen. We group three of these rowItem’s together and call it rowGroup.

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])
    
    ...
}

Next, we stack two of these rowGroups together and call it stackedRowGroup. This will give us a grid that has 3 columns and 2 rows.

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])

    ...
}

The last thing we need to do is add the bottom wideItem. We make this item on line 81 and then add it with the stackedRowGroup by creating the combinedGroup group which we give to the section to finish everything off.

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
}

Now you have the knowledge to go out and make your own fairly advanced layouts. Even more is possible with UICollectionViewCompositionalLayout though, including orthogonal scrolling, adding supplementary items and adding section headers and footers. To continue learning more about these topics, check out the other SwiftTips in this series.

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(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
        }
        
        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(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 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.