Xcoding with Alfian

Software Development Videos & Tutorials

Using Diffable Data Source iOS 13 API in UITableView

Alt text

Since the beginning of iOS SDK, UITableViewDataSource is the protocol that had the responsibility to drive the data and provide the cells in TableView. As good developers, we need to implement the protocol methods and making sure to sync our backing model with the data source properly to avoid any crashes because of inconsistencies between them.

optional func numberOfSections(in tableView: UITableView) -> Int
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

Also, when we need to perform granular updates for sections and rows in our TableView, we need to use the API like the sample code below.

tableView.beginUpdates()
// Delete section 3 and 4
tableView.reloadSections([3,4], with: .automatic)
// Insert at section 1 and row 0
tableView.insertRows(at: [IndexPath(row: 0, section: 1)], with: .automatic)
// delete at section 1 and row 1
tableView.deleteRows(at: [IndexPath(row: 1, section: 1)], with: .automatic)
tableView.endUpdates()

It's actually pretty hard to make sure all the section and rows are correctly updated for each insert, reload, and deletion based on the value between the old and new data. This is the error that UIKit threw when we incorrectly update the TableView.

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (10) must be equal to the number of sections contained in the table view before the update (10), plus or minus the number of sections inserted or deleted (0 inserted, 1 deleted).'
***

Just in time for WWDC 2019, Apple introduced a new API for managing data source for UITableView and UICollectionView in a much more simpler and safer way for developers to use without building their own diffing solution. It is called Diffable Data Source.

Diffable Data Source API helps us to manage data sources both in TableView and CollectionView by using snapshot. Snapshot acts as a source of truth between our view and data source, whenever there are changes in our model, we just need to construct a new snapshot and applies it to the current snapshot. All the diffing, view update with animation will be performed automagically for us. Also, we don't need to deal with indexPath when we dequeue our cell, the exact model will be given to us to render in the cell based on generic API.

You can read more about it by watching the Apple WWDC 2019 session Advances in Data Sources by Apple engineers. Advances in UI Data Sources.

Diffable Data Source Diffing Mechanism

There is a mandatory requirement that developer must follow to make sure the diffing works correctly. We need to provide sections and item type representation that can provide unique value. Both of them need to implement to Hashable protocol as we can see in the declaration of UITableViewDiffableDataSource class below.

@available(iOS 13.0, tvOS 13.0, *)
open class UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject, UITableViewDataSource where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable 

As we can see the SectionIdentifierType and ItemIdentifierType are generic that we must provide when we declare and initialise the class. We also need to make sure the hashValue are unique for each instance to avoid hash collision.

Beginning our sample project

Let's start working on a simple project to display data in UITableView using DiffableDataSource API!. There are several features that we want to build such as:

  1. Using Diffable Data Source API to load places in many cities from local JSON Stub file.
  2. Cities will be the sections and places will be the rows of the TableView.
  3. Navigation button that will shuffle the cities and places, then apply the new snapshot to the diffable data source with animation.


You can start by cloning the project from the GitHub Repository at StarterTableViewDiffableDataSource.

The Starter project

The starter project already provides the resources such as cells, image assets, and JSON stubs file for the cities and places. There are 3 JSON files representing city such as Osaka, Kyoto, and Tokyo. Here are the essential components from the starter project:

  1. CityPlaceViewController. Our main ViewController class, it is a subclass of UITableViewController. With this, we already have built in TableView without additional setup.
  2. Bundle+Extension. A simple extension of Bundle class that provide static method to load a decodable model from the main bundle resource given a filename of the JSON file.
  3. PlaceTableViewCell. A TableViewCell that we will use to display the place. It has a thumbnail image view, along with title and description of the place.


Let's begin building our app by creating the model next!

Create Model for the Section Identifier

Our section will be represented by the City enum. In this case we have 3 cities: Osaka, Kyoto, and Tokyo. The enum underlying raw type will be String. This enum will automatically conforms to Hashable protocol using the rawValue as the unique hashValue.

Create a new file called City.swift inside the Models folder. Fill the file using the code below to create the enum for the City.

enum City {
    case kyoto
    case osaka
    case tokyo
}

Create Model for the Row Identifier

For the row representation, we will create struct called Place. The struct has several underlying properties such as name, description, imageName. We also have the UUID as the properties to make sure each of the place instance is unique.

Create a new file called Place.swift inside the Models folder. Fill the file using the code below to create the struct for the Place.

struct Place: Decodable {
    let uuid: String
    let name: String
    let description: String
    let imageName: String
}

Next, we need to make it conform the Hashable protocol. Below the code, create an extension for the Place that implements Hashable. Then, we need to provide the hasher value that will be used to create the hashValue. In this case we combine all the properties of the Place into the hasher.

extension Place: Hashable {
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(uuid)
        hasher.combine(name)
        hasher.combine(description)
        hasher.combine(imageName)
    }
}

That's it our model for section and row are now unique and ready to be used for the snapshot diffing 😋.

Loading Stub Data for Places in Cities

To display data in the TableView we will be loading stub data from JSON file and decode it into Place instances. The starter project already provide a helper extension method for the bundle to load the data from a JSON file and decode it using generic method with Decodable type as the placeholder. You can take a peek at how it is implemented in the Extension+Bundle.swift file.

Inside the Place.swift source code, create an extension for Array with generic constraint for the element equal of Place. With the constraint, the extension will only be applied to array of Place. In the extension, we just create 3 static computed property that returns array of place for Osaka, Kyoto, and Tokyo using our extension bundle static method.

