Welcome to another installment of UIKit Learning! Today, we will dive into UICollectionView and explore how to use it effectively in your iOS apps.

A UICollectionView is a versatile and powerful component in UIKit that allows you to present a grid or list of items in a highly customizable layout. Whether you need a simple grid of images, a complex layout with multiple sections and headers, or dynamic, animated updates, UICollectionView provides the flexibility to create engaging user interfaces.

We’ll start with a minimal example and gradually build up to more complex scenarios, using the latest best practices and features, including diffable data sources (a favorite of mine).

Setting Up a Minimal UICollectionView#

Creating the base view controller#

As usual, we’re going to approach our development programmatically, so you can refer to my post on setting up a programmatic UIKit project.

Let’s start by creating a new file called ViewController.swift and add the following code.

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

This is the minimal view controller we are going to use to display the collection.

We are using this controller as the root ViewController for window in Scene Delegate.

Adding the collection to the view#

Now it’s time to add our UICollectionView, inside the ViewController add the following property.

import UIKit

class ViewController: UIViewController {

    private var collectionView: UICollectionView! // add this

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Next, let’s configure the collection, by adding and calling a configure function.

class ViewController: UIViewController {

    private var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupCollectionView() // 2. call the function
    }

    // 1. create this function
    func setupCollectionView() {
        // Creating layout
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 100, height: 100)
        layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        layout.scrollDirection = .horizontal
        
        // creating actual collection
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.backgroundColor = .darkGray
        
        // adding to view
        view.addSubview(collectionView)
        
        // adding constraints
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
        ])
    }
}

This functions creates the actual collection and the layout for the collection.

Why we need a collectionViewLayout#

When working with UICollectionView, you need to provide a layout object that determines how the items in the collection view are arranged.

UICollectionViewFlowLayout is a concrete layout object that organizes items into a grid with optional header and footer views for each section. It’s the most commonly used layout class because it provides a lot of built-in functionality that suits many common use cases.

UICollectionView does not come with a default layout. You must provide a layout object for it to work. UICollectionViewFlowLayout is the default layout provided by UIKit that supports basic grid-like arrangements.

Adding the data source#

Our collection is already created with a basic layout but we need to have actual data to make it display any information, to solve this problem we could implement the UICollectionViewDataSource but I prefer to use a diffable data source.

What’s a diffable data source#

A diffable data source is a modern way to manage the data in your collection views and table views. It makes it easier to update your UI when your data changes, ensuring smooth and efficient updates without having to manually calculate the differences between the old and new data.

In simple terms, you provide the entire set of new data to the data source using a snapshot, then the data source internally manages the update without any intervention from the user. A snapshot is like a picture of your data at a particular moment.

Creating the diffable data source#

Inside the class let’s create a new property called dataSource.

class ViewController: UIViewController {

    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Int, String>! // add this

    // More code ...

The provided types are for the section identifier and the actual data our collection will display, in our case we are using Int as the section identifier and String as the data we want to display. We could use any custom types as long as they are valid for the role.

Configuring the data source#

The next step is crucial for our collection to work, we need to configure how the cells will be updated and we need to register the collection in our data source.

Let’s add the following function to our class

class ViewController: UIViewController {
    // more code ...

    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        setupCollectionView()
        setupDataSource() // 2. call the function
    }

    // 1. create the function
    private func setupDataSource() {
            // configuring cell with diffable data source
            collectionView.register(CustomCollectionCell.self, forCellWithReuseIdentifier: CustomCollectionCell.name)
            
            // configure cell update
            dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, name in
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionCell.name, for: indexPath) as? CustomCollectionCell else { fatalError("Unable to dequeue CustomCollectionCell") }
                cell.backgroundColor = .orange
                cell.update(with: name)
                return cell
            }
        }

        // more code ...
}

First, we are registering the cell we are going to use in our collection.

Next, creating the diffable data source and passing the reference to our collection. By doing this we are telling our data source the collection it will be managing.

The closure we are passing tells the data source how the update of each cell will be managed. Inside the closure we have access to the actual collection, the indexPath of the current element and the actual data element we want to display inside the cell.

By doing these simple steps we have our collection connected with its data source.

Creating the snapshot#

Finally we need a way to update the collection data, to do this let’s add a new function that will handle the snapshot.

class ViewController: UIViewController {
    // more code ...

    // updating the diffable with new data
    func updateSnapshot(with data: [String]) {
        var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
        snapshot.appendSections([0])
        snapshot.appendItems(data) // Adding more items
        dataSource.apply(snapshot, animatingDifferences: true)
    }

    // more code ...
}

This function creates a snapshot and applies the new data to the underlying system that will automatically updates the collection for us.

If your collection wants to show different data, you need to update the snapshot and the data source to be compatible. The data source and snapshot must be of the same type to work.

