chourobin
10/15/2014 - 2:44 AM

Type-safe value-oriented collection view data source

Type-safe value-oriented collection view data source

// ...

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