Intro to UITableView in Swift

dining table

This tutorial is based on Xcode 6.3.1.

UITableView is the bread and butter of iOS development. And for good reason: it’s a great way to represent array data on a mobile device. You’ll find yourself using UITableView quite often.

Don’t be intimidated by method names like cellForRowAtIndexPath and dequeueReusableCellWithIdentifier. Soon they’ll roll off your tongue like JohnJacobJingleheimerSchmidt.

Also pay attention to iOS patterns like Delegate and DataSource. You can apply what you learn here toward Collection Views, Page Views, Map Views, and pretty much every other piece of UI in iOS.

I’m going to purposely run into errors along the way to help you understand what problem each piece of code is trying to solve. You’ll get to see some common issues and how to get past them. As the saying goes, “an expert is someone who has made all the mistakes which can be made.”

There’s also a lot of animated GIFs, just in case you missed them from the ‘90s. More is caught than taught, and in these videos I’ll try to capture the thought process of figuring out the answer.

Since it’s an intro tutorial, we’ll take things slow.

Setup the Project

We want to keep our project simple so that we can focus on the TableView itself.

  • Create a new Project, choosing Single View Application in Swift
  • Disable Size Classes to give us more elbow room in the Storyboard. Do this by selecting the File Inspector and unchecking Use Size Classes

disable size classes

Set the Table

TableView setup begins in the Storyboard. First we’ll position the Table View in a good spot, and then weld it into place.

  • Drag a Table View into the View Controller. If you hover over the View Controller for a second, the edges will try to line up on their own.
  • Click the Add New Constraints icon (the Tie Fighter, not the Tie Advanced)
    tie fighter icon
  • Uncheck Constrain to Margins
  • Set constants in all directions to zero by clicking the orange beams
  • Change Update Frames to Items of New Constraints. This syncs the screen display to the constraints you added.
  • Click Add 4 Constraints

Note: if the table goes somewhere crazy, just Undo (⌘Z). The Undo only applies to the table repositioning, but your constraints are still there.

dragging in a table

Now we’re going to wire up the DataSource. (More on what this means later)

  • Control-drag from the TableView to the View Controller’s Scene icon (it’s the yellow one on the top left).
    yellow icon
  • Choose the dataSource Outlet

set datasource

While we’re here, let’s drag in a Table View Cell into the Table View.

drag in table cell

It’s a good idea to run your project occasionally. Use the keyboard shortcut ⌘R. It will crash, thanks to the DataSource step.

What is a DataSource?

surgical tech

Scalpel?  Scalpel.
Sponge?  Sponge.
cellForRowAtIndexPath?  cell as UITableViewCell.

Surgical techs hand the surgeon various implements during surgery. Similarly, the DataSource hands the TableView a cell whenever it asks for a particular row.

In the previous step, we wired the View Controller as the DataSource. So we had better make sure View Controller is up to the task. Outside of the ViewController class, add the following:

extension ViewController : UITableViewDataSource {
    
}

We’re saying that ViewController can perform the duties of the UITableViewDataSource protocol. Notice the use of extension. This is just a code organization trick to group protocol methods together.

adding an extension in code

You’ll notice we’re getting a new error:

Type ‘ViewController’ does not conform to protocol ‘UITableViewDataSource’

UITableViewDataSource has two required methods that we haven’t implemented. Fortunately, the documentation includes boilerplate code that we can just copy. ⌘-click the word UITableViewDataSource and copy in any method that doesn’t say “optional.” In this case, it’s the first two.

copy boilerplate from datasource docs

numberOfRowsInSection gets called any time the table wants to know how many rows it needs.

cellForRowAtIndexPath gets called each time the table asks for a particular row.

You’ll notice we now have a different error (don’t worry, a different error is a sign of progress):

Missing return in a function expected to return…

The method signatures each have an arrow (->) indicating an expected return value. So let’s give these methods what they want.

return functions

extension ViewController : UITableViewDataSource {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
        return cell
    }
}

numberOfRowsInSection is asking for an Int, so hardcode 1 row for now.

cellForRowAtIndexPath is asking for an UITableViewCell. We’re going to use a method called dequeueReusableCellWithIdentifier. Instead of creating a new cell every time, we “reuse” a cell (much like rinsing out a dirty cup and handing it to your guest). This is a common pattern that you’ll see in collection views and map annotations as well. Why create new, when you can dequeue?

