iOS 14 Tutorial: UICollectionView List
In this tutorial, you’ll learn how to create lists, use modern cell configuration and configure multiple section snapshots on a single collection view.
Version
- Swift 5, iOS 14, Xcode 12

In iOS 14, Apple introduced new features for UICollectionView
. Lists let you include UITableView
-like sections in a UICollectionView
. Modern Cell Configuration makes registering and configuring collection view cells easier. And, Section Snapshots allow for multiple sections in a UICollectionView
, where each section can have a different layout.
In this tutorial, you’ll learn how to:
- Create an expandable list using
UICollectionLayoutListConfiguration
. - Use Modern Cell Configuration to configure
UICollectionView
cells. - Use Section Snapshots to add multiple sections to a
UICollectionView
.
UICollectionViewDiffableDataSource
and UICollectionViewCompositionalLayout
, introduced by Apple in iOS 13. If you haven’t used these before, check out Collection View and Diffable Data Source and Modern Collection Views with Compositional Layouts.
Without further ado, it’s time to get started!
Getting Started
Download the project materials using the Download Materials button at the top or bottom of this tutorial. Open the starter project in Xcode. Build and run.
You’ll see an empty Pet Explorer screen. It’s part of Get a Pet, an app that displays pets available for adoption. You’ll build on top of this app. In the final version, you can browse through pet categories and select a pet to view its details. Then when you’ve found a pet you like, you can tap Adopt to adopt the pet.
The completed app’s Pet Explorer screen shows the available and adopted pets:
Notice the cute dog Diego. When you complete this tutorial, you’ll be the proud owner of this virtual puppy. :]
Open Xcode. Browse around the project. When the app starts, it sets a navigation controller as the initial view controller using a PetExplorerViewController
as the root view controller. Open Main.storyboard to check out this setup.
Open PetExplorerViewController.swift to explore this file. PetExplorerViewController
‘s collectionView
is empty. Later, you’ll populate it with list items that represent pets and pet categories.
Pet.swift has all the data related to pets.
The DataSource
typealias is for convenience. You’ll use it later when configuring the UICollectionView
data source.
The enum Section
represents sections of the UICollectionView
for .availablePets
and .adoptedPets
.
Finally, in the PetExplorerViewController
extension, you’ll find pushDetailForPet(_:withAdoptionStatus:)
. This method presents PetDetailViewController
when the user selects an item.
Open PetDetailViewController.swift. It’s a simple view controller class that displays the pet’s image, name and birth year.
Now that you’ve explored the app’s structure, it’s time to learn about UICollectionView
lists next.
What is a List?
A list is a table view lookalike in a UICollectionView
. You can create a list by applying a configurable UICollectionViewCompositionalLayout
to a section of a UICollectionView
while using only a small amount of code.
You can configure a list to display hierarchical data, with the possibility to collapse and expand list items or to look similar to a traditional table view. If you need a table view in your app you can either use a list with the UICollectionView
API or use the traditional UITableView
.
In most cases, a list is easier to create and configure.
Now it’s time to create your first list.
Creating a List
You’ll create a flat list that shows the pet categories. This will be your first table view without using UITableView
. For a flat list, the advantages of UICollectionView
list over UITableView
may not be immediately apparent. Later, when you’ll make the list expandable, you’ll discover the real benefits of using UICollectionView
list.
UICollectionView
architecture has a clean separation between layout, presentation and data. The sample code for this tutorial follows this pattern. Every time you add a new feature to Get a Pet, you’ll add a block of code for layout first, then for presentation and finally for data.
Configuring the Layout
With iOS 13, Apple introduced UICollectionViewCompositionalLayout
, a new API for building complex layouts. In iOS 14, Apple has added:
static func list(using configuration: UICollectionLayoutListConfiguration) ->
UICollectionViewCompositionalLayout
This enables you to create a list layout in one line of code, without the need for detailed knowledge of the UICollectionViewCompositionalLayout
API. You can configure the appearance, colors, separators, headers and footers of the list with UICollectionLayoutListConfiguration
.
It’s time to apply this to your code:
Open PetExplorerViewController.swift. Add the following method below the line with // MARK: - Functions
:
func configureLayout() {
// 1
let configuration = UICollectionLayoutListConfiguration(appearance: .grouped)
// 2
collectionView.collectionViewLayout =
UICollectionViewCompositionalLayout.list(using: configuration)
}
This configures the layout of the collectionView
. Here, you:
- Create a
configuration
with.grouped
appearance. This gives you a layout configuration that looks like a table view. - Next, you create a
UICollectionViewCompositionalLayout
with list sections, that uses theconfiguration
. You’ll apply this layout to thecollectionView
.
As you can see, the entire layout configuration is only two lines of code.
Call this method at the end of viewDidLoad()
by adding:
configureLayout()
Configuring the Presentation
Now it’s time to create a collection view cell for the list. The cell displays the pet category. You’ll learn about the new way to register cells.
Inside the first PetExplorerViewController
extension block, add:
// 1
func categoryCellregistration() ->
UICollectionView.CellRegistration<UICollectionViewListCell, Item> {
// 2
return .init { cell, _, item in
// 3
var configuration = cell.defaultContentConfiguration()
configuration.text = item.title
cell.contentConfiguration = configuration
}
}
This is your first encounter with modern cell registration and configuration. Here’s what the code does:
-
categoryCellregistration()
creates a cell registration for a cell of typeUICollectionViewListCell
and a data item of typeItem
. This is the modern way of registering collection view cells. - You create the cell registration, passing in a closure to configure the cell. The closure is called when a cell needs to render.
- Then you configure the cell. The pet category is available in
item.title
. Don’t worry if you don’t understand what’s going on yet. This tutorial has an entire section about modern cell configuration.
You’ll call categoryCellregistration()
when configuring the data source.
Configuring the Data
You configured the layout and the cells for the collection view. Now you need a mechanism to create these cells based on the underlying data for the collection view. That’s where the data source comes in.
Add the following method to PetExplorerViewController
:
func makeDataSource() -> DataSource {
// 1
return DataSource(collectionView: collectionView) {
collectionView, indexPath, item -> UICollectionViewCell? in
// 2
return collectionView.dequeueConfiguredReusableCell(
using: self.categoryCellregistration(), for: indexPath, item: item)
}
}
Here’s what you did:
- You create and return a
DataSource
, passing incollectionView
and a closure that provides aUICollectionViewCell
to the data source. - Inside the closure, you ask
collectionView
to dequeue aUICollectionViewCell
. Then you pass the cell registration as a parameter, socollectionView
will know which cell type it has to dequeue.categoryCellregistration()
, which you created a moment ago, contains the logic for the cell configuration.
Add the following property to PetExplorerViewController
:
lazy var dataSource = makeDataSource()
This creates the data source for collectionView
when it’s first needed because you used lazy
in the declaration.
You configured collectionView
‘s layout, presentation and data. Now you’ll populate collectionView
with data items.
Still in PetExplorerViewController.swift, add the following method to PetExplorerViewController
:
func applyInitialSnapshots() {
// 1
var categorySnapshot = NSDiffableDataSourceSnapshot<Section, Item>()
// 2
let categories = Pet.Category.allCases.map { category in
return Item(title: String(describing: category))
}
// 3
categorySnapshot.appendSections([.availablePets])
// 4
categorySnapshot.appendItems(categories, toSection: .availablePets)
// 5
dataSource.apply(categorySnapshot, animatingDifferences: false)
}
This code uses diffable data source to update the list’s content. Apple introduced diffable data source in iOS 13. The code doesn’t have any new iOS 14 features yet. That’ll change when you make the list expandable and add section snapshots to the list.
With applyInitialSnapshots()
you:
- Create a
categorySnapshot
that holds the pet category names. - Then create an
Item
for eachcategory
and add it tocategories
. - Append
.availablePets
tocategorySnapshot
. - Then append the items in
categories
to.availablePets
ofcategorySnapshot
. - Apply
categorySnapshot
todataSource
.
You’ve added a section and indicated all the elements that belong to that section.
Now, add a call to applyInitialSnapshots()
at the end of viewDidLoad()
:
applyInitialSnapshots()
Build and run.
Congratulations! Here’s your first UICollectionView
with a list.
A list supports appearances that match the styles of a UITableView
: .plain
, .grouped
and .insetGrouped
. The list you created has the .grouped
appearance.
iOS 14 has new appearances for presenting list as sidebars: .sidebar
and .sidebarPlain
. They’re typically used as the primary view in a split view.
Now you’ll make the list expandable.
Making the List Expandable
It’s time to add pets to the categories.
Here, you’ll discover the powerful benefits of UICollectionView
lists. With a UITableView
, you would have to handle taps on category cells and pet cells, maintain the visible and expanded state of cells and write the code that shows or hides the pet cells.
With a UICollectionView
list, you only have to provide a hierarchical data structure of categories and pets. The list will take care of the rest. You’ll soon discover how much you can achieve with only a few lines of code.
Pet.swift has the data for all pets and the categories they belong to. There’s no need to change anything in the layout, so you’ll start with the presentation.
Configuring the Presentation
Earlier, you created a cell for a pet category. You learned about the new way to register cells. Here you’ll do the same, this time to create a cell for a pet. The cell will display the pet’s name.
In PetExplorerViewController.swift, add:
func petCellRegistration() ->
UICollectionView.CellRegistration<UICollectionViewListCell, Item> {
return .init { cell, _, item in
guard let pet = item.pet else {
return
}
var configuration = cell.defaultContentConfiguration()
configuration.text = pet.name
cell.contentConfiguration = configuration
}
}
petCellRegistration()
is similar to categoryCellregistration()
you added earlier. You create a cell registration and use modern cell configuration to configure the cell. Here you use defaultContentConfiguration()
and then assign the pet name as the text to display.
You’ll call petCellRegistration()
when configuring the data source.
Next, you’ll make the list expandable by adding an outline disclosure accessory to the category cell. This indicates that an item can expand and collapse. When you tap a category, the list expands and shows the pets for that category.
In categoryCellregistration()
and right below cell.contentConfiguration = configuration
, add:
// 1
let options = UICellAccessory.OutlineDisclosureOptions(style: .header)
// 2
let disclosureAccessory = UICellAccessory.outlineDisclosure(options: options)
// 3
cell.accessories = [disclosureAccessory]
Here, you:
- Create
options
you want to apply todisclosureAccessory
. You use.header
style to make the cell expandable. - Then, create a
disclosureAccessory
with the configuredoptions
. - Apply the accessory to the cell. Cells can have more than one accessory, so you add
disclosureAccessory
in an array.
Build and run.
The outline disclosure is visible, but when you tap a cell, nothing happens. Why? You didn’t add pets to their categories yet. You’ll do that next.
Configuring the Data
Next, you’ll learn how to add hierarchical data to a list. When you’re done, you’ll see that the list automatically supports collapsing and expanding cells.
Now, adapt the data source to add the pet cells to their categories.
In makeDatasource()
, replace:
return collectionView.dequeueConfiguredReusableCell(
using: self.categoryCellregistration(), for: indexPath, item: item)
With:
if item.pet != nil {
// 1
return collectionView.dequeueConfiguredReusableCell(
using: self.petCellRegistration(), for: indexPath, item: item)
} else {
// 2
return collectionView.dequeueConfiguredReusableCell(
using: self.categoryCellregistration(), for: indexPath, item: item)
}
An item can either represent a category or a pet. This depends on the value of pet
. In this code, the collectionView
will dequeue:
- A cell for a pet if
item.pet
is notnil
. - A cell for a category if
item.pet
isnil
.
You configured everything needed to display pets but didn’t add any pets yet. For this to work, you have to update the initial snapshot of the data.
Replace the body of applyInitialSnapshots()
with:
// 1
var categorySnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
// 2
for category in Pet.Category.allCases {
// 3
let categoryItem = Item(title: String(describing: category))
// 4
categorySnapshot.append([categoryItem])
// 5
let petItems = category.pets.map { Item(pet: $0, title: $0.name) }
// 6
categorySnapshot.append(petItems, to: categoryItem)
}
// 7
dataSource.apply(
categorySnapshot,
to: .availablePets,
animatingDifferences: false)
To build the hierarchical relation between categories and pets you:
- Create a
categorySnapshot
of typeNSDiffableDataSourceSectionSnapshot
. This is a section snapshot. With a section snapshot it’s possible to represent data with a hierarchical structure, such as an outline with expandable items.
For now, this is all you need to know about section snapshots. You’ll learn more about section snapshots later in this tutorial. - Then, loop over the categories in
Pet.Category.allCases
. Within the loop, you add the pets to their categories. - Create a
categoryItem
. - Append the
categoryItem
to thecategorySnapshot
. - Then, create an array,
petItems
, containing all pets that belong to the currentcategory
. - Create the hierarchical relationship between categories and pets by appending the
petItems
to the currentcategoryItem
. - Apply
categorySnapshot
to.availablePets
ofdataSource
.
Build and run.
Tap a category. The list expands and shows the pet names. Great!
Now it’s time to make the cells look a little better.
What is Modern Cell Configuration?
If you’ve used a UITableView
or UICollectionView
, you’re used to configuring your cells by directly setting their properties. In iOS 14, cell configuration can be entirely decoupled from the cell itself.
You create a cell content configuration of type UIContentConfiguration
. Then, you set the properties of this content configuration as you like. Similarly, you can create a cell background configuration of type UIBackgroundConfiguration
.
The result is a reusable configuration you can apply to any cell you like.
It’s time to see how this works!
Configuring the Cells
You just learned the theory of modern cell configuration. Now, you’ll put cell content configuration into practice by adding code to update the pet cell to show a pet’s image and age. In the next section, you’ll apply cell background configuration.
In petCellRegistration()
, replace:
var configuration = cell.defaultContentConfiguration()
configuration.text = pet.name
cell.contentConfiguration = configuration
With:
// 1
var configuration = cell.defaultContentConfiguration()
// 2
configuration.text = pet.name
configuration.secondaryText = "\(pet.age) years old"
configuration.image = UIImage(named: pet.imageName)
// 3
configuration.imageProperties.maximumSize = CGSize(width: 40, height: 40)
// 4
cell.contentConfiguration = configuration
Here, you see cell content configuration in action. You:
- Create a
configuration
of typeUIListContentConfiguration
with default styling. With this configuration, you have a cell where you can set an image, text and secondary text. - Apply the pet’s data to the configuration, including an image of the pet.
- Set the size of the image.
- Apply
configuration
tocontentConfiguration
of the cell.
Build and run.
Suddenly, the pets look more cuddly. Are you feeling motivated to adopt one? :]
Adopting a Pet
Finally, you’ll adopt a pet. Diego is waiting for you to pick him up!
First, you’ll learn how to create and apply a background configuration for a cell. Cells with adopted pets will get a background color. You’ll be using UIBackgroundConfiguration
, introduced in iOS 14 as a part of modern cell configuration.
The starter project already has a property to store the adopted pets: adoptions
.
In petCellRegistration()
and below cell.contentConfiguration = configuration
, add:
// 1
if self.adoptions.contains(pet) {
// 2
var backgroundConfig = UIBackgroundConfiguration.listPlainCell()
// 3
backgroundConfig.backgroundColor = .systemBlue
backgroundConfig.cornerRadius = 5
backgroundConfig.backgroundInsets = NSDirectionalEdgeInsets(
top: 5, leading: 5, bottom: 5, trailing: 5)
// 4
cell.backgroundConfiguration = backgroundConfig
}
To give the cell a colored background you:
- Check if the pet was adopted. Only adopted pets will have a colored background.
- Create a
UIBackgroundConfiguration
, configured with the default properties for alistPlainCell
. Assign it tobackgroundConfig
. - Next, modify
backgroundConfig
to your taste. - Assign
backgroundConfig
tocell.backgroundConfiguration
.
You can’t test this yet. You need to adopt a pet first.
The starter project has a PetDetailViewController
. This view controller has the Adopt button. But how do you navigate to the PetDetailViewController
?
You add a disclosure indicator to the pet cell. In petCellRegistration()
and below cell.contentConfiguration = configuration
, add:
cell.accessories = [.disclosureIndicator()]
Here you set the disclosure indicator of the cell.
Now you need to navigate to the PetDetailViewController
when you tap a pet cell.
Add the following code to collectionView(_:didSelectItemAt:)
:
// 1
guard let item = dataSource.itemIdentifier(for: indexPath) else {
collectionView.deselectItem(at: indexPath, animated: true)
return
}
// 2
guard let pet = item.pet else {
return
}
// 3
pushDetailForPet(pet, withAdoptionStatus: adoptions.contains(pet))
collectionView(_:didSelectItemAt:)
is called when you tap a pet cell. In this code you:
- Check if the
item
at the selectedindexPath
exists. - Safe-unwrap
pet
. - Then, push
PetDetailViewController
on the navigation stack.pushDetailForPet()
is part of the starter project.
Build and run. Look for Diego and tap the cell.
Here’s your friend Diego! Tap the Adopt button.
You’ve adopted Diego and navigated back to the Pet explorer. You would expect Diego’s cell to have a blue background, but it doesn’t. What happened?
The data source hasn’t been updated yet. You’ll do that now.
Add the following method to PetExplorerViewController
:
func updateDataSource(for pet: Pet) {
// 1
var snapshot = dataSource.snapshot()
let items = snapshot.itemIdentifiers
// 2
let petItem = items.first { item in
item.pet == pet
}
if let petItem = petItem {
// 3
snapshot.reloadItems([petItem])
// 4
dataSource.apply(snapshot, animatingDifferences: true, completion: nil)
}
}
In this code, you:
- Retrieve all
items
fromdataSource.snapshot()
. - Look for the
item
that representspet
and assign it topetItem
. - Reload
petItem
insnapshot
. - Then apply the updated
snapshot
to the dataSource.
Now make sure you call updateDataSource(for:)
when you adopt a pet.
In petDetailViewController(_:didAdoptPet:)
, add:
// 1
adoptions.insert(pet)
// 2
updateDataSource(for: pet)
This code is called when a user adopts a pet. Here you:
- Insert the adopted
pet
inadoptions
. - Call
updateDataSource(for:)
. This is the method you just created.
Build and run. Tap Diego. Then, on the detail screen, tap Adopt. After navigating back, you’ll see the following screen.
Diego has a blue background. He’s yours now. :]
What is a Section Snapshot?
A section snapshot encapsulates the data for a single section in a UICollectionView
. This has two important benefits:
- Section snapshots make it possible to model hierarchical data. You already applied this when you implemented the list with pet categories.
- A
UICollectionView
data source can have a snapshot per section, instead of a single snapshot for the entire collection view. This lets you add multiple sections to a collection view, where each section can have a different layout and behavior.
You’ll add a section for adopted pets to see how this works.
Adding a Section for Adopted Pets
You want to create your adopted pets list as a separate section in the collectionView
, below the expandable list with the pet categories you created earlier.
Configuring the Layout
Replace the body of configureLayout()
with:
// 1
let provider =
{(_: Int, layoutEnv: NSCollectionLayoutEnvironment) ->
NSCollectionLayoutSection? in
// 2
let configuration = UICollectionLayoutListConfiguration(
appearance: .grouped)
// 3
return NSCollectionLayoutSection.list(
using: configuration,
layoutEnvironment: layoutEnv)
}
// 4
collectionView.collectionViewLayout =
UICollectionViewCompositionalLayout(sectionProvider: provider)
This configures the collectionView
‘s layout on a per section basis. In this code, you:
- Create a closure that returns a
NSCollectionLayoutSection
. You have multiple sections now, and this closure can return a layout for each section separately, based on thesectionIndex
. In this case, your sections are laid out identically so you don’t use thesectionIndex
.You assign the closure to
provider
.layoutEnv
provides information about the layout environment. - Create a configuration for a list with
.grouped
appearance. - Return
NSCollectionLayoutSection.list
for thesection
with the givenconfiguration
. - Create
UICollectionViewCompositionalLayout
withprovider
assectionProvider
. You assign the layout tocollectionView.collectionViewLayout
.
Next, you’ll configure the presentation.
Configuring the Presentation
Add the following method to the first PetExplorerViewController
extension block:
func adoptedPetCellRegistration()
-> UICollectionView.CellRegistration<UICollectionViewListCell, Item> {
return .init { cell, _, item in
guard let pet = item.pet else {
return
}
var configuration = cell.defaultContentConfiguration()
configuration.text = "Your pet: \(pet.name)"
configuration.secondaryText = "\(pet.age) years old"
configuration.image = UIImage(named: pet.imageName)
configuration.imageProperties.maximumSize = CGSize(width: 40, height: 40)
cell.contentConfiguration = configuration
cell.accessories = [.disclosureIndicator()]
}
}
This code is applied to a cell in .adoptedPets
. It should look familiar to you. It’s similar to petCellRegistration()
you added in Configuring the Cells. Now, you’ll configure the data.
Configuring the Data
In makeDatasource()
, replace:
return collectionView.dequeueConfiguredReusableCell(
using: self.petCellRegistration(), for: indexPath, item: item)
With:
// 1
guard let section = Section(rawValue: indexPath.section) else {
return nil
}
switch section {
// 2
case .availablePets:
return collectionView.dequeueConfiguredReusableCell(
using: self.petCellRegistration(), for: indexPath, item: item)
// 3
case .adoptedPets:
return collectionView.dequeueConfiguredReusableCell(
using: self.adoptedPetCellRegistration(), for: indexPath, item: item)
}
With this code, you make the cell returned by the data source dependent on the section. Here you:
- Safely unwrap
section
. - Return a
petCellRegistration()
for.availablePets
- Return an
adoptedPetCellRegistration()
for.adoptedPets
It’s time to add the sections to the data source.
In applyInitialSnapshots()
, insert the following code at the beginning of the method:
// 1
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
// 2
snapshot.appendSections(Section.allCases)
// 3
dataSource.apply(snapshot, animatingDifferences: false)
In this code you:
- Create a new
snapshot
. - Append all sections to
snapshot
. - Apply the snapshot to
dataSource
.
Build and run. Adopt Diego. :]
Diego has a blue background so you know the adoption succeeded. But where’s the section you added?
The section is there, but it’s empty. You added Diego to adoptedPets
, but didn’t insert him into the data source yet. That’s what you’ll do now.
In petDetailViewController(_:didAdoptPet:)
, right below adoptions.insert(pet)
, add:
// 1
var adoptedPetsSnapshot = dataSource.snapshot(for: .adoptedPets)
// 2
let newItem = Item(pet: pet, title: pet.name)
// 3
adoptedPetsSnapshot.append([newItem])
// 4
dataSource.apply(
adoptedPetsSnapshot,
to: .adoptedPets,
animatingDifferences: true,
completion: nil)
With this code you:
- Retrieve a snapshot for
.adoptedPets
fromdataSource
. You assign it toadoptedPetsSnapshot
. - Create a new
Item
for the adoptedpet
and assign it tonewItem
. - Append
newItem
toadoptedPetsSnapshot
. - You apply the modified
adoptedPetsSnapshot
to.adoptedPets
of thedataSource
.
Build and run.
It works! Diego is in the section for adopted pets. :]
Where to Go From Here?
You can download the final project by using the Download Materials button at the top or bottom of this page.
You’ve learned a lot about UICollectionView improvements in iOS 14. This includes:
- Creating an expandable list using
UICollectionLayoutListConfiguration.
- Use Modern Cell Configuration to configure
UICollectionView
cells. - Use Section Snapshots to add multiple sections to a
UICollectionView
.
And you only touched the surface. For more details, check out the WWDC 2020 session Advances in UICollectionView.
If you have any questions or comments, join the forum below!
Comments