Introduction to Table Views

In this tutorial, we are going to work with the UITableView class to create a list view for an iOS app. In most cases, when you see a list in an app on your iPhone or iPhone, it uses UITableView to display the list. We will use Swift with Xcode 11 and setup the table view programmatically. You could also create the table view in a storyboard.

If you've used other programming technologies to display a list, you might expect that all you need to do is to create an array or list of items you want to display, and then add them to the table view, one by one. While this approach would work for user interfaces that only display a limited amount of information, the iOS table view uses a different approach that optimizes for speed and memory usage. This was very important when the first iPhone came out, and is somewhat less important now with modern iPhones. The approach is still the same, however.

We will be building the basic table view application shown in the below screenshot - it's not much to look at, but the concepts are important!

Basic UITableView Project


Table View Concepts

A table view (UITableView) displays table cells (UITableViewCell). Each row in the table is a cell, but not all rows have to be the same type of cell.

For each row, you can use the default class, UITableViewCell, or you can create your own custom table view class that extends the UITableViewCell class.

In this tutorial, we will be only be using the default UITableViewCell. We will add a separate tutorial to cover customizing table view cells - there's a lot to cover with table views.

The table view needs to have a data source to display any data, and a delegate if your app does something when a user selects one of the rows. Both of these are optional, but a table view without a data source is empty.

The table view's data source needs to adopt the UITableViewDataSource protocol, and in particular, needs to implement two required methods - one that returns the number of rows in a section, and one that provides a table view cell for any given row.

The delegate should adopt the UITableViewDelegate method, and all methods are optional. The most common method to implement in the delegate determines what happens when a user taps one of the rows in the table view.

We will be setting up a data source, and a delegate in this tutorial. In both cases, we will implement these as extensions to the ViewController class that comes with a default Swift iOS UIKit/Storyboard project in Xcode.

Let's start out by displaying an empty table view in our view.


Programmatically Displaying an Empty Table View

To setup the table view, we'll add a function named setupTableView() to our ViewController class, and then call that function from the viewDidLoad() method. This makes it easier to organize our code in the future, if we need to setup several different views.

We will store the table view as an instance variable named tableView on the ViewController class.

Inside the setupTableView() method, we create a new instance of UITableView, and assign its frame to be the view controller's view's bounds. This means that the table view will immediately occupy all of the space within the screen. You could customize this by leaving space for a toolbar, or a navigation bar, or any other user interface elements you need.

Once you make your changes to your ViewController class, build and run your project - you will see an empty table view in your app.

In the next section, we will set up Auto Layout constraints for the table view.

import UIKit

class ViewController: UIViewController {
    
    var tableView:UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        setupTableView()
    }

    func setupTableView() {
        tableView = UITableView(frame: view.bounds)
        view.addSubview(tableView)    
    }
}

Adding Auto Layout Constraints

We use Auto Layout constraints to anchor each side of the table view to the parent view. We keep things simple here and activate basic constraints with no margins.

func setupTableView() {
    tableView = UITableView(frame: view.bounds)
    view.addSubview(tableView)

    tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
    tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
    tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
    tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true        
}

Displaying Table Rows

The setup above programmatically displays the table view on the screen. We now need to switch focus to displaying content in the table view.

We'll use an array of animal names (as strings) as our data to display. You could use other data structures here, such as an array of objects, depending on what you are trying to show in the table view. A typical iOS developer coding exercise for an interview would be to retrieve JSON data from an API, and then display that data in a table view.

For our purposes, we will add a constant named animalNames to our ViewController class that holds some animal names - you could use cities, or musicians, or the names of your friends if you want. Just use a different name for the constant so you don't confuse yourself!

class ViewController: UIViewController {

    ...
    
    let animalNames = ["Dogs", "Cats", "Rabbits", "Horses"]
 
    ...
}

The next step is to setup cell reuse identifiers for our project - we only have type of cell for this basic project, so it is pretty straight forward.


Cell Reuse Identifiers

For performance reasons, cell reuse identifiers are essential for table views. You may have thousands of rows of data that could be in your table view, but the table view will only show 10, 15, or 20 rows of data at a time to the user. Keeping hundreds or thousands of table cell views in memory would be wasteful, and your project would be slow to load if it had to instantiate all of those views before displaying anything to the user.

Instead, the table view recycles table cell views for you. As a user scrolls through a table view, views that are no longer displaying to the user can be reused. Your application will need to replace the old data - the text, images, and anything else that changes from row to row - but the underlying labels, images, or other subviews in the cell remain in place.

For all of this to work, each type of cell in a table view needs a unique identifier (as a string).

These unique identifiers get used in two places. The first is when we register a specific table view cell class for a type of table row. The second is when we create or reuse a table view cell for a table row.

func setupTableView() {
    ...

    tableView.register(UITableViewCell.self,
                           forCellReuseIdentifier: "AnimalCell")     
}

Because we are using the basic, default UITableViewCell class for our rows, we register this class with the table view for the identifier "AnimalCell". If we had created our own custom table view class, we would have registered that class here (for instance, NameTableViewCell).

The second place we use the cell reuse identifier is in the table view's data source.


UITableViewDataSource

The UITableViewDataSource protocol supplies the content for the table view - the number of sections (defaults to 1), the number of rows in each section, and a table view cell for each row.

You could adopt this protocol on your ViewController class, or create another Swift class that implements this protocol. We are going to implement the protocol as an extension to the ViewController class.

There are two required methods, and we implement both of them. The first method returns the number of rows for a section. We don't implement the method that returns the number of sections for the table view, so the default is that there is only one section.

The second method returns a table view cell. We dequeue a table view cell using the same reuse identifier that we used to register a table view class with the table view.

After getting the table view cell, we need to update its contents for piece of data that corresponds to that row.

extension ViewController : UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return animalNames.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "AnimalCell")! as UITableViewCell
        cell.textLabel?.text = animalNames[indexPath.row]
        return cell
    }
}
import UIKit

class ViewController: UIViewController {
    
    var tableView:UITableView!
    
    let animalNames = ["Dogs", "Cats", "Rabbits", "Horses"]

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        setupTableView()
    }

    func setupTableView() {
        tableView = UITableView(frame: view.bounds)
        view.addSubview(tableView)

        tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        
        tableView.register(UITableViewCell.self,
                           forCellReuseIdentifier: "AnimalCell")
    }
}

Index Path


Complete Code Listing

import UIKit

class ViewController: UIViewController {
    
    var tableView:UITableView!
    
    let animalNames = ["Dogs", "Cats", "Rabbits", "Horses"]

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        setupTableView()
    }

    func setupTableView() {
        tableView = UITableView(frame: view.bounds)
        view.addSubview(tableView)
        
        tableView.register(UITableViewCell.self,
                           forCellReuseIdentifier: "AnimalCell")

        tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        
        tableView.dataSource = self
        tableView.delegate = self
    }
}

extension ViewController : UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return animalNames.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "AnimalCell")! as UITableViewCell
        cell.textLabel?.text = animalNames[indexPath.row]
        return cell
    }
}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        print(animalNames[indexPath.row])
    }
}
 

Try one of our App Challenges

We've come up with a list of app challenges - for everyone from coding beginners to seasoned developers. Build your portfolio!