Now that we’ve taken care of all the errors and warnings, build & run. Again, it should crash. (Just be glad I’m not teaching driver’s ed.)

We told the TableView to look for an UITableViewCell on the Storyboard with a Reuse Identifier of “cell.” Let’s fix this by selecting the Table View Cell, and on the Attributes Inspector change the Identifier to cell. Tip: you have to hit Enter to apply your change.

set cell identifier

Build and Run. It should actually run this time without crashing. Congratulations, we now have a blank tableview.

Now we’re cooking with gas

Now that the table is set, it’s time to eat! Let’s start populating the table rows. Add some more rows by updating numberOfRowsInSection to 10. And let’s have this row number show up in the cell. UITableViewCell has a built in UILabel called textLabel. You can set the text to indexPath.row, which is just the row number.

Build and Run, and you should see rows zero through 9.

add numbers to the table

extension ViewController : UITableViewDataSource {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
        cell.textLabel?.text = "(indexPath.row)"
        return cell
    }
}

We can even hardcode some mock data. Create an array of names.

let dwarves = ["Happy", "Sleepy", "Sneezy", "Dopey", "Grumpy", "Bashful", "Doc"]

Set numberOfRowsInSection to the array length.

return dwarves.count

And set the cell’s textLabel to the corresponding name in the array.

cell.textLabel?.text = "(dwarves[indexPath.row])"

Build and Run, and you should see the mock data.

mock data

class ViewController: UIViewController {
    let dwarves = ["Happy", "Sleepy", "Sneezy", "Dopey", "Grumpy", "Bashful", "Doc"]
}

extension ViewController : UITableViewDataSource {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dwarves.count
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
        cell.textLabel?.text = "(dwarves[indexPath.row])"
        return cell
    }
}

UITableViewCell not only has a built-in textLabel, it also has an imageView. Let’s populate it just for fun.

        let imageURL = NSURL(string: "http://tsumtsumdisney.com/wp-content/uploads/2014/10/Snow-White-and-the-Seven-Dwarfs-Tsum-Tsum-Set-of-8-0-0.jpg")
        let imageData = NSData(contentsOfURL: imageURL!)
        cell.imageView?.image = UIImage(data: imageData!)

Note: these images are being loaded synchronously, which is bad. There’s a much better way, but that’s for another tutorial. If you feel the need to fix this, there are tutorials online for downloading images asynchronously.

downloading an image

Here’s the code in View Controller so far:

import UIKit

class ViewController: UIViewController {
    let dwarves = ["Happy", "Sleepy", "Sneezy", "Dopey", "Grumpy", "Bashful", "Doc"]
}

extension ViewController : UITableViewDataSource {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dwarves.count
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
        cell.textLabel?.text = "(dwarves[indexPath.row])"
        let imageURL = NSURL(string: "http://tsumtsumdisney.com/wp-content/uploads/2014/10/Snow-White-and-the-Seven-Dwarfs-Tsum-Tsum-Set-of-8-0-0.jpg")
        let imageData = NSData(contentsOfURL: imageURL!)
        cell.imageView?.image = UIImage(data: imageData!)
        return cell
    }
}

Using Custom Cells

finished product cell

The vanilla UITableViewCell is okay, but most projects will have custom UI. We’re going to put the image in a circle, nothing fancy. The goal is to subclass UITableViewCell with a custom class, and wire the image.

First, create a new Swift File (⌘N) and name it something like TsumCell. Then subclass UITableViewCell.

import UIKit

class TsumCell : UITableViewCell {

}

create custom cell

In the Storyboard, set the cell’s Class to the one we just created. You can do this on the Identity Inspector.

set cell class

Now add some custom UI to the prototype cell.

  • Drag to resize the cell height to 60
  • Put an ImageView in the left side. Resize it so that it’s approximately a square.
  • Place a Label next the image and use the blue guidelines to center it vertically

custom cell elements

Now add some constraints to these elements.

  • Add top, left, and bottom padding to the image. 5 pixels should be good.
  • Don’t forget to uncheck Constrain to margins
  • Check the Aspect Ratio box
  • Control-drag from the Label to the Image View. ⌘-select both Horizontal Spacing and Center-Y
  • Control-drag from the Label to the empty space on the right, and select Trailing Space to Container Margin
  • If you’re feeling lucky, change Update Frames to Items of New Constraints

add constraints

