Supplementary views with Compositional Layout and Diffable Data Source

In this post we will see how to add badges to CollectionView cells with new APIs available since iOS 13.

Your collection view does not have to be "just" cells and header or footer views. You can also display extra supplementary items for each cell in the collection view. This can be simple badge or more complicated view.

I will show you how to first define these supplementary views as they are called in the official jargon and then how to tell the Diffable Data Source how to configure them. You can use any kind of view as supplementary, the only requirement is that it has to be a subclass of UICollectionReusableView. This does not have any extra requirements.

You will need to define reuse identifiers just like with cells. For this I am using this extension in my projects:

import UIKit

extension UICollectionReusableView {
    static var reuseIdentifier: String {
        return String(describing: Self.self)
    }
}

With this, every supplementary view now has static property reuseIdentifier which directly maps to its class name. So if we create BadgeView, its identifier will be "BadgeView".

import UIKit

class BadgeView: UICollectionReusableView {
    private var label: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textColor = .white
        label.font = UIFont.preferredFont(forTextStyle: .footnote)
        label.text = String(Int.random(in: 1...9))
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .systemRed

        addSubview(label)

        NSLayoutConstraint.activate([
            label.centerYAnchor.constraint(equalTo: centerYAnchor),
            label.centerXAnchor.constraint(equalTo: centerXAnchor)
        ])
    }

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

    override func layoutSubviews() {
        super.layoutSubviews()
        layer.cornerRadius = bounds.height/2
    }
}

I have decided to show the entire code in case you want to try creating these badges yourself while following this tutorial.

Telling Compositional Layout about supplementary views

This assumes you are familiar with the basics of Compositional Layout. If not, please read first my short post explaining its anatomy and check out basic examples from my GitHub project. Thanks!

If we want to tell Compositional Layout we want supplementary views, we need to associate them with NSCollectionLayoutItem. This can be done either with init or later with the supplementaryItems property.

Both cases expect an array of NSCollectionLayoutSupplementaryItem. This is a class that lets us define the supplementary items.

Here is an example using our BadgeView:

private func createBadgeItem() -> NSCollectionLayoutSupplementaryItem {
        let topRightAnchor = NSCollectionLayoutAnchor(edges: [.top, .trailing], fractionalOffset: CGPoint(x: 0.2, y: -0.2))
        let badgeSize: CGFloat = 18
        let item = NSCollectionLayoutSupplementaryItem(layoutSize: .init(widthDimension: .absolute(badgeSize), heightDimension: .absolute(badgeSize)), elementKind: BadgeView.reuseIdentifier, containerAnchor: topRightAnchor)

        return item
}

It is very similar to the item. We need to provide the size, tell it the reuse identifier and also anchor.

Anchor lets us "pin" this view to edges and also specify offset. If you know AutoLayout then this is pretty intuitive. There is also all option if you wanted your supplementary view to cover the cell for example.

The offset (either absolute or fractional) lets us "nudge" the item to specific directions. For badge this is ideal.

We have now created the supplementary view definition and can set it to our item:

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),                                            heightDimension: .fractionalHeight(1.0))

let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [createBadgeItem()])

Now each item will have a supplementary badge associated with it. It is important to note that now we have to provide the badge view always or we will get a crash.

Providing supplementary views with Diffable

Diffable Data Source has supplementaryViewProvider property we can use to provide supplementary views. This can be a closure or you can define method that takes collection view, element kind and indexPath and assign this method. I prefer this latter option because it is more readable.

Here is the signature:

func supplementary(collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView?

We can now assign it:

datasource.supplementaryViewProvider = supplementary(collectionView:kind:indexPath:)

And before we implement this method, we can't forget to register our BadgeView with the collection view:

collectionView.register(BadgeView.self, forSupplementaryViewOfKind: BadgeView.reuseIdentifier, withReuseIdentifier: BadgeView.reuseIdentifier)

And now for the supplementary implementation:

func supplementary(collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView? {
        switch kind {
        case BadgeView.reuseIdentifier:
            let badge = collectionView.dequeueReusableSupplementaryView(ofKind: BadgeView.reuseIdentifier, withReuseIdentifier: BadgeView.reuseIdentifier, for: indexPath) as! BadgeView
            badge.isHidden = Bool.random()
            return badge

        default:
            assertionFailure("Handle new kind")
            return nil
        }
}

I am using switch which makes it easy to support additional supplementary views later. Here we have just the badge. Since we must always return view and I don't have any real data in this example, I am using Bool.random() to randomly show or hide the badge.

And with this, the badges are finished. These are all the steps needed to have supplementary views. Below is screenshot from my example project:

Compositional Layout - supplementary views example

It has two types of supplementary views.

Alternatives

To be honest I don't find these supplementary views that much useful. If you want to display more complex data, you have another place where you need to query your datasource to get the data for configuring these views.

You can also sort of "fake" these supplementary views in your collection views cell. One would would be to create another content view with insets and then you can use the default contentView to also add badge or any other view and configure it while you configure the cell.

If you found cool use case for supplementaries, please let me know.

Uses: Xcode 12 & Swift 5.3


Follow me on Twitter for latest updates and news