Writing clean UITableViewController code


When dealing with UITableViews there’s often the challenge of handling different types of UITableViewCells while still keeping your code D.R.Y and readable. I want to show you a simple way to keep your UITableViewDataSource and -Delegate clean and still be able to alter different numbers and types of cells in your UITableView in an elegant way.

If you can’t wait to try it out, please feel free to download the source code and sample app here.

Screenshot on iOS Simulator

At the end of this post you’ll be able to define the contents of your UITableView by just altering a single array such as this:

class MyAppsTableViewController: SimpleTableViewController {    
    func viewDidLoad() {
      super.viewDidLoad()

      sections = [
        TableSection(title: "Purchasable items", rows: [
            PurchaseRow(title: "1 month subscription (out of stock)", price: nil),
            PurchaseRow(title: "12 month subscription", price: "$ 36.0"),
            PurchaseRow(title: "Preview", price: "Free")
        ]),
        TableSection(title: "Info", rows: [
            InfoRow(onSelected: { [weak self] in 
                self?.showInfo()
            }, title: "Read info", subtitle: "Click here to get some info about your purchases")
        ])
      ]
    }
}

First let’s start off with a few protocols and structs and set up a boilerplate which you can reuse for other tableViews as well. We’ll keep it as simple as possible while being as generic as required.

typealias TableSection = (title: String, rows: [TableRow])

protocol TableRow {
    var onSelected: (() -> Void)? { get }
    func configureCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell
}

The foundation of our work is a tuple and a protocol to describe sections and rows inside our tableView. One a row is being requested, we’ll simply delegate to the configureCell method of this specific row to let it configure it, this way we’ll end up with a very readable func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell method.

Now let’s add some concrete cells and have a look how they’re tied together with UITableViewDataSource.

struct PurchaseRow: TableRow {
    var onSelected: (() -> Void)?

    let cellIdentifier = "PurchaseCell"
    let title: String
    let price: String?
    
    func configureCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
        cell.textLabel?.text = title
        cell.detailTextLabel?.text = price
        return cell
    }
}

struct InfoRow: TableRow {
    var onSelected: (() -> Void)?

    let cellIdentifier = "InfoCell"
    let title: String
    let subtitle: String
    
    func configureCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
        cell.textLabel?.text = title
        cell.detailTextLabel?.text = subtitle
        return cell
    }
}

Looking at the code above you can see how we’re going to describe the contents of each of row type and explicitly couple the dequeing and configuration of the UITableViewCell to this specific row.

The code we have to write to wire this up with our UITableView now is pretty trivial.

open class SimpleTableViewController: UITableViewController {
    var sections = [TableSection]()
}

extension SimpleTableViewController {
    override func numberOfSections(in tableView: UITableView) -> Int {
        return sections.count
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections[section].rows.count
    }
    
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return sections[section].title
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let section = sections[indexPath.section]
        let tableRow = section.rows[indexPath.row]
        return tableRow.configureCell(for: tableView, at: indexPath)
    }

    override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let section = sections[indexPath.section]
        let tableRow = section.rows[indexPath.row]
        tableRow.onSelected?()
        tableView.deselectRow(at: indexPath, animated: true)
    }
}

To use this tableView, just subclass SimpleTableViewController and set the desired sections as shown at the very top. That’s it.

You can download the source code including a demo here.

I hope you’ve found this post interesting and that it inspires you on how you can make your code more readable and elegant.

Also I would love to hear your opinion on this so please feel free to drop me a line on Twitter or via Email.

Cheers, Marcus