How To: Working With Table View Controllers In Swift

Written by Reinder de Vries on May 18 2018 in App Development

How To: Working With Table View Controllers In Swift

A table view controller displays structured, repeatable information in a vertical list. You use the UITableViewController class in your iOS app to build a table view controller.

Working with a table view controller also means working with a few important iOS development concepts, such as subclassing, the delegation design pattern, and re-using views.

In this article I’ll show you step-by-step how table view controllers work, and how you can use them. We’ll go into the full gamut of UITableViewController, by diving into Object-Oriented Programming, delegation and the behind-the-scenes mechanisms of table views.

It’s important for professional and practical iOS developers (you!) to master working with table view controllers. Once you’ve gotten used to working on such a multifaceted UI component, like UITableViewController, other more complex aspects of iOS development will start to make sense too.

Ready? Let’s dive in!

  1. How A Table View Controller Works
  2. Setting Up Simple Table View Controller
  3. Implementing The Table View Controller Data Source
  4. Providing Cells To The Table View Controller
  5. Responding To User Interaction
  6. Further Reading

Check out this episode of Practice Hour, that’s entirely devoted to the inner workings of the table view controller.

How A Table View Controller Works

If you’ve used any iOS app before, you’ve used table view controllers before. They’re used that frequently in iOS apps!

Here’s an example of a table view controller, as created in the Habitat blockchain app:

How To: Working With Table View Controllers In Swift

A table view controller typically has these visible components:

  • A table view, which is the user interface component, or view, that’s shown on screen. A table view is an instance of the UITableView class, which is a subclass of UIScrollView.
  • Table view cells, which are the repeatable rows, or views, shown in the table view. A table view cell is an instance of a UITableViewCell class, and that class is often subclassed to create custom table view cells.

A table view controller also relies on the use of these components, behind-the scenes:

  • A table view delegate, which is responsible for managing the layout of the table view and responding to user interaction events. A table view delegate is an instance of the UITableViewDelegate class.
  • A table view data source, which is responsible for managing the data in a table view, including table view cells and sections. A data source is an instance of the UITableViewDataSource class.

A navigation controller is often used in conjuction with a table view controller to enable navigation between the table view and subsequent view controllers, and to show a navigation bar above the table view.

The most interesting part of working with table view controllers is the table view controller itself! How so?

Are you familiar with the Model-View-Controller architecture? As per the Model-View-Controller architecture, a table view and a table view cell are views, and a table view controller is a controller.

Views are responsible for displaying information visibly to the user, with a user interface (UI). Controllers are responsible for implementing logic, managing data and taking decisions. Said differently: you can’t see a controller, but it’s there, managing what you see through views.

When you use a table view controller in your app, you’re subclassing the UITableViewController class. The UITableViewController class itself is a subclass of UIViewController.

Here’s the class hierarchy of an example table view controller that displays contact information:

  • an instance of ContactsTableViewController
    • subclasses UITableViewController
      • subclasses UIViewController

As per the principles of Object-Oriented Programming (OOP), when class RaceCar subclasses class Vehicle, it inherits the properties and functions of that superclass, such as maxSpeed and drive().

The Vehicle class is then called the superclass of RaceCar. This principle is called inheritance, and it’s explained extensively in our free Basics of iOS Apps course.

Confusing? It can be! Think about it like this: in order for your table view controller to work OK, you’ll need to inherit a bunch of code, so you don’t have to write all that code yourself. The class hierarchy, and OOP, is there to structure that inheritance.

How To Working With Table View Controllers In Swift

You can work with table views without using a table view controller. Simply add a UITableView to a view controller, provide it with implementations of the table view delegate and data source functions, and you’re done.

The UITableViewController class provides default implementations of these table view delegate and table view data source functions. That’s a crucial aspect of working with a table view controller!

As you’ll see in the next chapters of this article, we’ll override these functions with our own implementations. We can customize the table view controller by doing that.

Learn how to build iOS apps

Get started with iOS 13 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 13 apps with Swift 5 and Xcode 11.

Setting Up Simple Table View Controller

Alright, let’s put that theory into practice. In this chapter, we’re going to build a simple table view controller. You’re going to implement the functions needed to make it work, and I’ll explain how they work as we go along.

You can use 3 different approaches of working with User Interface components in Xcode and Interface Builder.

  1. Creating views programmatically, i.e. coding them by hand
  2. Setting up UIs in Interface Builder and connecting them with Swift code via outlets
  3. Setting up UIs and their transitions in Interface Builder using Storyboards

It’s tedious and unproductive to code UIs by hand, and working with Storyboards often hides the mechanisms of a UI component from the developer. That’s why, in this article, we’re going with the second option: using Interface Builder and “XIBs”.

A XIB is an Interface Builder file that contains XML information about user interface components, or views. You use Interface Builder to “scaffold” your UIs, and then connect individual components, like buttons, to outlets, so you can change their behaviour with code. You can use Interface Builder to change many attributes of a UI component, like text color and background, which is much more productive than coding those things by hand.

Here’s what you’re going to do:

  • First, create a new Xcode project, a Single View Application.
  • Then, remove ViewController.swift and Main.storyboard from your project
  • Finally, go to your project’s properties by clicking on the project name, at the top, in Project Navigator, and remove Main from the Main Interface field under Deployment Info

How To Working With Table View Controllers In Swift

Now, let’s create that table view controller. Here’s how:

  • First, right-click on the project folder in Project Navigator and choose New File···.
  • Then, pick the Cocoa Touch Class template from the iOS -> Source category, and click Next.
  • Then, type UITableViewController in the Subclass of field, and ContactsTableViewController in the Class field, and make sure to tick the checkbox for Also create XIB file, and click Next.
  • Finally, save the file in the same folder as the AppDelegate.swift file, make sure the project is selected next to Targets, and click Create.

Now two new files appear, ContactsTableViewController.swift and ContactsTableViewController.xib. The former contains all the Swift code to make the table view controller work, and the latter contains all the layout information to properly display it on screen.

As you’ve guessed, your ContactsTableViewController class is a subclass of UITableViewController. You can see that in the Swift file, at the top, in the class declaration.

class ContactsTableViewController: UITableViewController 
{
    ···

This syntax means: the class ContactsTableViewController is a subclass of UITableViewController.

When you right-click on “UITableViewController” while holding the Option-key, you can see in the class declaration that UITableViewController is a subclass of UIViewController, and conforms to the UITableViewDelegate and UITableViewDataSource protocols.

How To Working With Table View Controllers In Swift

That’s the power of the table view controller! It doesn’t just give us the individual components to make a table view, the controller also provides a default implementation. That’s why we subclass UITableViewController, instead of creating a blank view controller with a table view.

OK, one last thing before we continue. Add the following code to the application(_:didFinishLaunchingWithOptions:) function of the AppDelegate class in AppDelegate.swift.

You can add it right above the line return true.

let contacts = ContactsTableViewController(nibName: "ContactsTableViewController", bundle: nil)
contacts.title = "Contacts"

let navigationController = UINavigationController(rootViewController: contacts)

window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = navigationController
window?.makeKeyAndVisible()

Here’s what happens:

  • First, we’re initializing an instance of our ContactsTableViewController class, and provide it the name of our XIB file. You also set its title to "Contacts", which will show nicely in the UI’s navigation bar.
  • Then, you initialize an instance of UINavigationController and provide the table view controller as its root view controller. This effectively embeds the table view controller in a navigation controller.
  • Finally, you create a new UIWindow with the size of the iPhone screen, assign the navigation controller as it’s root view controller, and call makeKeyAndVisible(), which makes it the front window of our app.

When you use Storyboards, you don’t have to implement the code we’ve just written. You can use the Storyboard to embed the table view controller in a navigation controller, and that’s that.

The code above still operates behind-the-scenes, and you would have missed that if you hadn’t set up your own window and root view controllers at least once.

Looking at the code above, our view controller hierarchy is this:

  • App window
    • Navigation controller
      • Contacts table view controller
        • Navigation bar
        • Table view
        • etc.

At this point, you can run your app with Command-R or the Play button, and see the empty table view controller appear on screen.

Why is that, by the way? We haven’t coded anything yet! That’s because the table view controller has a default implementation, that just shows empty cells on screen.

How To Working With Table View Controllers In Swift

A XIB and a NIB are basically the same thing – they contain layout information. A XIB has an XML format, whereas a NIB has a binary format. The XML is compiled to binary when you build your app, so that’s why UIKit’s functions always talks about a “nib”, while Xcode always calls it a “xib”.

Implementing The Table View Controller Data Source

Now that your table view controller has been set up, let’s bring it to life! In this chapter, we’ll focus on the different functions you’ll need to implement to make your table view controller work.

As explained earlier, these functions either belong to the table view controller delegate, or the table view controller data source.

The most important functions for UITableViewDataSource are:

  • numberOfSections(in:)
  • tableView(_:numberOfRowsInSection:)
  • tableView(_:cellForRowAt:)

Other relevant functions for UITableViewDelegate are:

  • tableView(_:didSelectRowAt:)
  • tableView(_:willDisplay:for:)

You can find more functions in the Apple Developer Documentation for UITableViewDelegate and UITableViewDataSource.

Adding The Contacts Data
You’re going to start by adding the contact information data for the table view controller. Add the following property to the ContactsTableViewController class:

let contacts:[[String]] = [
    ["Elon Musk",       "+1-201-3141-5926"],
    ["Bill Gates",      "+1-202-5358-9793"],
    ["Tim Cook",        "+1-203-2384-6264"],
    ["Richard Branson", "+1-204-3383-2795"],
    ["Jeff Bezos",      "+1-205-0288-4197"],
    ["Warren Buffet",   "+1-206-1693-9937"],
    ["The Zuck",        "+1-207-5105-8209"],
    ["Carlos Slim",     "+1-208-7494-4592"],
    ["Bill Gates",      "+1-209-3078-1640"],
    ["Larry Page",      "+1-210-6286-2089"],
    ["Harold Finch",    "+1-211-9862-8034"],
    ["Sergey Brin",     "+1-212-8253-4211"],
    ["Jack Ma",         "+1-213-7067-9821"],
    ["Steve Ballmer",   "+1-214-4808-6513"],
    ["Phil Knight",     "+1-215-2823-0664"],
    ["Paul Allen",      "+1-216-7093-8446"],
    ["Woz",             "+1-217-0955-0582"]
]

That’s a rolodex we’d all like to have, right? Here’s how it works:

  • The let contacts declares a constant with name contacts. You’ve added it as a property to the class, so every class instance has access to this constant throughout the class’ code.
  • The type of contacts is [[String]], which is array of array of strings. You’re essentially creating an array, of which the items are arrays of strings. (A variable name and its type are separated with a colon :)
  • The = [ ··· ] code assigns an array literal to contacts, filled out with the names and phone numbers of a few billionaires.

At a later point, we can use the number of items in the array with contacts.count. And we can get individual names and phone numbers with contacts[x][0] and contacts[x][1], using subscript syntax.

Registering A Table View Cell Class
Before you can use cells in a table view controller, you’ll need to register them with the table view. You can do so in two ways:

