Welcome to our Learn At Home Sale!

Limited-time Advanced Swift & Android book bundles, plus 50% off all books

Home · iOS & Swift Tutorials

iOS Tutorial: Collection View and Diffable Data Source

In this iOS tutorial, you’ll learn how to implement a collection view with UICollectionViewDiffableDataSource and NSDiffableDataSourceSnapshot.

4.8/5 13 Ratings

Version

  • Swift 5, iOS 13, Xcode 11

In iOS 13, Apple introduced a major update to the UICollectionView API: UICollectionViewDiffableDataSource. This new API is more flexible and declarative than the complicated, brittle and error-prone UICollectionViewDataSource API.

In this tutorial, you’ll learn how to:

  • Replace the old UICollectionViewDataSource with the new UICollectionViewDiffableDataSource.
  • Use NSDiffableDataSourceSnapshot.
  • Add sections to the data source.
  • Add supplementary views to the data source.

Further, you’ll see how easy it is to animate changes with this new type of data source. I hope you’re excited to get started!

Getting Started

Start by downloading the project materials using the Download Materials button at the top or bottom of this tutorial.

The project, fittingly called RayTube, allows you to browse a collection of RayWenderlich video courses, search for a specific one, and tap it to view more details about it.

First up, open the starter project. Build and run.

UICollectionView with RayWenderlich courses as collection view cells.

You’ll see a list of RayWenderlich videos. Tap a video to see its details. Also, try searching for a specific video by title using the search bar.

At the moment, the filtered videos aren’t animated when you perform a search query.

The search results appearing without animation as the user searches

Obviously, this isn’t ideal. This UI looks like it stutters, which may not be the animation you want. You need to that smooth animation! While adding a diffable data source, you will automagically solve this issue as well.

What is UICollectionViewDiffableDataSource?

Before iOS 13, you’d configure a UICollectionView‘s data source by adopting UICollectionViewDataSource. This protocol tells the collection view what cell to display, how many cells to display, which section to display the cells in, and so on.

Note: If you haven’t used UICollectionView before, check out UICollectionView Tutorial: Getting Started to understand how it works.

The new UICollectionViewDiffableDataSource abstracts a significant amount of UICollectionViewDataSource‘s logic. This leaves less room for client code errors when handling collection view’s data source.

Rather than telling the data source how many items to display, you tell it what sections and items to display.

The diffable part of UICollectionViewDiffableDataSource means that whenever you update the items you’re displaying, the collection view will automatically calculate the difference between the updated collection and the one previously shown. This will in turn cause the collection view to animate the changes, such as updates, insertions and deletions.

Benefits of UICollectionViewDiffableDataSource

Here are three benefits of implementing UICollectionViewDiffableDataSource:

  1. Automatic data change animations: Whenever you add, update or delete data, you can get the data change animation automatically.
  2. Automatic data synchronization: To utilize collection view’s standard animation without UICollectionViewDiffableDataSource, you’d have to manually manage and synchronize data changes between the collection view and the data source. If you have a misalignment in one of the synchronization operations, you’d see an error like this:

    Assertion error because the collection view update is invalid

  3. Reduced code: Overall, you can write less code and benefit from the collection view’s data change animations and data synchronization.

Smart, right!? So, how do you make use of the new UICollectionViewDiffableDataSource? More on this next.

Creating a Diffable Data Source

UICollectionViewDiffableDataSource has two generic types: Section type and item type. If you’ve used collection views before, you should be familiar with the concept of sections and items.

To create your section type, add the following code below videoList in VideosViewController.swift:

enum Section {
  case main
}

Now that you’ve created the Section enum, it’s time to create the diffable data source.

To keep things concise, create a type alias for the data source. This mitigates the need to write UICollectionViewDiffableDataSource value types when you need to configure the data source as well as every time you need to reference the same data type.

Below the section type, write the following code to declare a DataSource type alias:

typealias DataSource = UICollectionViewDiffableDataSource<Section, Video>

Awesome! Build and run.

An Xcode error saying Video doesn't conform to Hashable

