UICollectionViewDiffableDataSource and Decodable, step by step.

Image for post
Image for post

I don’t know you but this year WWDC left me with mixed feelings, on one hand, it made me feel already outdated and on the other, you know 🤯.

From Swift UI, to new ways of handling layout in collection views and managing Datasource states, the new features that are available for developers are just mind-blowing.

On this post, I will talk about one of the API enhancements that got me excited the most, Diffable DataSource. I’ve been working with collection views a lot lately and I can see why these changes where needed, from the complexity of building a custom layout to crashes like this when handling Data source updates…

Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (1) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).

Managing the state of the data source to be in sync with the state of the Collection view can give you headaches if you are not careful, lucky for us seems that we are not alone and Apple reacted by giving us a way to deal with this in a more reliable way, introducing “UICollectionViewDiffableDataSource” and “NSDiffableDataSourceSnapshot”.

I saw many tutorials online that shows how to use this API but couldn’t find one that uses a network response and updates sections dynamically, so we will do this using the Decodable protocol to decode a response from the SWAPI API (by the way the Star Wars API its pretty cool and if you can, donate to keep it running 😎). This project has all the networking layer you need to get lists of Star Wars related items, it was built using generics and its a good example in how to take advantage of them to build reusable code with minimum code, go explore the files and if you want to know more about how to build Generic API’s check this post.

Ok now let’s go step by step, we are going to build a simple list with section headers using a collection view, in them, we will display a list of Star Wars Movies…

Step 1:

The first thing we will need is a layout for our sectioned Collection view list kind, we won’t go in details about this because its a very extensive topic that we may need a new post just for it, for now, understand that this is how you can build custom layouts in iOS 13, copy and paste this method.

func createLayout() -> UICollectionViewLayout {        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),                                              heightDimension: .fractionalHeight(1.0))        let item = NSCollectionLayoutItem(layoutSize: itemSize)        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),                                               heightDimension: .absolute(44))        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])        let section = NSCollectionLayoutSection(group: group)        section.interGroupSpacing = 5        section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)        let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),                                                      heightDimension: .estimated(44))        let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(            layoutSize: headerFooterSize,            elementKind:  UICollectionView.elementKindSectionHeader, alignment: .top)        section.boundarySupplementaryItems = [sectionHeader]        let layout = UICollectionViewCompositionalLayout(section: section)        return layout    }

Before jumping into step 2 let’s understand what exactly we need…

We need a UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> object, ok but what does this mean? well…

SectionIdentifierType is an object that must conform to Hashable and represent a section in the data source, and ItemIdentifierType is an object that ALSO conforms to Hashable and represents each element in that section.

From Apple examples I saw that we can use an enum for the SectionIdentifierType and that’s cool I ❤️ enums, however, looks like each case represents one section, our apps usually have more than one section, and most of the time with dynamic content we have no idea on how many will be, so if we use an enum as identifier we will have to add a case for each section sounds easy!…well not so much enums can not be created dynamically they need to be compiled, so we cant just create cases on the fly, we need something more flexible.

I am a huge fan of generics in Swift so I created a workaround to be able to display dynamic content with N amount of sections. Think about how each section will look like, usually it has a header with an object that has some content like a title and also an array of items; to be able to use DiffableDatasource we need to provide objects that conform to Hashable, so each section will look something like this, create a new file and add…

