Skip to content

Latest commit

 

History

History
214 lines (153 loc) · 7.69 KB

File metadata and controls

214 lines (153 loc) · 7.69 KB

Diffable Data Source

UITableView

mountain demo

Here's what you need to know about UITableViewDiffableDataSource:

  • The item you stick in needs to be Hashable.
    struct Mountain: Hashable {
        
        let name: String
        let height: Int
        let identifier = UUID()
        
        func hash(into hasher: inout Hasher) {
            hasher.combine(identifier)
        }
        
        static func == (lhs: Mountain, rhs: Mountain) -> Bool {
            return lhs.identifier == rhs.identifier
        }
    }
  • You return the UITableViewCell as part of the initializer.
  • No more indexPath (through you can still translate between indexPaths and current cell you have if you need to).
  • It is thread safe
  • Just be consistent - always work on the main thread or a background thread but not both.
  • You still need a cell reuseIdentifier else you will get an error about a tableCell not being able to lay itself out.

How it works

Configure the Data Source

First you configure the UITableViewDiffableDataSource.

func configureDataSource() {
    dataSource = UITableViewDiffableDataSource<Section, MountainsController.Mountain>(tableView: tableView) {
        (tableView: UITableView, indexPath: IndexPath, item: MountainsController.Mountain) -> UITableViewCell?  in
        
        let cell = tableView.dequeueReusableCell(withIdentifier: self.reuseIdentifier, for: indexPath)
        cell.textLabel?.text = item.name
        return cell
    }
}

A data source takes a SectionIdentifierType, an ItemIdentifierType, a tableView, and a closure returning the UITableViewCell you would like the data source to use for each cell.

Here is the constructor:

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

    public typealias CellProvider = (UITableView, IndexPath, ItemIdentifierType) -> UITableViewCell?

    public init(tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider)

This is different from the UITableView that returns a cell as part of it's delegate. Here it's returned in the constructor.

Then you update

When you get a new updated collection of items to display, it's just four lines of code to update the tableView.

func performQuery(with filter: String?) {
    let mountains = mountainsController.filteredMountains(with: filter).sorted { $0.name < $1.name }

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

Common pattern in the WWDC videos

One patten the WWDC folks like to use when creating demso (in the Wifi example) is passing closures in as part of the initializer. Here is an example where the updateHandler is passed in, and then later executed withe WIFIController pass itself back to the ViewController that created it.

class WIFIController {
    typealias UpdateHandler = (WIFIController) -> Void
    private let updateHandler: UpdateHandler
    
    init(updateHandler: @escaping UpdateHandler) {
        self.updateHandler = updateHandler
    }
}

class WiFiSettingsViewController: UIViewController {
    
    var wifiController: WIFIController! = nil
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureDataSource()
    }
    
    func configureDataSource() {
        wifiController = WIFIController { [weak self] (controller: WIFIController) in
            guard let self = self else { return }
            self.updateUI()
        }
    }
    
    func updateUI() {
        print("Update")
    }
}

While strange looking at first, it is quite elegant. Basically you create Controllers for handling your diffable data. Only when you create them, you pass in an UpdateHandler which is an alias to a closure defined like this.

typealias UpdateHandler = (WIFIController) -> Void

which passes itself in as an input and returns nothing. What this effectively does is when the controller does an update, it executes this code passing itself in. This is how the WWDC callback when the WIFI settings change, thus allowing the _ WiFiSettingsViewController_ to update it's UI.

Full source

See repos for full source. Here is the ViewController.

//
//  MountainTableViewController.swift
//  DemoArcade
//
//  Created by Jonathan Rasmusson Work Pro on 2020-04-07.
//  Copyright © 2020 Rasmusson Software Consulting. All rights reserved.
//

import UIKit

class MountainTableViewController: UIViewController {

    enum Section {
        case main
    }
    
    let searchBar = UISearchBar()
    let tableView = UITableView()
    
    var dataSource: UITableViewDiffableDataSource<Section, MountainsController.Mountain>!
    var nameFilter: String?
    
    let reuseIdentifier = "reuse-identifier"
    
    let mountainsController = MountainsController()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Mountains Search"
        
        layout()
        configureDataSource()
        performQuery(with: nil)
    }
    
    func layout() {
        for viewable in [searchBar, tableView] {
            view.addSubview(viewable)
            viewable.translatesAutoresizingMaskIntoConstraints = false
        }
        
        searchBar.delegate = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier)
        
        searchBar.topAnchor.constraint(equalToSystemSpacingBelow: view.safeAreaLayoutGuide.topAnchor, multiplier: 3).isActive = true
        searchBar.leadingAnchor.constraint(equalToSystemSpacingAfter: view.leadingAnchor, multiplier: 1).isActive = true
        view.trailingAnchor.constraint(equalToSystemSpacingAfter: searchBar.trailingAnchor, multiplier: 1).isActive = true
        
        tableView.topAnchor.constraint(equalToSystemSpacingBelow: searchBar.bottomAnchor, multiplier: 0).isActive = true
        tableView.leadingAnchor.constraint(equalToSystemSpacingAfter: view.leadingAnchor, multiplier: 0).isActive = true
        view.trailingAnchor.constraint(equalToSystemSpacingAfter: tableView.trailingAnchor, multiplier: 0).isActive = true
        view.bottomAnchor.constraint(equalToSystemSpacingBelow: tableView.bottomAnchor, multiplier: 0).isActive = true
    }
    
    func configureDataSource() {
        dataSource = UITableViewDiffableDataSource<Section, MountainsController.Mountain>(tableView: tableView) {
            (tableView: UITableView, indexPath: IndexPath, item: MountainsController.Mountain) -> UITableViewCell?  in
            
            let cell = tableView.dequeueReusableCell(withIdentifier: self.reuseIdentifier, for: indexPath)
            cell.textLabel?.text = item.name
            return cell
        }
    }
        
    func performQuery(with filter: String?) {
        let mountains = mountainsController.filteredMountains(with: filter).sorted { $0.name < $1.name }

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

extension MountainTableViewController: UISearchBarDelegate {
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        performQuery(with: searchText)
    }
}

Links that help