Diffable Data source for UITableView and UICollectionView iOS 17.05.2021

Diffable Data Sources were introduced at WWDC 2019 and are available since iOS 13. They’re a replacement of the good old UICollectionViewDataSource and UITableViewDataSource protocols and make it easier to migrate changes in your data views.

These new diffable data source classes allow us to define data sources for collection- and table views in terms of snapshots that represent the current state of the underlying models. The diffable data source will then compare the new snapshot to the old snapshot and it will automatically apply any insertions, deletions, and reordering of its contents.

Diffable data source for TableView

In the first example we will see how to set a diffable data source for UITableView.

I use SnapKit to setup auto-layout constraints.

To get started, let's start creating a model object of named Movie which will act as a data for our table view.

struct Movie: Hashable {
    let id = UUID()
    let title: String

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

    static func ==(lhs: Movie, rhs: Movie) -> Bool {
        return lhs.id == rhs.id
    }

    static func items() -> [Movie] {
        var movies: [Movie] = []
        for i in 1...7 {
            movies.append(Movie(title: "Movie \(i)"))
        }
        return movies
    }
}

The elements must be hashed so that the Datasource can distinguish the elements from each other and find out which objects are already in the list.

Next, in your view controller start with declaration of three properties

  • dataSource, which will act as a provider of data for our tableView
  • array of movies containing objects of type Moview
  • a struct called Section which will house related sections.

For now, we can assume that we just have one section named main. In practice though you may have more than just one section

var dataSource: UITableViewDiffableDataSource<Section, Movie>!
var items: [Movie] = []
enum Section: CaseIterable {
    case main
}

The data source is generic and defines a type for section identifiers and a type for the data that is listed. The cell provider will take this generic as output for the third argument containing the data for which a cell needs to be returned.

Data is provided through so-called snapshots: a snapshot of data. Snapshots of data are compared with each other to determine the minimum amount of changes needed to go from one snapshot to another.

Next step, we will initialize these properties in viewDidLoad method to make they are usable in later stages.

import UIKit
import SnapKit

struct Movie: Hashable {
    let id = UUID()
    let title: String

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

    static func ==(lhs: Movie, rhs: Movie) -> Bool {
        return lhs.id == rhs.id
    }

    static func items() -> [Movie] {
        var movies: [Movie] = []
        for i in 1...7 {
            movies.append(Movie(title: "Movie \(i)"))
        }
        return movies
    }
}

class ViewController: UIViewController, UITableViewDelegate {
    var items: [Movie] = []
    var reuseIdentifier = "cellId"

    enum Section: CaseIterable {
        case main
    }

    lazy var dataSource: UITableViewDiffableDataSource<Section, Movie> = {
        let dataSource = UITableViewDiffableDataSource <Section, Movie>(tableView: tableView) {
            (tableView: UITableView, indexPath: IndexPath, movie: Movie) -> UITableViewCell? in
            let cell = tableView.dequeueReusableCell(withIdentifier: self.reuseIdentifier, for: indexPath)
            cell.textLabel?.text = movie.title
            return cell
        }
        dataSource.defaultRowAnimation = .fade
        return dataSource
    }()

    lazy var tableView: UITableView = {
        let v = UITableView()
        v.delegate = self
        v.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier)
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        setupView()
        loadData()
        setupTimer()
    }

    func setupView() {
        view.addSubview(tableView)
        tableView.snp.makeConstraints { (make) in
            make.edges.equalToSuperview()
        }
    }

    func loadData() {
        if items.isEmpty {
            items = Movie.items()
        } else {
            let number = Int.random(in: 0...100)
            let indx = Int.random(in: 0..<items.count)
            items.insert(Movie(title: "Movie \(number)"), at: indx)
        }

        var snapshot = NSDiffableDataSourceSnapshot<Section, Movie>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items, toSection: .main)
        dataSource.apply(snapshot, animatingDifferences: true)
    }

    func setupTimer() {
        Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { timer in
            self.loadData()
        }
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if let movie = dataSource.itemIdentifier(for: indexPath) {
            print("Selected movie \(movie.title)")
        }
    }
}

Diffable data source for CollectionView

import UIKit
import SnapKit

struct Movie: Hashable {
    let id = UUID()
    let title: String

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

    static func ==(lhs: Movie, rhs: Movie) -> Bool {
        return lhs.id == rhs.id
    }

    static func items() -> [Movie] {
        var movies: [Movie] = []
        for i in 1...7 {
            movies.append(Movie(title: "Movie \(i)"))
        }
        return movies
    }
}

class MovieCell: UICollectionViewCell {
    let titleLabel: UILabel = {
        let v = UILabel()
        v.font = UIFont.systemFont(ofSize: 14)
        v.textColor = .darkGray
        return v
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setupView(){
        backgroundColor = .white
        addSubview(titleLabel)
        titleLabel.snp.makeConstraints { (make) in
            make.edges.equalToSuperview()
        }
    }
}

class ViewController: UIViewController, UICollectionViewDelegate {
    var items: [Movie] = []
    var reuseIdentifier = "cellId"

    enum Section: CaseIterable {
        case main
    }

    lazy var dataSource: UICollectionViewDiffableDataSource<Section, Movie> = {
        let dataSource = UICollectionViewDiffableDataSource <Section, Movie>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, movie: Movie) -> UICollectionViewCell? in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier, for: indexPath) as! MovieCell
            cell.titleLabel.text = movie.title
            return cell
        }
        return dataSource
    }()

    lazy var collectionView: UICollectionView = {
        let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
        layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
        layout.itemSize = CGSize(width: 60, height: 60)

        let v = UICollectionView(frame: .zero, collectionViewLayout: layout)
        v.delegate = self
        v.backgroundColor = .white
        v.register(MovieCell.self, forCellWithReuseIdentifier: reuseIdentifier)
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        setupView()
        loadData()
        setupTimer()
    }

    func setupView() {
        view.addSubview(collectionView)
        collectionView.snp.makeConstraints { (make) in
            make.edges.equalToSuperview()
        }
    }

    func loadData() {
        if items.isEmpty {
            items = Movie.items()
        } else {
            let number = Int.random(in: 0...100)
            let indx = Int.random(in: 0..<items.count)
            items.insert(Movie(title: "Movie \(number)"), at: indx)
        }

        var snapshot = NSDiffableDataSourceSnapshot<Section, Movie>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items, toSection: .main)
        dataSource.apply(snapshot, animatingDifferences: true)
    }

    func setupTimer() {
        Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { timer in
            self.loadData()
        }
    }

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        if let movie = dataSource.itemIdentifier(for: indexPath) {
            print("Selected movie \(movie.title)")
        }
    }
}