  1. By providing a table view cell class and an identifier
  2. By providing a table view cell XIB and an identifier

When you’re using a custom table view cell, you most likely want to register a XIB for that. When you’re using the default table view cells, or some other programmatic cell, you register the class. We’ll use the class, for now!

Add the following code to the viewDidLoad() function:

tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellIdentifier")

Make sure to add it below the super.viewDidLoad() line. As you probably know, the viewDidLoad() function is part of the view controller life-cycle, and belongs to the UIViewController class.

You’re overriding the viewDidLoad() function to respond to this event in the life-cycle of a view controller, so you can set up your view after it has been loaded. In our case, we’re using the function to register the table view cell.

When you register a table view cell, you also have to provide an identifier. This is simply to associate the class of the cell with a name you can use later, when dequeuing the cell in tableView(_:cellForRowAt:).

Are you still with me? Let’s move on!

Implementing “numberOfSections(in:)”

The first delegate function we’re going to implement is numberOfSections(in:).

A table view can have multiple sections or groups. Every group has a header that floats on top of the vertical row of cells. In a Contacts app, you could group contacts together alphabetically. This is actually done in the Contacts app on iPhone, where contacts are grouped A-Z.

The app we’re building has just one section. Add the following function to the class:

override func numberOfSections(in tableView: UITableView) -> Int
{
    return 1
}

Simple, right? The function returns 1 when called.

Implementing “tableView(_:numberOfRowsInSection:)”

A similar function is tableView(_:numberOfRowsInSection:). Instead of providing the number of sections, it provides the number of rows in a section. Because a table view shows cells in a vertical list, every cell corresponds to a row in the table view.

The app we’re building has just one section, and that one section has a number of items equal to the number of items in the contacts array. So, that’s contacts.count!

Add the following function to the class:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
    return contacts.count
}

See how that works? We simply return contacts.count. If you were to add another name and phone number to contacts, it would show up nicely in the table view too.

Understanding Rows And Sections
Our Contacts app is one-dimensional, it just shows one list of names and phone numbers, and it doesn’t use groups. But what if you have a grouped table view?

In most cases, your data provider, like the contacts array, would be multi-dimensional too. You’d organize groups on the first level, and individual items on the second level, “below” the groups.

Like this:

- Countries
    - A
        - Afghanistan
        - Albania
        - ···
    - B
        - Bahamas
        - Bahrain
        - ···
    - C
        - Cambodia
        - Cameroon
        - ···

The number of groups is equal to countries.count, and the number of countries in a single group is equal to countries[x].count, where x is the section index. That section index is provided as a parameter in tableView(_:numberOfRowsInSection:).

Did you notice how these two functions have a parameter called tableView? That’s part of the Object-Oriented Programming principle. You can technically use a table view data source and delegate to power multiple table views. You’d use the tableView to identify which table view you are working with.

Imagine you have a Contacts app that can show phone numbers by name, or phone numbers organized by company. You could implement that in multiple ways, for instance by reusing your table view controllers. Or what if you want to reuse the layout of your Contacts app to display similar information, like restaurants, venues or Skype usernames? That’s where code re-use with OOP comes in!

Providing Cells To The Table View Controller

We’re getting there! Let’s move on to the most important function of a table view controller: tableView(_:cellForRowAt:).

We’ll implement the function before diving into the details, but there’s a couple things you need to understand about it:

  • When it’s called
  • What an index path is
  • How it re-uses cells

First, add the following function to the ContactsTableViewController class:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{

}

Here’s how it works:

  • The function overrides its superclass implementation from UITableViewController. By now you know how that works, right? We’re overriding the default implementation, and substitute our own. That’s because UITableViewController has already implemented the table view delegate and data source for us.
  • Like before, the function has one parameter tableView that we can use to identify the table view that this function is called on.
  • Another parameter is indexPath, with argument label cellForRowAt. The index path identifies the cell’s row and section indices. More on that later.
  • The function return type is UITableViewCell. Hey, that’s interesting. This function is called by the table view controller, every time we need to provide a table view cell!

When you scroll through the contacts in this app, every time a cell needs to be displayed on screen, the function tableView(_:cellForRowAt:) is called. Every time! I’ll prove it to you in a moment.

Next up, let’s write the function body. Add the following code inside the function:

let cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier", for: indexPath)

print("\(#function) --- section = \(indexPath.section), row = \(indexPath.row)")