Swift bird saying "Uh oh... What is Hashable?"

As it turns out, your video data type needs to conform to Hashable.

Implementing Hashable

Hashable allows the diffable data source to perform updates when videos are added, removed or updated. Conformance to the protocol is needed in order to know whether or not two elements are equal to each other.

Open Video.swift. Make Video adopt Hashable:

class Video: Hashable {

Next, you need to implement the protocol methods. Add the following code below init(title:thumbnail:lessonCount:link:):

// 1
func hash(into hasher: inout Hasher) {
  // 2
  hasher.combine(id)
}

// 3
static func == (lhs: Video, rhs: Video) -> Bool {
  lhs.id == rhs.id
}

Here’s what you did:

  1. Implemented hash(into:), which hashes the given components.
  2. Added the id of Video to the hash. For videos, you only need the ID to know whether two videos are equal.
  3. Implemented the Equatable protocol’s == function, because all Hashable objects must also be Equatable.

Your project should now be able to build again without any errors.

Configuring The Diffable Data Source

Open VideosViewController.swift. Now that Video conforms to Hashable, you can finish creating the diffable data source.

Below viewDidLoad(), add the following code:

func makeDataSource() -> DataSource {
  // 1
  let dataSource = DataSource(
    collectionView: collectionView,
    cellProvider: { (collectionView, indexPath, video) ->
      UICollectionViewCell? in
      // 2
      let cell = collectionView.dequeueReusableCell(
        withReuseIdentifier: "VideoCollectionViewCell",
        for: indexPath) as? VideoCollectionViewCell
      cell?.video = video
      return cell
  })
  return dataSource
}

Here:

  1. You create a dataSource, passing in collectionView and a cellProvider callback.
  2. Inside the cellProvider callback, you return a VideoCollectionViewCell. The code you write in this function is the same as you’re used to seeing in UICollectionViewDataSource‘s collectionView(_:cellForItemAt:).

Now that you’ve implemented makeDataSource() you can delete the data source methods under `// MARK: - UICollectionViewDataSource`. Specifically, delete the following two methods:

  • collectionView(_: numberOfItemsInSection:)
  • collectionView(_:cellForItemAt:)

You can delete these methods because the diffable data source automatically handles these functionalities for you.

Next, it’s time to actually use the makeDataSource() you worked so hard on!

In VideosViewController, add the following property to the top:

private lazy var dataSource = makeDataSource()

This creates the data source for the collection view. You must mark it lazy because Swift requires VidoesViewController to complete initialization before you can call makeDataSource().
Finally, inside collectionView(_:didSelectItemAt:) replace:

let video = videoList[indexPath.row]

…with:

guard let video = dataSource.itemIdentifier(for: indexPath) else {
  return
}

This ensures the app retrieves videos directly from the dataSource. This is important because UICollectionViewDiffableDataSource might do work in the background that makes videoList inconsistent with the currently displayed data.

Build and run.

The RayTube app with no videos displayed

And…nothingness.

Before the collection view can display any cells, you need to tell it what data you’d like to display. This is where snapshots come in!

Using NSDiffableDataSourceSnapshot

NSDiffableDataSourceSnapshot stores your sections and items, which the diffable data source references to understand how many sections and cells to display.

Just like you created a type alias for UICollectionViewDiffableDataSource, you can create a Snapshot type alias as well.

Add the following type alias to VideosViewController:

typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Video>

NSDiffableDataSourceSnapshot, like UICollectionViewDiffableDataSource, takes a section type and an item type: Section and Video.

Now, it’s time to create a snapshot!

Below viewDidLoad(), add the following method:

// 1
func applySnapshot(animatingDifferences: Bool = true) {
  // 2
  var snapshot = Snapshot()
  // 3
  snapshot.appendSections([.main])
  // 4
  snapshot.appendItems(videoList)
  // 5
  dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}

With applySnapshot(animatingDifferences:) you:

  1. Create a new method that applies a snapshot to the data source. The method takes a Boolean which determines if changes to the data source should animate.
  2. Create a new Snapshot object.
  3. Add the .main section to the snapshot. This is the only section type you currently have defined for your application.
  4. Add the video array to the snapshot.
  5. Tell the dataSource about the latest snapshot so it can update and animate accordingly.

Great! Now call the method at the end of viewDidLoad():

applySnapshot(animatingDifferences: false)

Build and run.

The RayTube app with multiple videos on screen

It works! But there is a small problem. Search for something, and you’ll notice the user interface doesn’t update at all.

The user types text into the search field, but the UI doesn't update

Fortunately, fixing the search feature is super easy!

Fixing Search

Inside updateSearchResults(for:), replace:

collectionView.reloadData()

With:

applySnapshot()

Instead of reloading the entire collection view, you new apply a new snapshot to the database, which will cause the changes to animate.

Build and run. Type No in the search bar and watch the UI animate:

The user types text into the search field, and the UI animates to reflect their search query

If you run the app on an iPad, the animation is even more complex, all for free!

The user types text into the iPad search field, and the UI animates to reflect their search query

Success! Next, you’re going to learn how to implement multiple sections.

Multiple Sections

There are two ways you can implement multiple sections using the diffable data source API.

Option One

Remember the Section enum?

[spoiler title=”The Section enum”]

enum Section {
  case main
}

[/spoiler]

You can add another case to the enum to implement multiple sections. This option is great when you have a predefined set of sections you’d like to display. For example, a messaging app with a friends section and an others section.

However, if you have no easy way of knowing what sections you’d like your app to display, option two is for you!

Option Two
The second option is to change Section from a value type to a class. Afterward, you can freely create any number of these objects without having to predefine each section.

This option is great if you have a server that provides categories that can change at any time, or if you allow users to create sections dynamically.

Because the RayTube app has more than a few sections, and because these sections may change later if your data changes, you’ll go with this option.

Creating the Section Class

Inside VideosViewController.swift, remove the following code:

enum Section {
  case main
}

Next, create a new file named Section.swift. Add the following code to the file:

import UIKit
// 1
class Section: Hashable {
  var id = UUID()
  // 2
  var title: String
  var videos: [Video]
  
  init(title: String, videos: [Video]) {
    self.title = title
    self.videos = videos
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
  
  static func == (lhs: Section, rhs: Section) -> Bool {
    lhs.id == rhs.id
  }
}

Here’s what you did:

  1. Like the Video class, you conform Section to Hashable.
  2. Section has two important properties that you’ll use in a moment to categorize the videos: title and videos.

Next, you need to create some sections. Open Video.swift and scroll down. You’ll notice an extension on the class with a static property named allVideos. This array stores all the videos the app displays.

Now that you’re introducing multiple sections, this property is insufficient. Remove all the code below // MARK: - Video Sample Data.

This will cause an issue in the places where you use Video.allVideos, but don’t worry about that for now. You’ll fix that in just a moment.

Next, open Section.swift. Then paste the following code at the bottom of the file:

extension Section {
  static var allSections: [Section] = [
    Section(title: "SwiftUI", videos: [
      Video(
        title: "SwiftUI",
        thumbnail: UIImage(named: "swiftui"),
        lessonCount: 37,
        link: URL(string: "https://www.raywenderlich.com/4001741-swiftui")
      )
    ]),
    Section(title: "UIKit", videos: [
      Video(
        title: "Demystifying Views in iOS",
        thumbnail: UIImage(named: "views"),
        lessonCount: 26,
        link:
        URL(string:
          "https://www.raywenderlich.com/4518-demystifying-views-in-ios")
      ),
      Video(
        title: "Reproducing Popular iOS Controls",
        thumbnail: UIImage(named: "controls"),
        lessonCount: 31,
        link: URL(string: """
          https://www.raywenderlich.com/5298-reproducing
          -popular-ios-controls
          """)
      )
    ]),
    Section(title: "Frameworks", videos: [
      Video(
        title: "Fastlane for iOS",
        thumbnail: UIImage(named: "fastlane"),
        lessonCount: 44,
        link: URL(string:
          "https://www.raywenderlich.com/1259223-fastlane-for-ios")
      ),
      Video(
        title: "Beginning RxSwift",
        thumbnail: UIImage(named: "rxswift"),
        lessonCount: 39,
        link: URL(string:
          "https://www.raywenderlich.com/4743-beginning-rxswift")
      )
    ]),
    Section(title: "Miscellaneous", videos: [
      Video(
        title: "Data Structures & Algorithms in Swift",
        thumbnail: UIImage(named: "datastructures"),
        lessonCount: 29,
        link: URL(string: """
          https://www.raywenderlich.com/977854-data-structures
          -algorithms-in-swift
        """)
      ),
      Video(
        title: "Beginning ARKit",
        thumbnail: UIImage(named: "arkit"),
        lessonCount: 46,
        link: URL(string:
          "https://www.raywenderlich.com/737368-beginning-arkit")
      ),
      Video(
        title: "Machine Learning in iOS",
        thumbnail: UIImage(named: "machinelearning"),
        lessonCount: 15,
        link: URL(string: """
          https://www.raywenderlich.com/1320561-machine-learning-in-ios
        """)
      ),
      Video(
        title: "Push Notifications",
        thumbnail: UIImage(named: "notifications"),
        lessonCount: 33,
        link: URL(string:
          "https://www.raywenderlich.com/1258151-push-notifications")
      ),
    ])
  ]
}

Phew, lots of code. Here you created a static property allSections which has four sections with one or more videos each. This is basically just dummy data — in a fully fledged application you would fetch this information from a server.

With this, you can now access the app’s sections using the Section.allSections property.

Adopting the New Section Class

Head back to VideosViewController.swift.

Replace:

private var videoList = Video.allVideos

…with:

private var sections = Section.allSections

Next, you need to update applySnapshot(animatingDifferences:) to work with Section.

Replace the following code in applySnapshot(animatingDifferences:):

snapshot.appendSections([.main])
snapshot.appendItems(videos)

…with:

snapshot.appendSections(sections)
sections.forEach { section in
  snapshot.appendItems(section.videos, toSection: section)
}

There are two changes here. First, you append the sections array to the snapshot. Second, you loop over each section and add its items (videos) to the snapshot.

You also specify each video’s section explicitly, because the data source won’t infer the item’s section correctly now that there are multiple sections.

Fixing Search, Again

Now that you have sections in the app, you need to fix the search feature again. This is the last time, I promise. The previous method that processed a search query returned an array of videos, so you need to write a new method that returns an array of sections.

Replace filteredVideos(for:) with the following method:

func filteredSections(for queryOrNil: String?) -> [Section] {
  let sections = Section.allSections

  guard 
    let query = queryOrNil, 
    !query.isEmpty 
    else {
      return sections
  }
    
  return sections.filter { section in
    var matches = section.title.lowercased().contains(query.lowercased())
    for video in section.videos {
      if video.title.lowercased().contains(query.lowercased()) {
        matches = true
        break
      }
    }
    return matches
  }
}

This new filter returns all sections whose name matches the search criteria plus those that contain a video whose title matches the search.

Inside updateSearchResults(for:), replace:

videoList = filteredVideos(for: searchController.searchBar.text)

…with:

sections = filteredSections(for: searchController.searchBar.text)

This switches out the search filter for the new section-based version you just implemented.

Phew! That was quite an adventure.

You can see the sections better on an iPad, so use an iPad simulator. Build and run.

The videos are separated into sections

Awesome! The videos are categorized! But, there isn’t an easy way to see what category a video is a part of.

Supplementary Views

To add a header to the sections, you need to implement a supplementary header view. Don’t worry because this isn’t as complicated as it sounds.

First, create a new file named SectionHeaderReusableView.swift. Add the following code to the file:

import UIKit

// 1
class SectionHeaderReusableView: UICollectionReusableView {
  static var reuseIdentifier: String {
    return String(describing: SectionHeaderReusableView.self)
  }

