iOS From Scratch With Swift: Table View Basics

Table views are among the most used components of the UIKit framework and are an integral part of the user experience on the iOS platform. Table views do one thing and they do it very well, presenting an ordered list of items. The UITableView class is a good place to continue our journey through the UIKit framework, because it combines several key concepts of Cocoa Touch and UIKit, including views, protocols, and reusability.

Data Source and Delegate

The UITableView class, one of the key components of the UIKit framework, is highly optimized for displaying an ordered list of items. Table views can be customized and adapted to a wide range of use cases, but the basic idea remains the same, presenting an ordered list of items.

A Plain Table View

The UITableView class is only responsible for presenting data as a list of rows. The data being displayed is managed by the table view's data source object, accessible through the table view's dataSource property. The data source can be any object that conforms to the UITableViewDataSource protocol, an Objective-C protocol. As we'll see later in this article, the table view's data source is often the view controller that manages the view the table view is a subview of.

Similarly, the table view is only responsible for detecting touches in the table view. It is not responsible for responding to the touches. The table view also has a delegate property. Whenever the table view detects a touch event, it notifies its delegate of that touch event. It's the responsibility of the table view's delegate to respond to the touch event.

By having a data source object managing its data and a delegate object handling user interaction, the table view can focus on data presentation. The result is a highly reusable and performant UIKit component that is in perfect alignment with the MVC (Model-View-Controller) pattern, which we discussed earlier in this series. The UITableView class inherits from UIView, which means that it's only responsible for displaying application data.

A data source object is similar, but not identical to, a delegate object. A delegate object is delegated control of the user interface by the delegating object. A data source object, however, is delegated control of data.

The table view asks the data source object for the data it should display. This implies that the data source object is also responsible for managing the data it feeds the table view.

Table View Components

The UITableView class inherits from UIScrollView, a UIView subclass that provides support for displaying content that is larger than the size of the application's window.

UITableView instance is composed of rows with each row containing one cell, an instance of UITableViewCell or a subclass thereof. In contrast to the counterpart of UITableView on OS X, NSTableView, instances of UITableView are one column wide. Nested data sets and hierarchies can be displayed by using a combination of table views and a navigation controller (UINavigationController). We discuss navigation controllers in the next article of this series.

I already mentioned that table views are only in charge of displaying data, delivered by the data source object, and detecting touch events, which are routed to the delegate object. A table view is nothing more than a view managing a number of subviews, the table view cells.

A New Project

Instead of overloading you with theory, it's better—and more fun—to create a new Xcode project and show you how to set up a table view, populate it with data, and have it respond to touch events.

Open Xcode, create a new project (File > New > Project...), and select the Single View Application template.

A New Project

Name the project Table Views, assign an organization name and identifier, and set Devices to iPhone. Tell Xcode where you want to save the project and hit Create.

Project Configuration

The new project should look familiar, because we chose the same project template earlier in this series. Xcode has already created an application delegate class for us, AppDelegate, and it also gave us a view controller class to start with, ViewController.

Adding a Table View

Build and run the project to see what we're starting with. The white screen that you see when you run the application in the simulator is the view of the view controller that Xcode instantiated for us in the storyboard.

The easiest way to add a table view to the view controller's view is in the project's main storyboard. Open Main.storyboard and locate the Object Library on the right. Browse the Object Library and drag a UITableView instance to the view controller's view.

Adding a Table View

If the dimensions of the table view don't automatically adjust to fit the bounds of the view controller's view, then manually adjust its dimensions by dragging the white squares at the edges of the table view. Remember that the white squares are only visible when the table view is selected.

Adjusting the Dimensions of the Table View

Add the necessary layout constraints to the table view to make sure the table view spans the width and height of its parent view. This should be easy if you've read the previous article about Auto Layout.

Adding Layout Constraints

This is pretty much all that we need to do to add a table view to our view controller's view. Build and run the project to see the result in the simulator. You will still see a white view since the table view doesn't have any data to display yet.

A table view has two default styles, plain and grouped. To change the current style of the table view (Plain), select the table view in the storyboard, open the Attributes Inspector, and change the style attribute to Grouped. For this project, we'll work with a plain table view so make sure to switch the table view's style back to plain.

A Grouped Table View

Connecting Data Source and Delegate

