Debugging UIKit Views with Reveal!

Learn how to find layout and rendering problems in UIKit views, and fix them,
with the Reveal app. Brought to you by Itty Bitty Apps.

Home iOS & Swift Tutorials

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.

4.7/5 6 Ratings

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.
Note: This tutorial assumes you’re familiar with 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.

Pet Explorer empty screen

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:

Pet Explorer final screen

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.

Note: The 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:

  1. Create a configuration with .grouped appearance. This gives you a layout configuration that looks like a table view.
  2. Next, you create a UICollectionViewCompositionalLayout with list sections, that uses the configuration. You’ll apply this layout to the collectionView.

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:

  1. categoryCellregistration() creates a cell registration for a cell of type UICollectionViewListCell and a data item of type Item. This is the modern way of registering collection view cells.
  2. You create the cell registration, passing in a closure to configure the cell. The closure is called when a cell needs to render.
  3. 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:

  1. You create and return a DataSource, passing in collectionView and a closure that provides a UICollectionViewCell to the data source.
  2. Inside the closure, you ask collectionView to dequeue a UICollectionViewCell. Then you pass the cell registration as a parameter, so collectionView 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:

  1. Create a categorySnapshot that holds the pet category names.
  2. Then create an Item for each category and add it to categories.
  3. Append .availablePets to categorySnapshot.
  4. Then append the items in categories to .availablePets of categorySnapshot.
  5. Apply categorySnapshot to dataSource.

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.

Pet Explorer group appearance

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:

  1. Create options you want to apply to disclosureAccessory. You use .header style to make the cell expandable.
  2. Then, create a disclosureAccessory with the configured options.
  3. Apply the accessory to the cell. Cells can have more than one accessory, so you add disclosureAccessory in an array.

Build and run.

Pet Explorer disclosure

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:

  1. A cell for a pet if item.pet is not nil.
  2. A cell for a category if item.pet is nil.

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:

  1. Create a categorySnapshot of type NSDiffableDataSourceSectionSnapshot. 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.
  2. Then, loop over the categories in Pet.Category.allCases. Within the loop, you add the pets to their categories.
  3. Create a categoryItem.
  4. Append the categoryItem to the categorySnapshot.
  5. Then, create an array, petItems, containing all pets that belong to the current category.
  6. Create the hierarchical relationship between categories and pets by appending the petItems to the current categoryItem.
  7. Apply categorySnapshot to .availablePets of dataSource.

Build and run.

Pet Explorer disclosure expanded

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:

  1. Create a configuration of type UIListContentConfiguration with default styling. With this configuration, you have a cell where you can set an image, text and secondary text.
  2. Apply the pet’s data to the configuration, including an image of the pet.
  3. Set the size of the image.
  4. Apply configuration to contentConfiguration of the cell.

Build and run.

Pet Explorer images

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:

  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:

  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.

    You assign the closure to provider. layoutEnv provides information about the layout environment.

  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. :]

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:

  1. Creating an expandable list using UICollectionLayoutListConfiguration.
  2. Use Modern Cell Configuration to configure UICollectionView cells.
  3. 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!

Average Rating

4.7/5

Add a rating for this content

6 ratings

More like this

Contributors

Comments