Last active
February 12, 2021 09:44
-
-
Save andymatuschak/f1e1691fa1a327468f8e to your computer and use it in GitHub Desktop.
Type-safe value-oriented collection view data source
This file contains 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
// | |
// CollectionViewDataSource.swift | |
// Khan Academy | |
// | |
// Created by Andy Matuschak on 10/14/14. | |
// Copyright (c) 2014 Khan Academy. All rights reserved. | |
// | |
import UIKit | |
/// A type which can produce and configure a cell for a given item. | |
public protocol CollectionViewCellFactoryType { | |
typealias Item | |
typealias Cell: UICollectionViewCell | |
func cellForItem(item: Item, inCollectionView collectionView: UICollectionView, atIndexPath indexPath: NSIndexPath) -> Cell | |
} | |
/// A concrete cell factory which makes use of UICollectionView's built-in cell reuse queue. | |
public struct RegisteredCollectionViewCellFactory<Cell: UICollectionViewCell, Item>: CollectionViewCellFactoryType { | |
private let reuseIdentifier: String | |
private let cellConfigurator: (Cell, Item) -> () | |
/// You must register Cell.Type with your collection view for `reuseIdentifier`. | |
public init(reuseIdentifier: String, cellConfigurator: (Cell, Item) -> ()) { | |
self.reuseIdentifier = reuseIdentifier | |
self.cellConfigurator = cellConfigurator | |
} | |
public func cellForItem(item: Item, inCollectionView collectionView: UICollectionView, atIndexPath indexPath: NSIndexPath) -> Cell { | |
// Will abort if you haven't already registered this reuse identifier for Cell.Type. | |
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as Cell | |
cellConfigurator(cell, item) | |
return cell | |
} | |
} | |
/// A type-safe collection view data source. Clients can specify its sections and their contents via any CollectionType. Configuration of the cells is delegated to an external factory type. | |
/// Use `bridgedDataSource` to get a UICollectionViewDataSource instance. | |
public class CollectionViewDataSource< | |
SectionCollection: CollectionType, | |
Factory: CollectionViewCellFactoryType, | |
Item | |
where | |
SectionCollection.Index == Int, | |
SectionCollection.Generator.Element: CollectionType, | |
SectionCollection.Generator.Element.Generator.Element == Item, | |
SectionCollection.Generator.Element.Index == Int, | |
Factory.Item == Item | |
> { | |
/// Clients are responsible for inserting/removing items in the collection view itself. | |
public var sections: SectionCollection | |
/// Returns an adapter for this data source that its bridgeable to Objective-C. | |
public var bridgedDataSource: UICollectionViewDataSource { return bridgedCollectionViewDataSource } | |
private let cellFactory: Factory | |
private let bridgedCollectionViewDataSource: BridgeableCollectionViewDataSource! // The bridge is initialized with a reference to self, so Swift thinks (correctly) that this could conceivably be used uninitialized. | |
public init(sections: SectionCollection, cellFactory: Factory) { | |
self.sections = sections | |
self.cellFactory = cellFactory | |
bridgedCollectionViewDataSource = BridgeableCollectionViewDataSource( | |
numberOfItemsInSectionHandler: { [weak self] in self?.numberOfItemsInSection($0) ?? 0 }, | |
cellForItemAtIndexPathHandler: { [weak self] in self?.collectionView($0, cellForItemAtIndexPath: $1) }, | |
numberOfSectionsHandler: { [weak self] in self?.numberOfSections() ?? 0 } | |
) | |
} | |
private func numberOfItemsInSection(section: Int) -> Int { | |
// This collection's index is Int, which is a RandomAccessIndexType, so this is O(1). | |
return countElements(sections[section]) | |
} | |
private func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { | |
return cellFactory.cellForItem(sections[indexPath.section][indexPath.row], inCollectionView: collectionView, atIndexPath: indexPath) | |
} | |
private func numberOfSections() -> Int { | |
// This collection's index is Int, which is a RandomAccessIndexType, so this is O(1). | |
return countElements(sections) | |
} | |
} | |
/// This separate type is necessary because CollectionViewDataSource itself is necessarily generic, so it can't be bridged to Objective-C. | |
@objc private class BridgeableCollectionViewDataSource: NSObject, UICollectionViewDataSource { | |
private let numberOfItemsInSectionHandler: Int -> Int | |
private let cellForItemAtIndexPathHandler: (UICollectionView, NSIndexPath) -> UICollectionViewCell? | |
private let numberOfSectionsHandler: () -> Int | |
init(numberOfItemsInSectionHandler: Int -> Int, cellForItemAtIndexPathHandler: (UICollectionView, NSIndexPath) -> UICollectionViewCell?, numberOfSectionsHandler: () -> Int) { | |
self.numberOfItemsInSectionHandler = numberOfItemsInSectionHandler | |
self.cellForItemAtIndexPathHandler = cellForItemAtIndexPathHandler | |
self.numberOfSectionsHandler = numberOfSectionsHandler | |
} | |
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { | |
return numberOfSectionsHandler() | |
} | |
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { | |
return numberOfItemsInSectionHandler(section) | |
} | |
// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath: | |
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { | |
return cellForItemAtIndexPathHandler(collectionView, indexPath)! // Better not have the data source bridge outlive the data source itself. | |
} | |
} |
This file contains 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
// ... | |
let cellFactory = RegisteredCollectionViewCellFactory(reuseIdentifier: "Topic") { (cell: TopicCatalogCell, topic: Topic) in | |
cell.topic = topic | |
} | |
dataSource = CollectionViewDataSource(sections: [topics], cellFactory: cellFactory) | |
topicCollectionView = UICollectionView(frame: CGRect(), collectionViewLayout: topicCollectionViewLayout) | |
topicCollectionView.dataSource = dataSource.bridgedDataSource |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment