Giles Van Gruisen

All about table views!

Table views are single-column lists of information. As you can imagine, they're pretty crucial for nearly any app that wishes to display a collection of information. UITableView inherits directly from UIScrollView, so you can take advantage of many of the nifty things that scroll views provide. This article assumes you prefer to do most things programmatically. If you're using Storyboards and Interface Builder extensively, things will work a bit different but much of this will still be useful knowledge to have.

Table view cells

On its own, a table view (UITableView) isn't very exciting. At most, you'll see a white view with a bunch of grey lines. Between each line is essentially a placeholder cell that the table view is expecting you to replace with a real cell. You can think of these cells as units of content, just like cells in a spreadsheet. By that I mean a distinct view used to display some sort of information to the user. You'll need to tell the table view which cells to present to the user and that's done via the dataSource property on UITableView. Before getting into the data source, it's valuable to know a bit more about how a table view manages its cells with a technique called "cell reuse".

Cell reuse

Cell reuse is a memory-saving technique that recycles cells as the user scrolls the table view. It's based on the notion that your table view likely holds more data than it can dsplay to the user at any given time (or scroll position). The table view holds a queue containing a bunch of reusable cells. Unless you have multiple cell types, the queue typically only holds onto the maximum number of cells that can fit into the table view's visible area. As you scroll, it will dequeue (pop off) a cell for you to configure in your data source.

Because the table view needs to know which kind of cells to reuse, each cell class must be registered by the table view with a reuse identifier. The class can be either the standard UITableViewCell or any subclass thereof where you can define custom layout and behavior. The reuse identifier is a string to describe the type of cell, used by the data source to determining which type of cell to dequeue. Any cell you wish to use must be registered for reuse by the table view upon data source initialization, as follows:

Example:

class MyTableViewController: UITableViewController {
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

        /* For purely programmatic subclasses: */
        tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: "Cell")

        /* If you're using Interface Builder: */
        // let cellNib = UINib(nibName: "MyCell", bundle: NSBundle.mainBundle())
        // tableView.registerNib(cellNib, forCellReuseIdentifier: "Cell")
    }
    ...
}

Table view data source

At this point your table view may be looking a bit dull. As I mentioned, we need to implement our table view's data source so it has something to display.

The data source, as its name implies, is where your table view looks when it's ready to display some information to the user. A table view without a data source is like a car without an engine (or motor), practically useless. This data source often tends to be the view controller containing the table view, but really it can be any object that follows the UITableViewDataSource protocol.

This protocol simply outlines a series of methods to be called on the data source by the table view itself as it goes about its quest for data to display. In particular, there are two required methods you should know about. The first returns the number of rows in a given section and the second returns the cell for a given row.

Number of rows in section

tableView(_:numberOfRowsInSection:) returns the number of rows for a given section. If you have only you have only one section and your data objects are held in an array then this is should return the length (count) of that array. If you have more than one section, see the the API declaration for numberOfSectionsInTableView(_:).

Example:

class MyTableViewController: UITableViewController {
    ...
    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        // countries is an my data array containing country names
        return countries.count
    }
    ...
}

Cell for row at index path

tableView(_:cellForRowAtIndexPath:) returns a properly configured cell for the given indexPath. You can dequeue one of your reusable cells by calling dequeueReusableCellWithIdentifier(_:) on the tableView, passing it the reuse identifier you used to register your cell class as mentioned in the Cell Reuse section. Once your cell is dequeued you can configure it however you wish, filling in the blanks with whatever data you choose, and then return it. If your data is held in an array, you can find the proper object with which to populate the cell by querying that array for the given index. Use the property row on indexPath to get that index within a particular section. (It's worth noting that row is a UIKit addition to NSIndexPath, and is thus not documented with the rest of NSIndexPath). You can find row and other extensions documented here.

Example:

class MyTableViewController: UITableViewController {
    ...
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        // Pulls a reusable cell from the queue for configuration
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell

        // Set the cell's text label to the country at index indexPath.row
        cell.textLabel.text = countries[indexPath.row]

        // Return the cell!
        return cell
    }
    ...
}

Updates