You already know that a table view is supposed to have a data source and a delegate. At the moment, the table view doesn't have a data source or a delegate. We need to connect the dataSource and delegate outlets of the table view to an object that conforms to the UITableViewDataSource and UITableViewDelegate protocols.

In most cases, that object is the view controller that manages the view which the table view is a subview of. Select the table view in the storyboard, open the Connections Inspector on the right, and drag from the dataSource outlet (the empty circle on its right) to the View Controller. Do the same for the delegate outlet. Our view controller is now wired up to act as the data source and the delegate of the table view.

Connecting Data Source and Delegate

If you run the application as is, it will crash almost instantly. The reason for this will become clear in a few moments. Before taking a closer look at the UITableViewDataSource protocol, we need to update the ViewController class.

The data source and delegate objects of the table view need to conform to the UITableViewDataSource and UITableViewDelegate protocol respectively. As we saw earlier in this series, protocols are listed after the superclass of the class. Multiple protocols are separated by commas.

Creating the Data Source

Before we start implementing the methods of the data source protocol, we need some data to display in the table view. We'll store the data in an array so let's first add a new property to the ViewController class. Open ViewController.swift and add a property, fruits, of type [String].

In the view controller's viewDidLoad() method, we populate the fruits property with a list of fruit names, which we'll display in the table view a bit later. The viewDidLoad() method is automatically invoked after the view controller's view and its subviews are loaded into memory hence the name of the method. It's therefore a good place to populate the fruits array.

The UIViewController class, the superclass of the ViewController class, also defines a viewDidLoad() method. The ViewController class overrides the viewDidLoad() method defined by the UIViewController class. This is indicated by the override keyword.

Overriding a method of a superclass is never without risk. What if the UIViewController class does some important things in the viewDidLoad() method? How do we make sure that we don't break anything when we override the viewDidLoad() method?

In situations like this, it's important to first invoke the viewDidLoad() method of the superclass before doing anything else in the viewDidLoad() method. The keyword super refers to the superclass and we send it a message of viewDidLoad(), invoking the viewDidLoad() method of the superclass. This is an important concept to grasp so make sure that you understand this before moving on.

Data Source Protocol

Because we assigned the view controller as the data source object of the table view, the table view asks the view controller what it should display. The first piece of information the table view wants from its data source is the number of sections it should display.

The table view does this by invoking the numberOfSectionsInTableView(_:) method on its data source. This is an optional method of the UITableViewDataSource protocol. If the table view's data source doesn't implement this method, the table view assumes that it needs to display only one section. We implement this method anyway since we're going to need it later in this article.

You may be wondering "What is a table view section?" A table view section is a group of rows. The Contacts application on iOS, for example, groups contacts based on the first letter of the first or last name. Each group of contacts forms a section, which is preceded with a section header at the top of the section and/or a section footer at the bottom of the section.

The numberOfSectionsInTableView(_:) method accepts one argument, tableView, which is the table view that sent the message to the data source object. This is important, because it allows the data source object to be the data source of multiple table views if necessary. As you can see, the implementation of numberOfSectionsInTableView(_:) is quite easy.

Now that the table view knows how many sections it needs to display, it asks its data source how many rows each section contains. For each section in the table view, the table view sends the data source a message of tableView(_:numberOfRowsInSection:). This method accepts two arguments, the table view sending the message and the section index of which the table view wants to know the number of rows.

The implementation of this method is pretty simple as you can see below. We start by declaring a constant, numberOfRows, and assign it the number of items in the fruits array by calling count on the array. We return numberOfRows at the end of the method.

The implementation of this method is so easy that we might as well make it a bit more concise. Take a look at the implementation below to make sure that you understand what has changed.

If we try to compile the project in its current state, the compiler will throw an error. The error tells us that the ViewController class doesn't conform to the UITableViewDataSource protocol, because we haven't implemented the required methods of the protocol yet. The table view expects the data source, the ViewController instance, to return a table view cell for each row in the table view.

We need to implement tableView(_:cellForRowAtIndexPath:), another method of the UITableViewDataSource protocol. The name of the method is pretty descriptive. By sending this message to its data source, the table view asks its data source for the table view cell of the row specified by indexPath, the second argument of the method.

Before continuing, I'd like to take a minute to talk about the NSIndexPath class. As the documentation explains, "The NSIndexPath class represents the path to a specific node in a tree of nested array collections." An instance of this class can hold one or more indices. In the case of a table view, it holds an index for the section an item is in and the row of that item in the section.

