UICollectionView CompositionalLayout Part 2: A List Layout

Get started with your first UICollectionViewCompositionalLayout by making a list layout

To ease into the process of making complex layouts with UICollectionViewCompositionalLayout, we will start by creating a table-view-like list layout.

Our first layout will be a simple list layout, just like how a table view looks. We will use this layout to better get our heads around how UICollectionViewCompositionalLayouts work and then move onto creating more complex layouts.

We create our collection view in the setUpCollectionView function on lines 31 through 43. We initialize the collection view on line 31 by passing in a value for the frame and collectionViewLayout parameters. The value for frame is .zero because will be using autolayout. The value for collectionViewLayout is given to us by the layoutSection function, which we will return to in detail in just a second. The rest of setUpCollectionView does the standard set up for a collection view - sets its dataSource and allows us to use our custom Cell class - and gives our collection view a size.

func setUpCollectionView() {
        
    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layoutSection())
    collectionView.backgroundColor = .white
    
    collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
    
    collectionView.dataSource = self
    
    view.addSubview(collectionView)
    collectionView.translatesAutoresizingMaskIntoConstraints = false
    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
}

Now let’s take a look at where our layout is specified. When can see that the layoutSection function returns a UICollectionViewCompositionalLayout type. We create this type using its initializer with a sectionProvider parameter. This is a closure that gets called any time a layout is needed for a section. Using the sectionIndex value provided, we can then specify a different layout for each section in our collection view, if desired.

func layoutSection() -> UICollectionViewCompositionalLayout {
        
    return UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
                
        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)
        
        return section
    })
}

To first make our list layout we need to say how each item in the layout will look, remembering that each cell is an item. All this requires is creating a NSCollectionLayoutItem instance, which in turn requires a NSCollectionLayoutSize instance to say how big each cell will be. On line 17 we create our size instance and pass in .fractionWidth(1.0) and .fractionalHeight(1.0) for the widthDimension and height Dimension parameters respectively. This means that the width of each cell will be 100% of its containing group. The same is true of its height. On line 19, to make each cell separate, we give them a content inset of 2.0 on all sides.

func layoutSection() -> UICollectionViewCompositionalLayout {
        
    return UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
                
        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)

        ...
    })
}

With our item template setup, we now need to create a group template. We use the horizontal initializer of the NSCollectionLayoutGroup class. This will layout each item on a horizontal line until there is no more space on that line, at which point it will move down and continue this process on a new line. Because our cells will take up 100% of the group’s width, each line will only have 1 cell. For the subitems parameter of the initializer, we pass in an array of the items we wish to use in this group, which will just be an array of one thing for us here. We also need to pass in width and height information. Again we use .fractionWidth(1.0) to say the group will take up its container’s entire width. This time for the height though we use .absolute(100.0) which gives the group a height of 50 points. Because each cell takes up 100% of its group’s height, each cell will also then be 100.0 points tall.

func layoutSection() -> UICollectionViewCompositionalLayout {
    
    return UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
                
        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])

        ...
    })
}

The final two steps to complete this function are to make an NSCollectionLayoutSection from our group and to return this section.

func layoutSection() -> UICollectionViewCompositionalLayout {
    
    return UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
                
        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)
        
        return section
    })
}

And that’s it! Running the playground, we can now see our list in action with the layout we wanted. Each cell shows its section index and item number.

In the next tip in this series we will learn how to make section layouts that are a step up in terms of difficulty and we will learn how to make each section scroll horizontally independent of the other section while still allowing the whole collection view to scroll vertically!

Full Code

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {
    
    override func loadView() {
        let view = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 667))
        self.view = view
    
        setUpCollectionView()
    }
    
    func layoutSection() -> UICollectionViewCompositionalLayout {
        
        return UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
                    
            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)
            
            return section
        })
    }
    
    func setUpCollectionView() {
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layoutSection())
        collectionView.backgroundColor = .white
        
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
        
        collectionView.dataSource = self
        
        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        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 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 = .red
        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.