cellForRowAtIndexPath in Four Lines

The UITableViewController is a go-to class for iOS developers but seems especially prone to code bloat. Consider that right out of the box it is both a UITableViewDataSource and UITableViewDelegate. If you use Core Data you can add an NSFetchedResultsControllerDelegate. Add UISearchBarDelegate and friends and you are well on your way to a 500 line fat controller that nobody wants to touch.

If you have not seen it I recommend spending an hour to watch the great talk Andy Matuschak gave at NSSpain on refactoring view controllers. It contains a wealth of advice on slimming a table view controller. For this post I am going to tackle just one method, cellForRowAtIndexPath, with the aim to reduce cellForAtIndexPath to just four lines.

Refactoring cellForRowAtIndexPath

This method is the core of a table view controller since it constructs each table view cell we display. Consider a simple todo list application that uses cells to show a task name, optional note and a checkbox to mark the task complete. The cellForRowAtIndexPath method might be as below:

override func tableView(_ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
  let cell = tableView.dequeueReusableCell(withIdentifier: cellId,
             for: indexPath) as! TaskCell
  let task = fetchTaskAtIndexPath(indexPath)
    
  cell.taskTitle.text = task.title
  cell.taskTitle.font = UIFont.preferredFont(forTextStyle: .headline)

  cell.taskNote.text = task.note
  cell.taskNote.font = UIFont.preferredFont(forTextSTyle: .subheadline)
    
  cell.checkbox.image = task.complete ? UIImage(named: "checked"):
                                        UIImage(named: "unchecked")
  return cell
}

It starts off well by dequeuing a table view cell - not much we can do about that. Then it retrieves the task object for this row in the table (this might be using a fetched results controller if we are using Core Data but I want to avoid those details for now).

Unfortunately it then gets lazy and reaches into our custom table view cell and directly messes with the view. We have all done it (at least I have) but this is straying from the path of lean and clean view controllers which leads to pain and we all know where that ends up.

First Make Your IBOutlets Private

Our custom subclass of UITableViewCell has IBOutlets connected in Interface Builder but it is more or less empty as the fat view controller does all the view work - this is not good:

class TaskCell: UITableViewCell {
  @IBOutlet weak var checkbox: UIImageView!
  @IBOutlet weak var taskTitle: UILabel!
  @IBOutlet weak var taskNote: UILabel!
}

Nobody except the view should know or care about the view details. We can make that true by making our outlets private:

@IBOutlet private weak var checkbox: UIImageView!
@IBOutlet private weak var taskTitle: UILabel!
@IBOutlet private weak var taskNote: UILabel!

It would help if Interface Builder made IBOutlets private by default but now we have done it our view controller can no longer interfere.

Immutable View Data

With our outlets private we need a public interface to give the task data to the view. Our task has a title, optional note and a completion flag. We could define separate properties for each but it is easier to group them into a struct defined in our view class:

struct TaskViewData {
  let title: String
  let note: String?
  let complete: Bool
}

We can then add a property to the view class to hold the view data and use the didSet method of the property to setup our view all at once:

var taskViewData : TaskViewData? {
  didSet {
    taskTitle.text = taskViewData?.title
    taskTitle.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
    // other view setup code...
}

The key principle here is that our table view cell is now working with simple, immutable value types. Our view controller only needs to set the view data and no longer needs to care about the view internals.

Transforming our Task object to View Data

A tip I picked up from the talk by Andy Matushak. We need to convert from our task object to the view data used by the cell. For example our task object could have an NSDate object but our view data needs a formatted and localised string to display. We don’t want that data transformation code cluttering our view controller.

A convenient solution is to define an initialiser for our view data struct which takes a task object as an argument:

extension TaskCell.TaskViewData {
  init(task: Task) {
    title = task.title
    note = task.note
    complete = task.complete
  }
}

Now our view controller can in one line initialise the view data from a task object:

cell.taskViewData = TaskCell.TaskViewData(task: task)

Down to Four Lines

With all the view manipulation code moved to our UITableViewCell subclass we have a cellForRowAtIndexPath function with just four lines:

override func tableView(_ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! TaskCell
  let task = fetchTaskAtIndexPath(indexPath)
  cell.taskViewData = TaskCell.TaskViewData(task: task)
  return cell
}
  • Line 1: Dequeue our custom table view cell
  • Line 2: Fetch the object
  • Line 3: Transform the object into view data
  • Line 4: Return the cell

May it always be so.

Further Reading