A table view is never more than two levels deep, the first level being the section and the second level being the row in the section. Even though NSIndexPath is a Foundation class, the UIKit framework adds a handful of extra methods to the class that make working with table views easier. Let's inspect the implementation of the tableView(_:cellForRowAtIndexPath:) method.

Reusing Table View Cells

Earlier in this series, I told you that views are an important component of an iOS application. But you should also know that views are expensive in terms of the memory and processing power they consume. When working with table views, it's important to reuse table view cells as much as possible. By reusing table view cells, the table view doesn't have to initialize a new table view cell from scratch every time a new row needs a table view cell.

Table view cells that move off-screen are not discarded. Table view cells can be marked for reuse by specifying a reuse identifier during initialization. When a table view cell that is marked for reuse moves off-screen, the table view puts it into a reuse queue for later use.

When the data source asks its table view for a new table view cell and specifies a reuse identifier, the table view first inspects the reuse queue to check if a table view cell with the specified reuse identifier is available. If no table view cell is available, the table view instantiates a new one and passes it to its data source. That is what happens in the first line of code.

The table view's data source asks the table view for a table view cell by sending it a message of dequeueReusableCellWithIdentifier(_:forIndexPath:). This method accepts the reuse identifier I mentioned earlier as well as the index path of the table view cell.

The compiler will tell you that cellIdentifier is an "unresolved identifier". This simply means that we're using a variable or constant, which we havent declared yet. Above the declaration of the fruits property, add the following declaration for cellIdentifier.

How does the table view know how to create a new table view cell? In other words, how does the table view know what class to use to instantiate a new table view cell? The answer is simple. In the storyboard, we create a prototype cell and give it a reuse identifier. Let's do that now.

Creating a Prototype Cell

Open Main.storyboard, select the table view you added earlier, and open the Attributes Inspector on the right. The Prototype Cells field is currently set to 0. Create a prototype cell by setting it to 1. You should now see a prototype cell in the table view.

Creating a Prototype Cell

Select the prototype cell and take a look at the Attributes Inspector on the right. The Style is currently set to Custom. Change it to Basic. A basic table view cell is a simple table view cell containing one label. That's fine for the application we're building. Before we head back to the ViewController class, set Identifier to CellIdentifier. The value should be identical to the value assigned to the cellIdentifier constant we declared a moment ago.

Configuring the Prototype Cell

Configuring the Table View Cell

The next step involves populating the table view cell with the data stored in the fruits array. This means that we need to know what element to use from the fruits array. This in turn means that we somehow need to know the row or index of the table view cell.

The indexPath argument of the tableView(_:cellForRowAtIndexPath:) method contains this information. As I mentioned earlier, it has a few extra methods for making working with table views easier. One of these methods is row, which returns the row for the table view cell. We fetch the correct fruit by asking the fruits array for item at indexPath.row, using Swift's convenient subscript syntax.

Finally, we set the text of the textLabel property of the table view cell to the fruit name we fetched from the fruits array. The UITableViewCell class is a UIView subclass and it has a number of subviews. One of these subviews is an instance of UILabel and we use this label to display the name of the fruit in the table view cell. Whether or not the textLabel property is nil depends on the style of the UITableViewCell. That is why the textLabel property is followed by a question mark. This is better known as optional chaining.

The tableView(_:cellForRowAtIndexPath:) method expects us to return an instance of the UITableViewCell class (or a subclass thereof) and that is what we do at the end of the method.

Run the application. You should now have a fully functional table view populated with the array of fruit names stored in the view controller's fruits property.

Sections

Before we take a look at the UITableViewDelegate protocol, I want to modify the current implementation of the UITableViewDataSource protocol by adding sections to the table view. If the list of fruits were to grow over time, it would be better and more user friendly to sort the fruits alphabetically and group them into sections based on the first letter of each fruit.

If we want to add sections to the table view, the current array of fruit names won't suffice. Instead, the data needs to be broken up into sections with the fruits in each section sorted alphabetically. What we need is a dictionary. Start by declaring a new property, alphabetizedFruits, of type [String: [String]] in the ViewController class.

In viewDidLoad(), we use the fruits array to create a dictionary of fruits. The dictionary should contain an array of fruits for each letter of the alphabet. We omit a letter from the dictionary, if there are no fruits for that particular letter.

