Nib-backed Collection Views
When working with collection views (and table views) in UIKit
, you have to implement a data source which feeds the collection view with cells. Since a collection view often has hundreds (or even thousands) of cells, but only a few are visible on screen at a time, UIKit
provides a mechanism for reusing cells as they scroll offscreen and new cells scroll into view. And although UIKit
provides such a mechanism, the API is somewhat lacking. And it certainly isn’t very swifty.
From the Apple documentation:
The collection view’s data source object provides both the content for items and the views used to present that content. When the collection view first loads its content, it asks its data source to provide a view for each visible item. To simplify the creation process for your code, the collection view requires that you always dequeue views, rather than create them explicitly in your code. There are two methods for dequeueing views. The one you use depends on which type of view has been requested:
- Use the
dequeueReusableCell(withReuseIdentifier:for:)
to get a cell for an item in the collection view.- Use the
dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:)
method to get a supplementary view requested by the layout object.
These APIs are so-called “stringly” typed and requires force casting to the correct subclass if you need to access properties of the newly dequeued/reused cell, other than those defined in UICollectionViewCell
.
We can do better! Enter Swift!
Protocols and default implementation using protocol extensions
Since the UIKit
APIs require strings for identifiers and nib names, we must implement a way of getting these from a view. We declare a protocol for nib-backed views:
protocol NibReusable: class where Self: UICollectionReusableView {
static var reuseIdentifier: String { get }
static var nibName: String { get }
}
Since we usually use the same name for the nib file and the class, we can supply default implementations for these class properties. The reuseIdentifier
can be whatever as long as it is unique, and we use the same identifier to register the cell as to retrieve it. Therefore, we can use the class name for this as well:
extension NibReusable {
static var nibName: String {
return String(describing: self)
}
static var reuseIdentifier: String {
return nibName
}
}
Extending UICollectionView with generic functions
Now, we can extend UICollectionView
with a new set of generic register
and dequeue
methods:
extension UICollectionView {
func register<Cell: NibReusable>(_ cell: Cell.Type) {
let nib = UINib(nibName: cell.nibName, bundle: Bundle(for: cell))
register(nib, forCellWithReuseIdentifier: cell.reuseIdentifier)
}
func dequeue<Cell: NibReusable>(_ cell: Cell.Type, for indexPath: IndexPath) -> Cell {
return dequeueReusableCell(withReuseIdentifier: cell.reuseIdentifier, for: indexPath) as! Cell
}
}
Putting it all to use
Now, simply declaring conformance to NibReusable
for a collection view cell class, will allow us to register and dequeue instances of it—as long as there is a nib file with the same name.
class MyCell: UICollectionViewCell, NibReusable {
// add your properties, function and overloaded behavior to the class
}
class ViewController: UIViewController, UICollectionViewDataSource {
@IBOutlet var collectionView: UICollectionView!
override func viewDidLoad() {
collectionView.register(MyCell.self)
super.viewDidLoad()
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell
let cell = collectionView.dequeue(MyCell.self, for: indexPath)
// cell is now of the correct type
return cell
}
}
Ah! Much swiftier!
Update: Adding support for supplementary views
Why not reuse the same register
and dequeue
functions with an optional kind
and add support for supplementary views as well?
extension UICollectionView {
func register<Cell: NibReusable>(_ cell: Cell.Type, ofKind kind: String? = nil) {
let nib = UINib(nibName: cell.nibName, bundle: Bundle(for: cell))
if let kind = kind {
register(nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: cell.reuseIdentifier)
} else {
register(nib, forCellWithReuseIdentifier: cell.reuseIdentifier)
}
}
func dequeue<Cell: NibReusable>(_ cell: Cell.Type, ofKind kind: String? = nil, for indexPath: IndexPath) -> Cell {
if let kind = kind {
return dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: cell.reuseIdentifier, for: indexPath) as! Cell
} else {
return dequeueReusableCell(withReuseIdentifier: cell.reuseIdentifier, for: indexPath) as! Cell
}
}
}
Leave a Comment