Skip to content

Instantly share code, notes, and snippets.

@emptyfuel
Last active October 10, 2022 07:05
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emptyfuel/77d1154477789f160379d605678e62e6 to your computer and use it in GitHub Desktop.
Save emptyfuel/77d1154477789f160379d605678e62e6 to your computer and use it in GitHub Desktop.
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