struct Section<U: Hashable, T: Hashable>: Hashable {    let headerItem: U    let items: T

Step 2:

We will display Films in our list for this example, in order to do that we need to navigate into the “models” folder and open the “Film” file, Film its a struct that conforms to Decodable now let’s also make it conform to “Hashable” remember that all the stored properties in a Struct also need to conform to “Hashable” In our Film object all of the properties satisfies that requirement, so we only need to add the “Hashable” protocol like so…

struct Film: Decodable, Hashable { // Properties...

Now let’s create a “Header” object for each of our section headers…open a new file and call it “Header” copy and paste…

struct Header: Hashable {    let titleHeader: String

Step 3:

Go to the ViewController and create a new property…

private var dataSource: UICollectionViewDiffableDataSource<Section<Header, [Film]>, Film>

The syntax can be pretty intimidating for the first time and mostly if you are not very familiar with Swift generics but don’t worry just remember that anything inside <> is a generic constraint, this means that the object is constrained to use the type declared on it for any functionality associated with that particular instance, if you want to know more about generics check this post.

Let’s now configure the data source, in “ViewDidLoad” call this method…

private func configureDataSource() {    dataSource = UICollectionViewDiffableDataSource.init(collectionView: collectionView, cellProvider: { collectionView, indexPath, film  in           let cell: MovieCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
cell.configureCell(with: film)
return cell

Now “viewDidLoad” should look like this…

override func viewDidLoad() {     super.viewDidLoad()     configureDataSource()

For our example purpose before starting with step 4, we need to do a bit of a hack to get a two-dimensional array for our sections, this is because the SWAPI API will return a one-dimensional array in the payload, no big deal we can do a convenient method to do so, I added an array Extension that will let us split the array in half, in the view controller you need to add this inside the handle(_ films: [Film]) method…

private func handle(_ films: [Film]) {        let splitArray = films.splitInTwoDimensions()    }

We also need an array of items for each section, right? for this example an array of Strings to be used as the title for each header it’s more than enough. When we work with dynamic content in tables or collection views that displays headers, the content for each header is modeled in the payload, in our case and just for this example we know that we will only have 2 sections so we need to provide an array with 2 titles.

Disclaimer: this implementation is intended to be used for dynamic content where we have no idea in the number of sections, however, because of the API limitation we have to provide a mock implementation, hope it makes sense.

Create a new variable in the View Controller…

private let starWarMovieSectionTitles = ["Classics", "Not that cool stuff"]

Step 4:

Same as UICollectionViewDiffableDataSource, NSDiffableDataSourceSnapshot needs a SectionIdentifierType and ItemIdentifierType, both are defined as a generic constraint, and it’s very important that the types used in NSDiffableDataSourceSnapshot are the same as the ones used in UICollectionViewDiffableDataSource

Create a new property to be used as a reference for the current snapshot…

private var currentSnapshot: NSDiffableDataSourceSnapshot<Section<Header, [Film]>, Film>?

Step 5:

NSDiffableDataSourceSnapshot has two methods that we need to populate our data source, “appendSections” and “appendItems”, seems that we can use a model that holds an array of Section<Header, [Film]> items, “Section” conforms to Hashable so we can do something like this…

struct DataSource<T: Hashable> {
let sections: [T]

Now, inside the handle(_ films: [Film]) method …

private func handle(_ films: [Film]) {        let splitedArray = films.splitInTwoDimensions()        // a        var sectionItems = [Section<Header, [Film]>]()        // b        for (title, films) in zip(starWarMovieSectionTitles, splitedArray) {            sectionItems.append(Section(headerItem: Header(titleHeader: title), items: films))        }        // c        let payloadDatasource = DataSource(sections: sectionItems)        // d        currentSnapshot = NSDiffableDataSourceSnapshot<Section<Header, [Film]>, Film>()        // e        payloadDatasource.sections.forEach {            currentSnapshot.appendSections([$0])            currentSnapshot.appendItems($0.items)        }        // f        self.dataSource?.apply(currentSnapshot, animatingDifferences: true)    }

So let’s go piece by piece here…

a) Create a mutable array to append the new Section Items. (this step is only needed for this mock implementation)

b) Zip the “starWarMovieSectionTitles” array and the “splitedArray” array in order to create “Section” objects (this step is only needed for this mock implementation)

c) Now that we have an Array of Sections we can Instantiate the DataSource with them.

d) Same as UICollectionViewDiffableDataSource, NSDiffableDataSourceSnapshot needs a SectionIdentifierType and ItemIdentifierType, both are defined as a generic constraint, in this case, defined as “Section<Header, [Film]>” for the section identifier and “Film” for the item identifier.

e) NSDiffableDataSourceSnapshot has 2 methods, one appends each Section, and the other appends the items for each section.

f) Finally, we call apply() to apply the updates in the snapshot, that’s it!

Let’s now run the app…


Yeap, we just crashed, this is what you can see in the console…

Terminating app due to uncaught exception 'NSInternalInconsistencyException', 
reason: 'Invalid parameter not satisfying: self.supplementaryViewProvider || (self.supplementaryReuseIdentifierProvider && self.supplementaryViewConfigurationHandler)'

As you probably can deduce by this log, we need to provide a Supplementary view to be used on each section header, let’s do that now, I added a “TitleSupplementaryView” in the views folder to be used for our headers, now just copy this function…

func configureHeader() {        dataSource?.supplementaryViewProvider = { (            collectionView: UICollectionView,            kind: String,            indexPath: IndexPath) -> UICollectionReusableView? in
let header: TitleSupplementaryView = collectionView.dequeueSuplementaryView(of: UICollectionView.elementKindSectionHeader, at: indexPath) header.backgroundColor = .lightGray
if let section = self.currentSnapshot?.sectionIdentifiers[indexPath.section] { header.label.text = "\(section.headerItem.titleHeader)" } return header } }

Then call it on viewDidLoad like this…

override func viewDidLoad() {     super.viewDidLoad()     configureDataSource()     configureHeader()     fetchData() }

Now run the app…

Image for post
Image for post

There you have it, a dynamic list with dynamic sections, this is good so far but let me finish by showing you how to delete and insert items as well.

Step 6:

Deleting items, can not be easier, we will delete an item in the list by tapping on the specific cell, we will use a UICollectionViewDelegate method to do it…

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// a
guard let movie = self.dataSource?.itemIdentifier(for: indexPath) else { return }
// b
guard let snaphost = self.dataSource?.snapshot() else { return }
// c
dataSource?.apply(snaphost, animatingDifferences: true) collectionView.deselectItem(at: indexPath, animated: true) }

So what is going on here:

a) UICollectionViewDiffableDataSource has different handy methods to access items, check the docs!

b) We can use our “currentSnapshot” property or we can also get the current snapshot by calling this method.