It's great to have some data in your table view, but what happens if your data changes? Your data array may change in size but the data source will never know. You may want to refresh the table view periodically as data comes in, or upon a user event like adding a new item. There's a table view method reloadData() which tells it to check the data source again and update itself. One handy trick with swift is to add a didSet property observer on your data array so the table view automatically reloads when the array changes.

Example:

class MyTableViewController: UITableViewController {
    var countries: [String] {
        didSet {
            tableView.reloadData()
        }
    }
    ...
}

Table view delegate

The table view delegate helps you handle interactivity and more advanced layout within the table view. the table view's delegate property can be any object, so as it follows the UITableViewDelegate protocol. This protocol outlines a number of methods that you may choose to override to provide your table view with additional functionality. The UITableViewDelegate protocol is fairly comprehensive so I'll only cover a few of of the most common tasks but I urge you to go through the documentation to learn more.

User taps

Probably the most common piece of functionality you'll want to add to your table views is the ability to tap an individual row and make something happen. Whether that's displaying a new view with more information or making a call or what, it all starts with the same UITableViewDelegate method, tableView(_:didSelectRowAtIndexPath:).

This gets called on your table view delegate whenever a user taps a row. The method will provide you with two variables, both fairly crucial for determining exactly which row was tapped, so your application logic knows what to do next.

The first, tableView (UITableView), tells the receiver which table view the user is interacting with, in case you may have two table views with the same delegate.

The second, indexPath (NSIndexPath), helps you determine which individual row has been tapped. Just like in the data source method tableView(_:cellForRowAtIndexPath:), you can use this and its property row to query your data array for the object behind the cell that has been tapped. Now you can initialize a new view or view controller with the proper data to present to the user. If you're using UINavigationController, this is where you can push that new view controller onto the stack. (navigation controllers are super cool and I plan to cover them more in depth soon. For now, check out the documentation).

Example:

class MyTableViewController: UITableViewController {
    ...
    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 
        // Get the country at index indexPath.row
        let country = countries[indexPath.row]

        // Build alert controller with country as title
        let alert = UIAlertController(title: country, message: nil, preferredStyle: UIAlertControllerStyle.Alert)

        // Add a cancel action to dismiss the alert
        let cancelAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Cancel, handler: nil)
        alert.addAction(cancelAction)

        // Display the alert to the user
        presentViewController(alert, animated: true, completion: nil)
    }
    ...
}

User swipes

Another common bit of functionality is allowing the user to swipe a table view cell to reveal additional controls such as a delete button. Apple makes this relatively easy, but it can be a bit confusing the first time to figure out how the pieces fit together.

To enable editing on your table view cells, there's one method, tableView(_:canEditRowAtIndexPath:), called on the table views data source that lets the table view know that the row in question can indeed be edited. If you only want some rows to be editable, you can check the indexPath parameter to help determine whether to return true or false. Adding this method alone won't result in any noticeable changes. Hang in there!

Example:

class MyTableViewController: UITableViewController {
    ...
    override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
        // Return false if you don't want the item to be editable
        return true
    }
    ...
}

When a user makes a swipe gesture, the table view looks for (but does not call) tableView(_:commitEditingStyle:forRowAtIndexPath:) to determine whether to display the additional controls. By default, this control is simply a "delete" button. If the user then decides to tap the "delete" button, then this method will be called on the delegate. It will give you access to the tableView, editingStyle (UITableViewCellEditingStyle), and indexPath of the row being acted upon.

So what's this editing style? This is how you determine which type of action the user is taking on the cell. In the case of delete, editingStyle will be UITableViewCellEditingStyleDelete. With this in mind, you can then take the necessary steps to remove the row from the table and delete any associated data to prevent it from popping up when the table view next reloads.

You can remove rows without reloading the table view, using the table view method deleteRowsAtIndexPaths(_:withRowAnimation:). You must also remove the object from your data array or else this method will throw an error. It's also worth mentioning that this is not compatible with the aforementioned data array property observer.

Example:

class MyTableViewController: UITableViewController {
    ...
    override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {

            // Remove the string from the data array
            countries.removeAtIndex(indexPath.row)

            // Remove the row from the table view with a fade animation
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
        }
    }
    ...
}

You can reach me for feedback on twitter or via email. Thanks for reading!