Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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