c) NSDiffableDataSourceSnapshot has handy methods like this one that allows us to delete an array of items, here we pass just the one selected. documentation here 🤓

d) We apply the changes to the snapshot.

Image for post
Image for post

Pretty amazing right? we have a cool animation without implementing the horrible “performBatchUpdates” or even reloading the section! …However, we have one problem now, if you delete all the items in a section you can see that the header is not been removed, let’s fix that now, we need to check if the section is empty then remove it, we will create an extension to implement what we need…

extension NSDiffableDataSourceSnapshot {
func deleteItems(_ items: [ItemIdentifierType], at section: Int) {
// a Delete Items in section deleteItems(items) // b Get the section Identifier let sectionIdentifier = sectionIdentifiers[section] // c Check if its empty guard numberOfItems(inSection: sectionIdentifier) == 0 else { return } // d Delete Section deleteSections([sectionIdentifier]) }}

What is happening here is very straight forward, we remove items one by one and then when the section is empty we also have to remove it, let’s replace now the code in collection view did select method like this…

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {    guard let movie = self.dataSource?.itemIdentifier(for: indexPath) else { return }    guard let snaphost = self.dataSource?.snapshot() else { return }    snaphost.deleteItems([movie], at: indexPath.section)    dataSource?.apply(snaphost, animatingDifferences: true)    collectionView.deselectItem(at: indexPath, animated: true)    }

Step 7:

Inserting items, we reached the final step on this post, we will insert a new movie into the dataSource; I hooked an IBAction and inside of it I added the needed code to do so, is extremely simple…

@IBAction func insertItem() {        
let lastJedi = Film(title: "The Last Jedi", episodeId: nil, openingCrawl: nil, director: nil, producer: nil, releaseDate: nil, species: nil, starships: ["nil"], vehicles: nil, characters: nil, planets: nil, url: nil, created: nil, edited: nil)
guard let snapshot = dataSource?.snapshot() else { return }
dataSource?.apply(snapshot, animatingDifferences: true)

a) First, we need a new Film, it can be done so much better of course by showing some kind of text view to the user but for this example, this is good enough.

b) Get the current snapshot.

c) Append the items to the snapshot. (With this code it will append it in the last index of the last section)

d) Finally…this will look familiar 🤣… apply() the snapshot to the dataSource!

Run the app and tap insert, you will see a new item inserted with cool animation and one more time with just 3 lines of code! 🔥

This API is amazing for sure, it redefines everything we know about Data sources and its such a great help that will let us focus on more entertaining components on our apps.

Hope you find this helpful and please share any tip or error that you find using UICollectionViewDiffableDataSource and NSDiffableDataSourceSnapshot, enjoy!

You can clone the full project here.

Written by

Senior iOS Engineer #latinintech

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store