The constraints don’t always come out right the first time, so use the Size Inspector to tweak the constants.

size inspector icon

tweaking constraints

Now wire up the image and label. Alt-click TsumCell.swift to open it alongside the Storyboard. Control-drag from the Image View to the TsumCell class and create an Outlet. Name it dwarfFace.

Do the same for the Label as well, but call it dwarfName.

Note: the outlets need to be made to TsumCell rather than View Controller.

cell outlets

Now set the cell background to a banana yellow, like Snow White’s dress. You can do this from the Attributes Inspector by choosing a Custom color for the Background.

set background color to yellow

Now let’s go back to our View Controller and update our cellForRowAtIndexPath method to use our custom cell. Change UITableViewCell to TsumCell (remember we set the custom class in the Storyboard).

let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! TsumCell

Then replace the built-in textLabel and imageView properties with our custom dwarfName and dwarfFace properties.

        cell.dwarfName?.text = "(dwarves[indexPath.row])"
        ...
        cell.dwarfFace?.image = UIImage(data: imageData!)

wire custom cell properties

Build and Run. Great, now after all that work, it looks pretty much like what we had before. But we did lay the groundwork for future customization. Let’s try to make the image circular, which is common for user profile avatars.

In TsumCell, change the cornerRadius to make the image round. You’ll need to set clipsToBounds as well. Use the method layoutSubviews() which is kind of like viewDidLoad(), but for cells.

    override func layoutSubviews() {
        dwarfFace.layer.cornerRadius = dwarfFace.bounds.size.width / 2
        dwarfFace.clipsToBounds = true
    }

circle face

Build and Run. Looks pretty cute, but what’s with all these lines? For now, let’s patch up this UI issue by setting UITableViewAutomaticDimension on the TableView. Before we do so, we need to wire up the TableView to the View Controller.

Alt-click on ViewController.swift to open it alongside the Storyboard. Control-drag from the TableView to create an outlet. Call it tableView.

Within viewDidLoad, set the rowHeight to automatic dimension. It should look something like this:

override func viewDidLoad() { 
    tableView.rowHeight = UITableViewAutomaticDimension 
}

automatic dimension

Build and Run. The lines look under control, but now the circles aren’t quite round anymore. This is good enough for now, but there are tutorials online if you want to resolve the row height issue.

Recap

From a blank project, we dropped a TableView onto the Storyboard. We wired up the DataSource, and populated the cell with a hard-coded image and an array of text. Then we subclassed UITableViewCell and wired outlets to some custom UI. This allowed us to do something like clip the image to a circle. This should give you a good starting point to play around with default cell types, and create some custom cells of your own.

Here’s the code so far:

TsumCell.swift

import UIKit

class TsumCell : UITableViewCell {

    @IBOutlet weak var dwarfFace: UIImageView!
    @IBOutlet weak var dwarfName: UILabel!
    override func layoutSubviews() {
        dwarfFace.layer.cornerRadius = dwarfFace.bounds.size.width / 2
        dwarfFace.clipsToBounds = true
    }
}

ViewController.swift

import UIKit

class ViewController: UIViewController {
    let dwarves = ["Happy", "Sleepy", "Sneezy", "Dopey", "Grumpy", "Bashful", "Doc"]

    @IBOutlet weak var tableView: UITableView!
    override func viewDidLoad() {
        tableView.rowHeight = UITableViewAutomaticDimension
    }
}

extension ViewController : UITableViewDataSource {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dwarves.count
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! TsumCell
        cell.dwarfName?.text = "(dwarves[indexPath.row])"
        let imageURL = NSURL(string: "http://tsumtsumdisney.com/wp-content/uploads/2014/10/Snow-White-and-the-Seven-Dwarfs-Tsum-Tsum-Set-of-8-0-0.jpg")
        let imageData = NSData(contentsOfURL: imageURL!)
        cell.dwarfFace?.image = UIImage(data: imageData!)
        return cell
    }
}

Got any tips and tricks for UITableView? Or have any suggestions for future tutorial topics? Feel free to add your thoughts to the comments.

Like this post? Please share it! Then follow us on Twitter @thorntech and join our mailing list below for future updates.

Dining Table Image courtesy of Stijn Nieuwendijk on Flickr

Surgical Image courtesy of Carrington College on Flickr

Get insights on SFTP Gateway, cloud computing and more, in your inbox.

Get smarter about all things tech. Sign up now!

Scroll to Top