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. By Peter Fennema.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iOS 14 Tutorial: UICollectionView List
25 mins
- Getting Started
- What is a List?
- Creating a List
- Configuring the Layout
- Configuring the Presentation
- Configuring the Data
- Making the List Expandable
- Configuring the Presentation
- Configuring the Data
- What is Modern Cell Configuration?
- Configuring the Cells
- Adopting a Pet
- What is a Section Snapshot?
- Adding a Section for Adopted Pets
- Configuring the Presentation
- Configuring the Data
- Where to Go From Here?
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:
You assign the closure to provider
. layoutEnv
provides information about the layout environment.
- 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. :]