Last active
October 10, 2022 07:05
-
-
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/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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