Swift: Simple Generic UITableView Extensions

If you're looking to make your first steps into Swift generics, here are a couple of very simple yet useful examples. I assume basic knowledge of what generics are, but provide a little more explanation of how these particular functions work with types. The code shown is using Swift 3 syntax.

UITableView has some functions that ask for a cell reuse identifier, which is a string. Specifying strings as arguments is always less desirable than specifying a real type that the compiler has knowledge of, so whenever a string argument can be replaced by a real type, it's a great idea to do so. Some of the benefits include:

  • Compile time checking of the real type instead of a potential run time failure on a mistyped string
  • Code completion hints are given when typing the name of a real type
  • If a faulty string is entered there is no immediate warning or error, but Xcode will give an error immediately if an unknown type is entered

The following extensions rely on the following convention: a UITableViewCell subclass' name, xib file name, and reuse identifier are always the same. Given that, we can use a class name to get the reuse identifier (since it is the same name) and the nib (by loading the nib with the same name). If we can also convert a class type to a class name (as a String), then we can replace the use of Strings with class types, gaining the benefits noted above. After showing the extension functions, I'll discuss a little bit more about how they work.

Here's the first extension:

extension UITableView {
/**
Registers a cell with the table view.
Depends on the assumption that the cell's reuse identifier matches its class name.
If a nib is found in the main app bundle with a filename matching the cell's class name, that nib is registered with the table view. Otherwise, the cell's class is registered with the table view.
- parameters:
   - type: The class type of the cell to register.
*/
    func registerCell<T: UITableViewCell>(ofType type: T.Type) {
        let cellName = String(describing: T.self)
        
        if Bundle.main.path(forResource: cellName, ofType: "nib") != nil {
            let nib = UINib(nibName: cellName, bundle: Bundle.main)
            
            register(nib, forCellReuseIdentifier: cellName)
        } else {
            register(T.self, forCellReuseIdentifier: cellName)
        }
    }
}

As shown below, this extension function slightly simplifies table view setup code and provides the type checking benefits noted at the start of this post.

// Instead of this
let nib = UINib(nibName: "CustomTableViewCell", bundle: Bundle.main)
tableView.register(nib, forCellReuseIdentifier: "CustomTableViewCell")

// We can do this
tableView.registerCell(ofType: CustomTableViewCell.self)

Here's the second extension:

extension UITableView {
/**
Dequeues a cell that has been previously registered for use with the table view.
Depends on the assumption that the cell's class name and reuse identifier are the same.
- parameters:
   - type: The class type of the cell to dequeue.
- returns: A UITableViewCell already typed to match the `type` provided to the function.
*/
    func dequeueCell<T: UITableViewCell>(ofType type: T.Type) -> T     {
       let cellName = String(describing: T.self)
        
       return dequeueReusableCell(withIdentifier: cellName) as! T
    }
}

Again, this extension function simplifies code at the call site and provides type checking, as show below:

// Before having the extension, we might have had code like this to dequeue and populate a cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // This is the line that will change
    let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell") as! CustomTableViewCell

    // Populate the cell here
    
    return cell
}

// Now, the one line changes and we have this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueCell(ofType: CustomTableViewCell.self)

    // Populate the cell here
    
    return cell
}

So, what's happening in these extensions? Lets deconstruct the second extension for a better understanding. Here it is again with more explanation:

/*
The function signature is standard for generics with one exception. The argument is of type `T.Type`. Using `.Type` indicates the function is expecting an actual type, not an instance of the type.
*/
func dequeueCell<T: UITableViewCell>(ofType type: T.Type) -> T {
   /*
   String provides this initializer that gets the name of a type as a String. 
   Note that when passing a type into a function, the type must be suffixed with `.self`.
   */
   let cellName = String(describing: T.self)
        
   /*
   This is the call to the standard UITableView function we are wrapping. It is cast to the type provided to this function. Otherwise the object returned is considered to be of type UITableViewCell by the compiler. Doing the cast here simpilfies code at the call site for this function.
   */
   return dequeueReusableCell(withIdentifier: cellName) as! T
}

Here's the call site code with a little more explanation:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    /*
    Again, when passing a type into a function rather than an instance, use the type name with the suffix `.self`. The standard UITableView function returns a UITableViewCell object that must typically be cast to the subclass type before working with the object. But our extension function performs the cast for us, so the cell will be of type CustomTableViewCell already.
    */
    let cell = tableView.dequeueCell(ofType: CustomTableViewCell.self)

    // Populate the cell here
    
    return cell
}