Type-safe value-oriented collection view data source

October 15, 2014 ยท View on GitHub

// // 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.
}

}