Implement reuse with Swift protocol extensions and generics

November 11, 2016 (8y ago)

As a developer, the most commonly used task is to implement customization of UITableView or UICollectionView by subclassing custom cells. And UITableView and UICollectionView have very similar APIs for registering custom cell subclasses in this area.

public func registerClass(cellClass: AnyClass?, forCellWithReuseIdentifier identifier: String)
public func registerNib(nib: UINib?, forCellWithReuseIdentifier identifier: String)

The most commonly used solution for registering a custom cell is to declare a constant reuseIdentifier, like the following.:

private let reuseIdentifier = "BookCell"

class BookListViewController: UIViewController, UICollectionViewDataSource {

    @IBOutlet private weak var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let nib = UINib(nibName: "BookCell", bundle: nil)
        self.collectionView.registerNib(nib, forCellWithReuseIdentifier: reuseIdentifier)
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath)
    
        if let bookCell = cell as? BookCell {
            // TODO: configure cell
        }
    
        return cell
    }
}

Next, let's try to use generics to simplify and make it safer.

First of all, it would be great if we don't need to declare the reuse identifier constant everywhere in our code. In fact, we can directly use the class name of the custom cell as the default reuseIdentifier.

We can create a protocol called ReusableViews and create a default declaration method for subclasses of UIView.

protocol ReusableView: class {
    static var defaultReuseIdentifier: String { get }
}

extension ReusableView where Self: UIView {
    static var defaultReuseIdentifier: String {
        return NSStringFromClass(self)
    }
}

extension UICollectionViewCell: ReusableView {
}

By making UICollectionViewCell conform to the ReusableView protocol, we can obtain a unique reuse identifier for each subclass of cell.

let identifier = BookCell.defaultReuseIdentifier
// identifier = "MyModule.BookCell"

Next, we will remove some dirty code from the steps of registering Nib using the same method.

We create a protocol called Nib Loadable Views and add a default method implementation through protocol extension.

protocol NibLoadableView: class {
    static var nibName: String { get }
}

extension NibLoadableView where Self: UIView {
    static var nibName: String {
        return NSStringFromClass(self).componentsSeparatedByString(".").last!
    }
}

extension BookCell: NibLoadableView {
}

By making our BookCell class conform to the NibLoadableView protocol, we now have a safer and more convenient way to obtain the name of the Nib.

let nibName = BookCell.nibName
// nibName = "BookCell"

With these two protocols, we can simplify the registration and usage of cells by using Swift's generics and extending UICollectionView.

extension UICollectionView {
    
    func register<T: UICollectionViewCell where T: ReusableView>(_: T.Type) {
        registerClass(T.self, forCellWithReuseIdentifier: T.defaultReuseIdentifier)
    }
    
    func register<T: UICollectionViewCell where T: ReusableView, T: NibLoadableView>(_: T.Type) {
        let bundle = NSBundle(forClass: T.self)
        let nib = UINib(nibName: T.nibName, bundle: bundle)
        
        registerNib(nib, forCellWithReuseIdentifier: T.defaultReuseIdentifier)
    }
    
    func dequeueReusableCell<T: UICollectionViewCell where T: ReusableView>(forIndexPath indexPath: NSIndexPath) -> T {
        guard let cell = dequeueReusableCellWithReuseIdentifier(T.defaultReuseIdentifier, forIndexPath: indexPath) as? T else {
            fatalError("Could not dequeue cell with identifier: \(T.defaultReuseIdentifier)")
        }
        
        return cell
    }    
}

Pay attention here, we have created two versions of the registration method. One is used to register subclasses of ReusableView, and the other is used to register subclasses of both ReusableView and NibLoadableView. This effectively separates the specific registration methods for view controllers.

Another great detail is that the dequeueReusableCell method no longer needs any reuse identifier strings and can directly use the subclass of cell as its return value.

Now, the code for cell registration and usage looks fantastic :) .

class BookListViewController: UIViewController, UICollectionViewDataSource {

    @IBOutlet private weak var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.collectionView.register(BookCell.self)
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        
        let cell: BookCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
        
        // TODO: configure cell
    
        return cell
    }
    ...
}

Summary

If you are transitioning from Objective-C to Swift, it is worth studying Swift's powerful new features such as protocol extensions and generics in order to find more elegant implementation methods and alternative approaches. iOS development