  // 2
  lazy var titleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = UIFont.systemFont(
      ofSize: UIFont.preferredFont(forTextStyle: .title1).pointSize,
      weight: .bold)
    label.adjustsFontForContentSizeCategory = true
    label.textColor = .label
    label.textAlignment = .left
    label.numberOfLines = 1
    label.setContentCompressionResistancePriority(
      .defaultHigh, 
      for: .horizontal)
    return label
  }()
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    // 3
    backgroundColor = .systemBackground
    addSubview(titleLabel)

    if UIDevice.current.userInterfaceIdiom == .pad {
      NSLayoutConstraint.activate([
        titleLabel.leadingAnchor.constraint(
          equalTo: leadingAnchor, 
          constant: 5),
        titleLabel.trailingAnchor.constraint(
          lessThanOrEqualTo: trailingAnchor, 
          constant: -5)])
    } else {
      NSLayoutConstraint.activate([
        titleLabel.leadingAnchor.constraint(
          equalTo: readableContentGuide.leadingAnchor),
        titleLabel.trailingAnchor.constraint(
          lessThanOrEqualTo: readableContentGuide.trailingAnchor)
      ])
    }
    NSLayoutConstraint.activate([
      titleLabel.topAnchor.constraint(
        equalTo: topAnchor, 
        constant: 10),
      titleLabel.bottomAnchor.constraint(
        equalTo: bottomAnchor, 
        constant: -10)
    ])
  }
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

This is quite the block of code, but there’s not much to it. In short, the view has one label which displays the title of the section. Going over the code:

  1. You add a class and make it a subclass of UICollectionReusableView. This means the section header view can be reused just like the cells.
  2. You setup the title label’s style
  3. On initialization, you add the title label to the header view and set up its Auto Layout constraints. Depending on whether you’re on an iPad or not you use different rules.

Open VideosViewController.swift. Below // MARK: - Layout Handling, add the following code to the beginning of configureLayout():

collectionView.register(
  SectionHeaderReusableView.self, 
  forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, 
  withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier
)

This registers the header view you just wrote with the collection view, so you can use section headers.

Next, in the same method, add the following code inside the sectionProvider closure and right before return section:

// Supplementary header view setup
let headerFooterSize = NSCollectionLayoutSize(
  widthDimension: .fractionalWidth(1.0), 
  heightDimension: .estimated(20)
)
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
  layoutSize: headerFooterSize, 
  elementKind: UICollectionView.elementKindSectionHeader, 
  alignment: .top
)
section.boundarySupplementaryItems = [sectionHeader]

This code tells the layout system that you’d like to display a header for every section.

You’re almost done! Inside makeDataSource(), add the following code right before return dataSource:

// 1
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
  // 2
  guard kind == UICollectionView.elementKindSectionHeader else {
    return nil
  }
  // 3
  let view = collectionView.dequeueReusableSupplementaryView(
    ofKind: kind,
    withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier,
    for: indexPath) as? SectionHeaderReusableView
  // 4
  let section = self.dataSource.snapshot()
    .sectionIdentifiers[indexPath.section]
  view?.titleLabel.text = section.title
  return view
}

Here you:

  1. Get an instance of the section for the supplementary view.
  2. Ensure the supplementary view provider asks for a header.
  3. Dequeue a new header view.
  4. Retrieve the section from the data source, then set the titleLabel‘s text value to the section‘s title.

Build and run.

Final build and run screenshot with section headers.

And, here’s how the app looks on an iPad:

The sections now have titles

Success!

Where to Go From Here?

Great job getting this far! You can download the final project by using the Download Materials button at the top or bottom of this page.

In this tutorial, you’ve learned how to add UICollectionViewDiffableDataSource to your existing collection view-based project.

If you want a challenge, try to introduce different collection view cells based on the item returned by the data source.

The sample app also uses compositional layouts. If you’d like to learn more about them, check out Modern Collection Views with Compositional Layouts.

If you have any questions or comments, join the forum below!

Average Rating

4.8/5

Add a rating for this content

13 ratings

More like this

Contributors

Comments