Working With Collections
Table of Contents
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 theview
.
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.
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
])
}
}