extension Array where Element == Place {
    
    static var osakaStubs: Self {
        try! Bundle.decodeJSONFromMainResources(filename: "osaka")
    }
    
    static var kyotoStubs: Self {
        try! Bundle.decodeJSONFromMainResources(filename: "kyoto")
    }

    static var tokyoStubs: Self {
        try! Bundle.decodeJSONFromMainResources(filename: "tokyo")
    }
}

Next, we will associate the stub places for each of the City enum. Go to City.swift source code and create an extension for the City. We declare a static computed property with array of tuple containing the city associated with array of places.

extension City {
    
    static var stubCitiesWithPlaces: [(city: City, places: [Place])] {
        [
            (.osaka, .osakaStubs),
            (.kyoto, .kyotoStubs),
            (.tokyo, .tokyoStubs)
        ]
    }
}

That's it! We now have our model with the stub data as well, let's move on to our ViewController to build our TableView using Diffable Data Source.

Using the TableViewDiffableDataSource in ViewController

Next, we will move on to the CityPlaceViewController source file. As a reminder, we are not going to use any of UITableViewDataSource protocol methods to drive the TableView. Instead, we are going to use the UITableViewDiffableDataSource class.

Subclassing UITableViewDiffableDataSource

We will be subclassing the UITableViewDiffableDataSource for this app because we need to override one of the method to provide city name as the header for our section. If your app don't have any section header, you don't need to subclass it. Declare the file at the top of the source code and fill the class like so.

class CityPlaceTableViewDiffableDataSource: UITableViewDiffableDataSource<City, Place> {
    
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return snßapshot().sectionIdentifiers[section].rawValue.uppercased()
    }
}

In this case we just need to override one method, tableView(_:titleForHeaderInSection:). In the implementation, we can just access the snapshot property section identifiers array and passing the section index to retrieve the associated City. Then, we can access the rawValue and capitalized the string to be displayed in the header. We also fill the generic placeholder in the class declaration to make the City as the section and Place as the Item/Row.

Next, Let's move on to the CityPlaceViewController class!

Setting up the TableViewDiffableDataSource in TableView

Let's begin by declaring the instance properties that we will be using, there are 2 instance properties:

  1. diffableDataSource. The type of this will be the CityPlaceTableViewDiffableDataSource.
  2. citiesWithPlaces. This will be our stub data, the array containing tuple of a city associated with places. City will be the section, and the places will be the rows inside the section.


Also, we need to add a method in the PlaceTableViewCell to setup the labels and image view given a place to render like so.

func setup(with place: Place) {
    titleLabel.text = place.name
    subtitleLabel.text = place.description
    placeImageView.image = UIImage(named: place.imageName)
}

Next, we will create setupTableView method in CityPlaceViewController. There are several tasks that we will perform inside this method, such as:

  1. Registering PlaceTableViewCell nib to our TableView with reuse identifier.
  2. Initializing CityPlaceTableViewDiffableDataSource. The initializer accepts the TableView and a closure. The closure parameter are the TableView, IndexPath, and the associated Place, this closure will be invoked when the TableView needs to dequeue the cell.


We don't need to retrieve the place manually using the IndexPath anymore as it is already passed in the parameter. In this case we just need to dequeue the cell from the TableView using the reuse identifier and setup the cell with the place.

private func setupTableView() {
    tableView.register(UINib(nibName: "PlaceTableViewCell", bundle: nil), forCellReuseIdentifier: "Cell")
    diffableDataSource = CityPlaceTableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, place) -> UITableViewCell? in
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! PlaceTableViewCell
            cell.setup(with: place)
            return cell
    }
}

Setup and Applying Snapshot

For building and applying snapshot to diffable data source, we will create a method called buildAndApplySnapshot. Here are things that we will perform in this method:

  1. Initialize a new snapshot using the NSDiffableDataSourceSnapshot class using the City and Place as the generic placeholder to represent the section and item.
  2. Looping the citiesWithPlaces instance property array, for each element we are appending the section/city to the snapshot and also the associated items/places to the section/city
  3. Last, we just invoke the diffableDataSource apply method passing the new snapshot and set the diffing animation to true.


Next, we need to add the invocation of the methods in the viewDidLoad like so.

  override func viewDidLoad() {
    super.viewDidLoad()
        
    setupNavigationItems()
    setupTableView()
    buildAndApplySnapshot()
}

Try to build and run the app, voila we have successfully display cities and places using the Diffable Data Source in the TableView!

Implement the shuffle method

Finally, we are going to add the shuffle mechanism, that will shuffle our citiesWithPlaces property. In the shuffleTapped method, we will shuffle the cities and places in random position. At last, we just invoke the buildAndApplySnapshot to build new snapshot and apply it to the current snapshot with diffing animation.

@objc func shuffleTapped(_ sender: Any) {
    self.citiesWithPlaces = citiesWithPlaces.map {
        ($0.city, $0.places.shuffled())
    }.shuffled()
       
    buildAndApplySnapshot()
}

Conclusion

That's it folks! Pat yourself on the shoulder as you all have successfully build TableView using the new Diffable Data Source API. The API it's pretty simple and easy to use as well as providing safety for us by providing single source of truth for the data source in our app.

As SwiftUI is still in infancy, we can still use many UIKit class to build our app. Remember that diffable data source is also available for CollectionView and by using it with new Compositional Layout, we can create complex user interface easier and faster.

Let's keep the lifelong learning goes on and happy Swifting!!!

You can also download the completed project from the GitHub repository at CompletedTableViewDiffableDataSource.