Using CollectionView Compositional Layouts in Swift 5

At the WWDC 2019, Apple introduced a new form of UICollectionViewLayout.
If you ever worked with UICollectionView, you may have struggled with your layout attributes, or tweaking the FlowLayout. I, personally literally spent hours trying to get a good result, working for all iOS version, in every device, every size class, every orientation.

Well, meet UICollectionViewCompositionalLayout !

A layout that allows you to specify your rows/items, groups and sections size and insets in a very simple way.
With Compositional Layouts, you could build very complex layout just within few lines of code.
Let's take a look at it.

To start using a CollectionViewLayout, let's make a quick collectionViewCell.
Here is a simple UICollectionViewCell that has a container with a shadow and a corner radius and a rounded colored view inside :

class Cell: UICollectionViewCell {
    var container: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.white
        view.layer.shadowColor = UIColor.black.cgColor
        view.layer.cornerRadius = 4
        view.layer.shadowOpacity = 0.5
        view.layer.shadowRadius = 4
        view.layer.shadowOffset = CGSize(width: 0, height: 2)
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    var colorView: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.red
        view.layer.cornerRadius = 10
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.contentView.addSubview(self.container)
        self.container.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true
        self.container.leftAnchor.constraint(equalTo: self.contentView.leftAnchor).isActive = true
        self.container.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor).isActive = true
        self.container.rightAnchor.constraint(equalTo: self.contentView.rightAnchor).isActive = true

        self.container.addSubview(self.colorView)
        self.colorView.centerXAnchor.constraint(equalTo: self.container.centerXAnchor).isActive = true
        self.colorView.centerYAnchor.constraint(equalTo: self.container.centerYAnchor).isActive = true
        self.colorView.widthAnchor.constraint(equalToConstant: 20).isActive = true
        self.colorView.heightAnchor.constraint(equalToConstant: 20).isActive = true
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Looks like :

Now we will make a quick ViewController with a UICollectionView inside :

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
    var collectionView: UICollectionView!
    let colors = [UIColor.red, UIColor.blue, UIColor.green, UIColor.orange, UIColor.purple]

    override func viewDidLoad() {
        super.viewDidLoad()
        // init collection view
        self.collectionView = UICollectionView()
        self.collectionView.backgroundColor = UIColor.white
        // set the delegate and dataSource on self
        self.collectionView.delegate = self
        self.collectionView.dataSource = self
        // register our cell
        self.collectionView.register(Cell.self, forCellWithReuseIdentifier: "cell")

        // place the collectionView in the viewController's view
        self.collectionView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(self.collectionView)
        NSLayoutConstraint.activate([
            self.collectionView.topAnchor.constraint(equalTo: self.view.layoutMarginsGuide.topAnchor),
            self.collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
            self.collectionView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
            self.collectionView.rightAnchor.constraint(equalTo: self.view.rightAnchor)
            ])
    }

    // data source

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

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 5
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? Cell {
            cell.colorView.backgroundColor = colors[indexPath.row]
            return cell
        } else {
            return UICollectionViewCell()
        }
    }
}

So now, we need to properly init the collectionView, with bounds and layout. Let's make a function, we'll call makeLayout() that will return our compositional layout.

A UICollectionViewCompositionalLayout is initialized with a block that takes two parameters : the section, and the environment, and returns a NSCollectionLayoutSection. As you can guess, it will be called on render of every distinct section.

    func makeLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { (section: Int, environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: NSCollectionLayoutDimension.fractionalWidth(1.0), heightDimension: NSCollectionLayoutDimension.absolute(44)))
            item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),  heightDimension: .absolute(50))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
            let section = NSCollectionLayoutSection(group: group)
            section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
            return section
        }
        return layout
    }

In these few lines, we set a few notable objects :

  • Item : NSCollectionLayoutItem initialized with its size (width & hieght). In this sample of code I also set contentInsets. This is basically a cell.
  • Group : NSCollectionLayoutGroup initilalized with its size, its item and count. A group represents a "line" of the layout. The count is the number of item in one line. Call it a column if you want.
  • Section : NSCollectionLayoutSection initialized with its group. It's, well, a section.

So we could make an interesting layout by playing the item count in groups like so :

Easy! We'll make a func numberOfColumns(section: Int) -> Int and call it in the NSCollectionLayoutGroup init.

    func numberOfColumns(section: Int) -> Int {
        switch section {
        case 0:
            return 5
        case 1:
            return 3
        default:
            return 1
        }
    }

Composition Layout allows also Orthogonal Scrolling, you know, like in the AppStore, with bi directional scrolling. But we'll keep that for another time.

Hope you enjoyed this little intro. If you want to go deeper in Compositional Layout, I highly recommend WWDC 2019 Session 215.

Happy Coding! 🙂

Leave a Comment