Source code to use with Xcode playground related to the blog post on emptytheory.com at https://emptytheory.com/2021/01/11/using-ios-diffable-data-sources-with-different-object-types/
import UIKit | |
import PlaygroundSupport | |
/// Simple sample diffable table view to demonstrate using diffable data sources. Approximately 33% of the time, it should show "bad weather" UI instead of apples and oranges | |
final class DiffableTableViewController : UIViewController { | |
var tableView: UITableView! | |
enum Section: String, CaseIterable, Hashable { | |
case apples = "Apples" | |
case oranges = "Oranges" | |
case empty = "Bad Weather Today!" | |
} | |
private lazy var dataSource: DiffableViewDataSource = makeDataSource() | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
//Set up the table view | |
tableView = UITableView(frame: view.frame, style: .plain) | |
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "AppleCell") | |
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "OrangeCell") | |
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "EmptyDataCell") | |
tableView.dataSource = dataSource | |
self.view.addSubview(tableView) | |
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true | |
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true | |
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true | |
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true | |
view.setNeedsUpdateConstraints() | |
// Just a silly method to pretend we're getting empty data every 3rd or so call (for demo purposes every 3 days or so we get rain at the fruit stand) | |
Int.random(in: 0..<3) > 0 ? getData() : getEmptyData() | |
} | |
/// Update the table with some "real" data (1 apple and 1 orange for now) | |
private func getData() { | |
DispatchQueue.global().async { | |
//Pretend we're getting some data asynchronously | |
let apples = [Apple(name: "Granny Smith", coreThickness: 12)] | |
let oranges = [Orange(name: "Navel", peelThickness: 3)] | |
DispatchQueue.main.async { | |
//Have data | |
self.updateTable(apples: apples, oranges: oranges) | |
} | |
} | |
} | |
/// Update the table with empty data | |
private func getEmptyData() { | |
DispatchQueue.global().async { | |
//Pretend we're getting some data asynchronously and it fails | |
DispatchQueue.main.async { | |
//Have data | |
self.updateTable(apples: [], oranges: []) | |
} | |
} | |
} | |
/// Update the data source snapshot | |
/// - Parameters: | |
/// - apples: Apples if any | |
/// - oranges: Oranges if any | |
private func updateTable(apples: [Apple], oranges: [Orange]) { | |
// Create a new snapshot on each load. Normally you might pull | |
// the existing snapshot and update it. | |
var snapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>() | |
defer { | |
dataSource.apply(snapshot) | |
} | |
// If we have no data, just show the empty view | |
guard !apples.isEmpty || !oranges.isEmpty else { | |
snapshot.appendSections([.empty]) | |
snapshot.appendItems([EmptyData()], toSection: .empty) | |
return | |
} | |
// We have either apples or oranges, so update the snapshot with those | |
snapshot.appendSections([.apples, .oranges]) | |
snapshot.appendItems(apples, toSection: .apples) | |
snapshot.appendItems(oranges, toSection: .oranges) | |
} | |
/// Create our diffable data source | |
/// - Returns: Diffable data source | |
private func makeDataSource() -> DiffableViewDataSource { | |
return DiffableViewDataSource(tableView: tableView) { tableView, indexPath, item in | |
if let apple = item as? Apple { | |
//Apple | |
let cell = tableView.dequeueReusableCell(withIdentifier: "AppleCell", for: indexPath) | |
cell.textLabel?.text = "\(apple.name), core thickness: \(apple.coreThickness)mm" | |
return cell | |
} else if let orange = item as? Orange { | |
//Orange | |
let cell = tableView.dequeueReusableCell(withIdentifier: "OrangeCell", for: indexPath) | |
cell.textLabel?.text = "\(orange.name), peel thickness: \(orange.peelThickness)mm" | |
return cell | |
} else if let emptyData = item as? EmptyData { | |
//Empty | |
let cell = tableView.dequeueReusableCell(withIdentifier: "EmptyDataCell", for: indexPath) | |
cell.textLabel?.text = emptyData.emptyMessage | |
return cell | |
} else { | |
fatalError("Unknown cell type") | |
} | |
} | |
} | |
/// Subclass to help set up sections, etc. | |
class DiffableViewDataSource: UITableViewDiffableDataSource<Section, AnyHashable> { | |
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { | |
//Use the snapshot to evaluate the section title | |
return snapshot().sectionIdentifiers[section].rawValue | |
} | |
} | |
} | |
/// Data to show if we have nothing returned from whatever API we use | |
struct EmptyData: Hashable { | |
let emptyMessage = "We're sorry! The fruit stand is closed due to inclement weather!" | |
let emptyImage = "cloud.bold.rain.fill" | |
} | |
/// One type of data | |
struct Apple: Hashable { | |
var name: String | |
var coreThickness: Int | |
} | |
/// Another type of data | |
struct Orange: Hashable { | |
var name: String | |
var peelThickness: Int | |
} | |
/// This will make debugging playground issues simpler | |
NSSetUncaughtExceptionHandler { exception in | |
print("Exception thrown: \(exception)") | |
} | |
// Present the view controller in the Live View window | |
PlaygroundPage.current.liveView = DiffableTableViewController() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment