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.

4.8 (12) · 3 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. Check if the pet was adopted. Only adopted pets will have a colored background.
  2. Create a UIBackgroundConfiguration, configured with the default properties for a listPlainCell. Assign it to backgroundConfig.
  3. Next, modify backgroundConfig to your taste.
  4. Assign backgroundConfig to cell.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:

  1. Check if the item at the selected indexPath exists.
  2. Safe-unwrap pet.
  3. Then, push PetDetailViewController on the navigation stack. pushDetailForPet() is part of the starter project.

Build and run. Look for Diego and tap the cell.

Pet Explorer detail

Here’s your friend Diego! Tap the Adopt button.

Pet Explorer no background

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:

  1. Retrieve all items from dataSource.snapshot().
  2. Look for the item that represents pet and assign it to petItem.
  3. Reload petItem in snapshot.
  4. 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:

  1. Insert the adopted pet in adoptions.
  2. 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.

Pet Explorer background

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:

  1. Section snapshots make it possible to model hierarchical data. You already applied this when you implemented the list with pet categories.
  2. 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.

  1. 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 the sectionIndex. In this case, your sections are laid out identically so you don’t use the sectionIndex.
  2. Create a configuration for a list with .grouped appearance.
  3. Return NSCollectionLayoutSection.list for the section with the given configuration.
  4. Create UICollectionViewCompositionalLayout with provider as sectionProvider. You assign the layout to collectionView.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:

  1. Safely unwrap section.
  2. Return a petCellRegistration() for .availablePets
  3. 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:

  1. Create a new snapshot.
  2. Append all sections to snapshot.
  3. Apply the snapshot to dataSource.

Build and run. Adopt Diego. :]

Pet Explorer no new section

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:

  1. Retrieve a snapshot for .adoptedPets from dataSource. You assign it to adoptedPetsSnapshot.
  2. Create a new Item for the adopted pet and assign it to newItem.
  3. Append newItem to adoptedPetsSnapshot.
  4. You apply the modified adoptedPetsSnapshot to .adoptedPets of the dataSource.

Build and run.

Pet Explorer final diego

It works! Diego is in the section for adopted pets. :]