Skip to content

Instantly share code, notes, and snippets.

@emptyfuel
Created February 27, 2021 16:18
Show Gist options
  • Save emptyfuel/dcb234c4b16d1b64f3608c2c9145f079 to your computer and use it in GitHub Desktop.
Save emptyfuel/dcb234c4b16d1b64f3608c2c9145f079 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/
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