cell.textLabel?.text = contacts[indexPath.row][0]

return cell

Here’s what happens:

  • First, we dequeue a cell with an identifier. It’s exactly the same identifier we used before, when registering the cell. That way the table view knows what type of cell we want. The dequeued cell is assigned to the cell constant. Now we have a table view cell to work with. More on dequeuing later.
  • Then, we print out some information to the Console. This is so we can see when this function is called, when the app runs.
  • Then, we assign the name of the contact to the text label of this table view cell. The contacts[indexPath.row][0] contains the value of the name of the contact, which we get to by using indexPath.row. Every instance of UITableViewCell has a property textLabel of UILabel, and every label of that type has a property text. You use it to set the text on the label.

Don’t worry, we’ll go over each of these things in more detail. First, see if you can run your app. Do you see contact names? Do you see the debug output in the Console? Try scrolling the app!

How To Working With Table View Controllers In Swift

When Is “tableView(_:cellForRowAt:)” Called?

If you ran the Contacts app, and played around with scrolling up and down, you can’t help but notice that every time you scroll, debug output shows up in the Console.

Every time a cell that wasn’t on screen before appears, the function tableView(_:cellForRowAt:) is called, and a new line appears in the Console.

So when is tableView(_:cellForRowAt:) called? Every time a table view cell needs to be shown on screen!

The table view controller has determined that a cell is needed, so it calls tableView(_:cellForRowAt:). Our implementation of that function dequeues a cell, changes it, and provides it back to the table view controller. The table view controller, and the UIKit framework, then renders it graphically on screen.

What Is An Index Path?

Every time the table view controller needs a cell from tableView(_:cellForRowAt:), it provides an index path as an argument for the function. Within the function body you can use the parameter indexPath to know exactly which cell the table view controller needs.

An index path is like an address, or a coordinate in a grid. A typical graph has an X axis and a Y axis, so you could express a coordinate in that graph as x, y like 0, 1 and 42, 3. Similarly, a spreadsheet has rows and columns with indices.

A table view uses sections and rows. As discussed before, you can use sections to group cells together. Our app only has one section, and it has contacts.count rows. The rows of the table view run from top to bottom.

Said differently: the sections and rows of a table view are what columns and rows are to a spreadsheet. An index path defines a location in the table view, by using a row and a section.

The rows and sections are represented by numbers, called indices. These indices start at zero, so the first row and section will have index number 0.

When you look back at the previous screenshot, it makes much more sense. The first cell has index path 0, 0, the second cell 0, 1, continuing up to the last visible cell with index path 0, 11.

The Table View Reuse Mechanism
What’s most noteworthy about the table view is its mechanism for cell reuse. It’s quite simple, actually.

  • Every time a table view controller needs to show a cell on screen, the function tableView(_:cellForRowAt:) is called, as we’ve discussed before.
  • Instead of creating a new table view cell every time that function is called, it picks off a previously created cell from a queue.
  • The cell resets to an empty state, clears its appearance, and the cell is customized again in tableView(_:cellForRowAt:).
  • Whenever a cell is scrolled off-screen, it’s not destroyed. It’s added to the queue, waiting to be reused.

It’s quite clever, right? Instead of creating and deleting cells, you simply reuse them. But… why?

It’s much less memory intensive to reuse cells. The table view controller would constantly write to memory when creating and deleting cells. Managing the memory would also be more intensive. When cells are reused, memory is used more efficiently, and less memory operations are needed.

Also, itt’s slightly less CPU intensive to reuse cells instead of creating and deleting them, because there simply are less operations involved in reusing, compared to creating and deleting cells.

When you’re quickly scrolling through a table view, you’re not seeing new cells – you’re seeing the same cells over and over again, with new information.

The code involved with cell reuse is this:

let cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier", for: indexPath)

The function dequeueReusableCell(withIdentifier:) attempts to dequeue a cell. When no cells are in the queue, it will create a cell for us. The identifier is used to keep every type of cell on their own queue, and to make sure the correct class is used to create new cells.

Learn how to code your own iOS apps by mastering Swift 5 and Xcode 11 » Find out how

Responding To User Interaction

One thing is missing from our table view controller: the ability to call people in our contacts list! But before we do that, let’s make sure you can also see a contact’s phone number in the table view controller.

The default UITableViewCell class has 4 different types, as expressed in the UITableViewCellStyle enumeration. You can choose between:

  • .default – a simple view with one line of black text
  • .value1 – a simple view with one line of black text on the left, and a small blue label on the right (used in the Settings app)
  • .value2 – a simple view with one line of black text on the right, and a small blue label on the left (used in the Contacts app)
  • .subtitle – a simple view with one line of black text, and a smaller line of gray text below it

Most developers use custom table view cells these days, so you won’t see these cell types that often. But they’re there!

We have to slightly adjust the code in tableView(_:cellForRowAt:). Replace the first line of the function with the following code:

var cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier")

if cell == nil
{
    cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cellIdentifier")
}

If you look closely, you’ll see that we’ve removed the for: indexPath part of the dequeueReusableCell(···) call. Instead, that function now returns an optional. When it can’t dequeue a cell, the function returns nil.

We then jump in ourselves to create the cell, if it’s nil. You see that in the second part of the code. You use a conditional if statement to check if cell is equal to nil, and if that’s true, you create the cell using the UITableViewCell(style:reuseIdentifier:) initializer.

That initializer gets two arguments, the cell style .subtitle, and the identifier we used earlier.

At this point we have a problem, because cell is an optional now! Its type is UITableViewCell?, but the function return type demands that we return an instance with non-optional type UITableViewCell.

Fortunately, this is one of those instances where we can safely use force unwrapping to unwrap the optional value. Because of the way our code is written, it’s impossible for cell to be nil beyond the conditional. You can guarantee that cell is not nil after the if statement.

Make sure to update the function to use force unwrapping for cell. Also, add the following line of code below cell!.textLabel ··· to set the subtitle of the cell show the phone number of the contact.

cell!.detailTextLabel?.text = contacts[indexPath.row][1]

The entire function now looks like this:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
    var cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier")

    if cell == nil {
        cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cellIdentifier")
    }

    print("\(#function) --- section = \(indexPath.section), row = \(indexPath.row)")

    cell!.textLabel?.text = contacts[indexPath.row][0]
    cell!.detailTextLabel?.text = contacts[indexPath.row][1]

    return cell!
}

Finally, make sure to remove the following line from viewDidLoad(). It’ll prevent the table view from initializing cells with the wrong type.

tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellIdentifier")

Mighty fine! Run your app with Command-R or the Play button, and check if it works. Do you see names and phone numbers? Good!

Then, for the piece-the-resistance, let’s add that user interaction function. Now that you’ve learned the intricacies of the table view controller, I think you already know how this next function works.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
    if let url = URL(string: "tel://" + contacts[indexPath.row][1])
    {
        UIApplication.shared.open(url)
    }
}

Again, we’re overriding the default implementation of the tableView(_:didSelectRowAt:) function. This function is called when a user taps the cell of a table view, and it belongs to the UITableViewDelegate protocol. Like the other functions, it’s provided the index path of the cell that’s tapped.

In the function body, we’re simply creating a tel:// URL from the phone number. We then tell the app to open that URL, which effectively tells iOS to initiate a call to this number. Note that this doesn’t work on iPhone Simulator, and that the numbers in our app are fake!

You can add the following code to the function if you want to check whether it works OK.

print("\(#function) --- Calling: \(contacts[indexPath.row][1])")

This will print out a debug message when you tap the cell of the table view.

Further Reading

And that’s all there is to it! It’s been quite a trip, but now you know how a table view controller works.

Want to learn more? Check out these resources:

Enjoyed this article? Please share it!

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.