Creating a custom cell#

By default, collection cell doesn’t have text or other elements, so let’s create a custom cell we can reuse in our collection. You already register this cell for the collection and you’re already using it in the data source. It’s a simple cell that only shows a centered label.

final class CustomCollectionCell: UICollectionViewCell {
    static let name = "CustomCollectionCell"
    
    private var label: UILabel!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLabel()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func update(with data: String) {
        label.text = data
    }
    
    private func setupLabel() {
        label = UILabel()
        label.textAlignment = .center
        
        self.contentView.addSubview(label) // remember to add it to the contentView
        
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: contentView.topAnchor), // we are referring to contentView not view
            label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), // we are referring to contentView not view
            label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), // we are referring to contentView not view
            label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), // we are referring to contentView not view
        ])
    }
}

In cells you need to add sub elements to the contentView not the view.

Testing the collection#

Let’s test the collection. We need some data to show, here is some data you can add to your class.

class ViewController: UIViewController {
    
    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Int, String>!
    
    private var mockData: [String] {
        [
            "Bulbasaur", "Ivysaur", "Venusaur", "Charmander", "Charmeleon", "Charizard",
            "Squirtle", "Wartortle", "Blastoise", "Caterpie", "Metapod", "Butterfree",
            "Weedle", "Kakuna", "Beedrill", "Pidgey", "Pidgeotto", "Pidgeot",
            "Rattata", "Raticate", "Spearow", "Fearow", "Ekans", "Arbok",
            "Pikachu", "Raichu", "Sandshrew", "Sandslash", "Nidoran♀", "Nidorina",
            "Nidoqueen", "Nidoran♂", "Nidorino", "Nidoking", "Clefairy", "Clefable",
            "Vulpix", "Ninetales", "Jigglypuff", "Wigglytuff", "Zubat", "Golbat",
            "Oddish", "Gloom", "Vileplume", "Paras", "Parasect", "Venonat", "Venomoth",
            "Diglett", "Dugtrio", "Meowth", "Persian", "Psyduck", "Golduck",
            "Mankey", "Primeape", "Growlithe", "Arcanine", "Poliwag", "Poliwhirl",
            "Poliwrath", "Abra", "Kadabra", "Alakazam", "Machop", "Machoke", "Machamp",
            "Bellsprout", "Weepinbell", "Victreebel", "Tentacool", "Tentacruel",
            "Geodude", "Graveler", "Golem", "Ponyta", "Rapidash", "Slowpoke",
            "Slowbro", "Magnemite", "Magneton", "Farfetch'd", "Doduo", "Dodrio",
            "Seel", "Dewgong", "Grimer", "Muk", "Shellder", "Cloyster",
            "Gastly", "Haunter", "Gengar", "Onix", "Drowzee", "Hypno",
            "Krabby", "Kingler", "Voltorb", "Electrode", "Exeggcute", "Exeggutor",
            "Cubone", "Marowak", "Hitmonlee", "Hitmonchan", "Lickitung", "Koffing",
            "Weezing", "Rhyhorn", "Rhydon", "Chansey", "Tangela", "Kangaskhan",
            "Horsea", "Seadra", "Goldeen", "Seaking", "Staryu", "Starmie",
            "Mr. Mime", "Scyther", "Jynx", "Electabuzz", "Magmar", "Pinsir",
            "Tauros", "Magikarp", "Gyarados", "Lapras", "Ditto", "Eevee",
            "Vaporeon", "Jolteon", "Flareon", "Porygon", "Omanyte", "Omastar",
            "Kabuto", "Kabutops", "Aerodactyl", "Snorlax", "Articuno", "Zapdos",
            "Moltres", "Dratini", "Dragonair", "Dragonite", "Mewtwo", "Mew"
        ]
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        setupCollectionView()
        setupDataSource()
        updateSnapshot(with: mockData) // call the update and pass the data
    }

    // more code ...

Pass the data to the snapshot and run the code, you should see your data in the collection.

result-collection

It should looks and behaves like the video above.

Conclusion#

Congratulations! You’ve successfully set up a UICollectionView using a diffable data source and a custom cell, all programmatically. By following this tutorial, you now understand the basics of UICollectionView and how to leverage diffable data sources to manage your data efficiently. This approach not only simplifies your code but also ensures smooth and animated updates to your collection view.

In this post, we’ve covered:

  • Creating a minimal view controller and adding a UICollectionView to it.
  • Setting up a layout using UICollectionViewFlowLayout.
  • Understanding the importance of a layout object for UICollectionView.
  • Implementing a diffable data source to manage and update your collection view data.
  • Creating and registering a custom cell to display text within your collection view.

With these foundational concepts in place, you’re well-equipped to build more complex and dynamic collection views in your iOS apps. Whether you’re displaying a grid of images, a list of text items, or something entirely custom, UICollectionView and diffable data sources provide the flexibility and power you need.

Stay tuned for more in our UIKit Learning series, where we’ll dive deeper into advanced topics and best practices. Happy coding!

Apendix#

Here is the full code.

import UIKit

class ViewController: UIViewController {
    
    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Int, String>!
    
