Forked from emptyfuel/DiffableDataSourceCollectionView.swift
Created
October 16, 2023 12:24
-
-
Save suhitp/db46df308822c3feb52ebe73baed923f 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/02/27/registering-collection-view-cells-in-ios-14/
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 DiffableCollectionViewController : UIViewController { | |
var collectionView: UICollectionView! | |
enum Section: String, CaseIterable, Hashable { | |
case apples = "Apples" | |
case oranges = "Oranges" | |
case empty = "Bad Weather Today!" | |
} | |
private lazy var dataSource: UICollectionViewDiffableDataSource<Section, AnyHashable> = makeDataSource() | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
collectionView = UICollectionView(frame: view.frame, collectionViewLayout: layout()) | |
collectionView.backgroundColor = .white | |
self.view.addSubview(collectionView) | |
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true | |
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true | |
collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true | |
collectionView.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.updateSnapshot(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.updateSnapshot(apples: [], oranges: []) | |
} | |
} | |
} | |
/// Update the data source snapshot | |
/// - Parameters: | |
/// - apples: Apples if any | |
/// - oranges: Oranges if any | |
private func updateSnapshot(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() -> UICollectionViewDiffableDataSource<Section, AnyHashable> { | |
let dataSource = UICollectionViewDiffableDataSource<Section, AnyHashable>(collectionView: collectionView) { collectionView, indexPath, item in | |
if let apple = item as? Apple { | |
//Apple | |
return collectionView.dequeueConfiguredReusableCell(using: self.appleCell(), for: indexPath, item: apple) | |
} else if let orange = item as? Orange { | |
//Orange | |
return collectionView.dequeueConfiguredReusableCell(using: self.orangeCell(), for: indexPath, item: orange) | |
} else if let emptyData = item as? EmptyData { | |
//Empty | |
return collectionView.dequeueConfiguredReusableCell(using: self.emptyCell(), for: indexPath, item: emptyData) | |
} else { | |
fatalError("Unknown item type") | |
} | |
} | |
dataSource.supplementaryViewProvider = { (view, kind, indexPath) in | |
print("\(view), \(kind), \(indexPath)") | |
return self.collectionView.dequeueConfiguredReusableSupplementary(using: self.configuredHeader(), for: indexPath) | |
} | |
return dataSource | |
} | |
//MARK: - Cell configurations and layout | |
/// Get an appropriate layout | |
/// - Returns: Compositional layout for our simple example | |
private func layout() -> UICollectionViewLayout { | |
// Item | |
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) | |
let item = NSCollectionLayoutItem(layoutSize: itemSize) | |
item.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10) | |
// Group | |
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(100)) | |
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) | |
// Section | |
let section = NSCollectionLayoutSection(group: group) | |
section.boundarySupplementaryItems = [headerLayout()] | |
return UICollectionViewCompositionalLayout(section: section) | |
} | |
private func headerLayout() -> NSCollectionLayoutBoundarySupplementaryItem { | |
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), | |
heightDimension: .estimated(44)) | |
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: "section-header", alignment: .top) | |
return sectionHeader | |
} | |
private func configuredHeader() -> UICollectionView.SupplementaryRegistration<HeaderView> { | |
return UICollectionView.SupplementaryRegistration<HeaderView>(elementKind: "section-header") { (supplementaryView, title, indexPath) in | |
let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section] | |
supplementaryView.titleLabel.text = section.rawValue | |
} | |
} | |
/// Configured apple cell | |
/// - Returns: Cell configuration | |
private func appleCell() -> UICollectionView.CellRegistration<UICollectionViewCell, Apple> { | |
return UICollectionView.CellRegistration<UICollectionViewCell, Apple> { (cell, indexPath, item) in | |
cell.configure(label: "\(item.name), core thickness: \(item.coreThickness)mm", relatedColor: .systemGreen) | |
} | |
} | |
/// Configured orange cell | |
/// - Returns: Cell configuration | |
private func orangeCell() -> UICollectionView.CellRegistration<UICollectionViewCell, Orange> { | |
return UICollectionView.CellRegistration<UICollectionViewCell, Orange> { (cell, indexPath, item) in | |
cell.configure(label: "\(item.name), peel thickness: \(item.peelThickness)mm", relatedColor: .systemOrange) | |
} | |
} | |
/// Configured empty data cell | |
/// - Returns: Cell configuration | |
private func emptyCell() -> UICollectionView.CellRegistration<UICollectionViewCell, EmptyData> { | |
return UICollectionView.CellRegistration<UICollectionViewCell, EmptyData> { (cell, indexPath, item) in | |
cell.configure(label: item.emptyMessage, relatedColor: .systemRed) | |
} | |
} | |
} | |
extension UICollectionViewCell { | |
/// Just set up a simple cell with text in the middle | |
/// - Parameter label: Label | |
/// - Parameter relatedColor: Color associated with the data | |
func configure(label: String, relatedColor: UIColor) { | |
//Content | |
var content = UIListContentConfiguration.cell() | |
content.text = label | |
content.textProperties.color = .white | |
content.textProperties.font = UIFont.preferredFont(forTextStyle: .body) | |
content.textProperties.alignment = .center | |
contentConfiguration = content | |
//Background | |
var background = UIBackgroundConfiguration.listPlainCell() | |
background.cornerRadius = 8 | |
background.backgroundColor = relatedColor | |
backgroundConfiguration = background | |
} | |
} | |
class HeaderView: UICollectionReusableView { | |
var titleLabel: UILabel! | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
configure() | |
} | |
required init?(coder: NSCoder) { | |
fatalError() | |
} | |
func configure() { | |
titleLabel = UILabel(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)) | |
titleLabel.translatesAutoresizingMaskIntoConstraints = false | |
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline) | |
titleLabel.textColor = .label | |
titleLabel.textAlignment = .center | |
addSubview(titleLabel) | |
let inset: CGFloat = 10 | |
NSLayoutConstraint.activate([ | |
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: inset), | |
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -inset), | |
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), | |
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor) | |
]) | |
} | |
} | |
/// 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 = DiffableCollectionViewController() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment