Nib-backed Collection Views

5 minute read

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

Tags:

Updated:

Leave a Comment