    private var mockData: [String] {
        [
            "Bulbasaur", "Ivysaur", "Venusaur", "Charmander", "Charmeleon", "Charizard",
            "Squirtle", "Wartortle", "Blastoise", "Caterpie", "Metapod", "Butterfree",
            "Weedle", "Kakuna", "Beedrill", "Pidgey", "Pidgeotto", "Pidgeot",
            "Rattata", "Raticate", "Spearow", "Fearow", "Ekans", "Arbok",
            "Pikachu", "Raichu", "Sandshrew", "Sandslash", "Nidoran♀", "Nidorina",
            "Nidoqueen", "Nidoran♂", "Nidorino", "Nidoking", "Clefairy", "Clefable",
            "Vulpix", "Ninetales", "Jigglypuff", "Wigglytuff", "Zubat", "Golbat",
            "Oddish", "Gloom", "Vileplume", "Paras", "Parasect", "Venonat", "Venomoth",
            "Diglett", "Dugtrio", "Meowth", "Persian", "Psyduck", "Golduck",
            "Mankey", "Primeape", "Growlithe", "Arcanine", "Poliwag", "Poliwhirl",
            "Poliwrath", "Abra", "Kadabra", "Alakazam", "Machop", "Machoke", "Machamp",
            "Bellsprout", "Weepinbell", "Victreebel", "Tentacool", "Tentacruel",
            "Geodude", "Graveler", "Golem", "Ponyta", "Rapidash", "Slowpoke",
            "Slowbro", "Magnemite", "Magneton", "Farfetch'd", "Doduo", "Dodrio",
            "Seel", "Dewgong", "Grimer", "Muk", "Shellder", "Cloyster",
            "Gastly", "Haunter", "Gengar", "Onix", "Drowzee", "Hypno",
            "Krabby", "Kingler", "Voltorb", "Electrode", "Exeggcute", "Exeggutor",
            "Cubone", "Marowak", "Hitmonlee", "Hitmonchan", "Lickitung", "Koffing",
            "Weezing", "Rhyhorn", "Rhydon", "Chansey", "Tangela", "Kangaskhan",
            "Horsea", "Seadra", "Goldeen", "Seaking", "Staryu", "Starmie",
            "Mr. Mime", "Scyther", "Jynx", "Electabuzz", "Magmar", "Pinsir",
            "Tauros", "Magikarp", "Gyarados", "Lapras", "Ditto", "Eevee",
            "Vaporeon", "Jolteon", "Flareon", "Porygon", "Omanyte", "Omastar",
            "Kabuto", "Kabutops", "Aerodactyl", "Snorlax", "Articuno", "Zapdos",
            "Moltres", "Dratini", "Dragonair", "Dragonite", "Mewtwo", "Mew"
        ]
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        setupCollectionView()
        setupDataSource()
        updateSnapshot(with: mockData)
    }
    
    
}

private extension ViewController {
    
    func setupView() {
        view.backgroundColor = .systemBackground
    }
    
    func setupCollectionView() {
        // Creating layout
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 100, height: 100)
        layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        layout.scrollDirection = .horizontal
        
        // creating actual collection
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.backgroundColor = .darkGray
        
        // adding to view
        view.addSubview(collectionView)
        
        // adding constraints
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
        ])
    }
    
    func setupDataSource() {
        // registering cell to the collection
        collectionView.register(CustomCollectionCell.self, forCellWithReuseIdentifier: CustomCollectionCell.name)
        
        // creating and configuring diffable data source
        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, name in
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionCell.name, for: indexPath) as? CustomCollectionCell else { fatalError("Unable to dequeue CustomCollectionCell") }
            cell.backgroundColor = .orange
            cell.update(with: name)
            return cell
        }
    }
    
    // updating the diffable with new data
    func updateSnapshot(with data: [String]) {
        var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
        snapshot.appendSections([0])
        snapshot.appendItems(data) // Adding more items
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

final class CustomCollectionCell: UICollectionViewCell {
    static let name = "CustomCollectionCell"
    
    private var label: UILabel!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLabel()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func update(with data: String) {
        label.text = data
    }
    
    private func setupLabel() {
        label = UILabel()
        label.textAlignment = .center
        
        self.contentView.addSubview(label) // remember to add it to the contentView
        
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: contentView.topAnchor), // we are referring to contentView not view
            label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), // we are referring to contentView not view
            label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), // we are referring to contentView not view
            label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), // we are referring to contentView not view
        ])
    }
}