The dictionary is created with the help of a helper method, alphabetizeArray(_:). It accepts the fruits array as an argument. The alphabetizeArray(_:) method might be a bit overwhelming at first glance, but its implementation is actually pretty straightforward.

We create a mutable dictionary, result of type [String: [String]], which we use to store the arrays of alphabetized fruits, one array for each letter of the alphabet. We then loop through the items of array, the method's first argument, and extract the first letter of the item's name, making it uppercase. If result already contains an array for the letter, we append the item to that array. If it doesn't, we create an array with the item in it and add it to result.

The items are now grouped based on their first letter. However, the groups aren't alphabetized. That's what happens in the second for loop. We loop through result and sort each array alphabetically.

Don't worry if the implementation of alphabetizeArray(_:) isn't entirely clear. In this tutorial, we focus on table views, not on creating an alphabetized list of fruits.

Number of Sections

With the new data source in place, the first thing we need to do is update the implementation of numberOfSectionsInTableView(_:). In the updated implementation, we ask the dictionary, alphabetizedFruits, for its keys. This gives us an array that contains every key of the dictionary. The number of items in keys equals the number of sections in the table view. It's that simple.

We also need to update tableView(_:numberOfRowsInSection:). As we did in numberOfSectionsInTableView(_:), we ask alphabetizedFruits for its keys and sort the result. Sorting the array of keys is important, because the key-value pairs of a dictionary are unordered. This is a key difference between arrays and dictionaries. This is something that often trips up people who are new to programming.

We then fetch the key from sortedKeys that corresponds with section, the second parameter of tableView(_:numberOfRowsInSection:). We use the key to fetch the array of fruits for the current section, using optional binding. Finally, we return the number of items in the resulting array of fruits.

The changes we need to make to tableView(_:cellForRowAtIndexPath:) are similar. We only change the way we fetch the fruit name the table view cell displays in its label.

If you were to run the application, you wouldn't see any section headers like the ones you see in the Contacts application. This is because we need to tell the table view what it should display in each section header.

The most obvious choice is to display the name of each section, that is, a letter of the alphabet. The easiest way to do this is by implementing tableView(_:titleForHeaderInSection:), another method defined in the UITableViewDataSource protocol. Take a look at its implementation below. It's similar to the implementation of tableView(_:numberOfRowsInSection:). Run the application to see what the table view looks like with sections.

Delegation

In addition to the UITableViewDataSource protocol, the UIKit framework also defines the UITableViewDelegate protocol, the protocol to which the table view's delegate needs to conform.

In the storyboard, we already set the view controller as the delegate of the table view. Even though we haven't implemented any of the delegate methods defined in the UITableViewDelegate protocol, the application works just fine. This is because every method of the UITableViewDelegate protocol is optional.

It would be nice to be able to respond to touch events, though. Whenever a user touches a row, we should be able to print the name of the corresponding fruit to Xcode's console. Even though this isn't very useful, it will show you how the delegate pattern works.

Implementing this behavior is easy. All we have to do is implement the tableView(_:didSelectRowAtIndexPath:) method of the UITableViewDelegate protocol.

Fetching the name of the fruit that corresponds to the selected row should be familiar by now. The only difference is that we print the fruit's name to Xcode's console.

It might surprise you that we use the alphabetizedFruits dictionary to look up the corresponding fruit. Why don't we ask the table view or the table view cell for the name of the fruit? That's a very good question. Let me explain what happens.

A table view cell is a view and its sole purpose is displaying information to the user. It doesn't know what it's displaying other than how to display it. The table view itself doesn't have the responsibility to know about its data source, it only knows how to display the sections and rows that it contains and manages.

This example is another good illustration of the separation of concerns of the Model-View-Controller (MVC) pattern that we saw earlier in this series. Views don't know anything about application data apart from how to display it. If you want to write reliable and robust iOS applications, it's very important to know about and respect this separation of responsibilities.

Conclusion

Table views are not that complicated once you understand how they behave and know about the components involved, such as the data source and delegate objects the table view talks to.

In this tutorial, we only saw a glimpse of what a table view is capable of. In the rest of this series, we will revisit the UITableView class and explore a few more pieces of the puzzle. In the next installment of this series, we take a look at navigation controllers.

If you have any questions or comments, you can leave them in the comments below or reach out to me on Twitter.

Tags:

Comments

Related Articles