Lesson 5 - Introduction to the important TableView component
In the previous lesson, Simple iOS calculator in Swift, we programmed a simple iOS calculator in
Swift. We used StackView
to place components on the form under or
next to each other.
In today's Swift tutorial, we're going to look at TableView
.
It's the base of many app and we probably can't avoid it. When we open a news
app, calls, notes, and many others, the first thing we'll see is
TableView
.
TableView
is an ideal way to present more data to the user, or a
data collection if you prefer this term. It can be a list of tasks, contacts,
music albums, etc. In these cases, TableView
is the obvious choice.
It allows us to display practically unlimited number of elements or objects
simply under each other and to handle scrolling through them or selecting
individual items. Removing is also a peace of cake.
Creating a project
Enough with theory, let's have a look at a simple way to start with
TableView
. Prepare either a new Xcode iOS app project (Single View
App) or use the one from previous lessons.
Now, look for TableView
(not Table View Controller, we'll get to
that later) in the object library and drop it into your controller located in
Main.storyboard
. Ideally, set constraints as well which could be,
in this case, 0
from all four sides.
Our TableView
is ready, but how do we display data in it? For
that, we need to visit the code, specifically ViewController.swift
.
First, we have to specify that this controller represents a data source and a
delegate for the TableView
.
Let's implement these two protocols in our controller:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
But that's not enough. Now we have to set the controller as the
DataSource
and Delegate
of our TableView
.
We need this to be able to choose the data source and also to react to events,
such as selecting a Cell
. We can even choose from different
approaches.
Connecting TableView to the controller using the mouse
The first approach is to set the DataSource
and
Delegate
directly in the UI designer. Just select the
TableView
and drag it while holding Ctrl or right-click
the ViewController
and select dataSource
. The same
goes for the delegate
. Or you could right-click the
TableView
in the component list and drop the
dataSource
and delegate
from there.
Now we basically told the TableView
that our controller is going
to react to the TableView
events and provide data at the same time.
This way, the TableView
can notify us if the user selected an item
or performed other actions.
Connecting TableView to the controller in the code
The second approach is to set DataSource
and
Delegate
in the viewDidLoad()
method in our code. I
personally prefer this approach, I can see the connection and I always know that
I haven't forgot it. If there's some error related to TableView
,
the first thing you should do is to check you connected it properly.
First, you have to create an Outlet
for your
TableView
, which we learned last time. Then, you just have to set
your class (i.e. the controller) to dataSource
and
delegate
.
override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self tableView.delegate = self }
The necessary preparations are done, let's display something in the
TableView
.
Protocols implementation
You can't build the app yet, because we've specified some protocols our class
is going to implement, but we haven't added any code yet. Swift is expecting
that our class is able to provide data for the TableView
. For
starters, let's just add these two methods:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { }
The first method returns how many rows we have. Let's keep it simple and have only one section for now. The second method is responsible for returning the cell according the a given row (cell) index.
Because both functions starts the same, in the first case, start
typing numberOfRowsIn...
and Xcode will automatically complete the
right method. This is exactly why there are two different parameter names. The
first is used to identify the method from the "outside" and the second is used
in the method body. Let Xcode autocomplete the second method the same way, by
typing cellForRow...
.
Preparing data
Let's create a simple array which will represent to-do list items. The best practice is to declare our variables and constants right below the class name.
var todos = ["Buy coffee", "Take out the trash", "Netflix and chill"]
List anything you want. The purpose is to have any array with some values for
our TableView
to display. Now let's modify our two methods:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return todos.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell() cell.textLabel?.text = todos[indexPath.row] return cell }
Easy stuff. The first method returns the number of items, and the second creates a cell and sets it the text depending on its index. The texts are extracted from our array. You can try to run the app, you'll see all the items listed under each other:
TableView under the hood
Remember that this is not what the method with cellForRowAt
should look like. I just wanted to show you a functional TableView
as quickly as possible. Now, let's have a look at how TableView
works internally.
Scrolling through the items is very smooth even with hundreds of cells. It's
because TableView
is a smart component and keeps only the cells
that need to be displayed. We can simply say that it "recycles" the cells. An
important method is needed for this:
let recycledCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
There are actually two of these methods. The second one doesn't have the second parameter and you shouldn't use it at all. It's a deprecated alternative, that is there only for backwards compatibility.
Maybe you noticed the withIdentifier
parameter which we have to
set to make everything work. But before that, we have to prepare our
TableView
. Open the designer and after selecting the component, set
the Prototype Cells
attribute in the
Attribute Inspector
to 1
.
Then, we have to select this prototype cell which will show up in the preview. You can click it directly in the preview or in the component list of this controller on the left.
Finally, we just set the Identifier
in the
Attribute Inspector
to "cell", which we already use in the code
above. Of course, you can choose anything else.
Now run the app, you should see the to-do list.
Why all this work? We've just used the TableView
correctly. The
component is now ready to display hundreds of rows. Because only the rows being
displayed actually exist, a large amount of data won't make the app slower. This
isn't a concern in our case, but at least we showed the correct solution.
There's also a second benefit. We're on the right track to create a more
complex cell, or a TableView
row. It's what the
Prototype Cell
allows us. We'd simply drop the needed components
and create a special class. We'll discuss that later in this course.
Selecting and deleting items
Now let's have a look at how to select and delete individual items in
TableView
.
Selecting items
We are provided with a method for the situation when the user has selected an item:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { }
Again, we can just start typing didSelect...
and Xcode will
autocomplete the method. For now, we won't do anything fancy when selecting a
row, let's just print the selected text (into the Output window in Xcode), the
selected to-do in our case.
We'll add calling the print()
function to the method. We'll know
which row has been selected though the row
property of the
indexPath
parameter. We'll use it as the index for our
todos[]
array:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(todos[indexPath.row])
}
If you select a row now, the selected to-do is printed to the console.
Finally, we'll finish the introduction to TableView
by deleting its
items.
Deleting items
We are, again, provided with a method for deleting items:
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { }
By adding this method we basically say we want to modify the
TableView
. If you swipe left now, a delete button should appear
next to individual rows. But it won't work just yet.
Using the editingStyle
parameter, we'll determine whether it's a
deleting action. If so, we'll delete this item. First, we'll remove it from our
todos[]
source array, then from the TableView
where we
can choose an animation as well. The code would look like this:
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { todos.remove(at: indexPath.row) tableView.deleteRows(at: [indexPath], with: .fade) } }
You can choose whatever animation you want. The .automatic
option is often used to make things easier and consistent.
Static TableView
Finally, let's have a look at how to create a TableView
if we
know exactly what cells, or sections, we're going to have. If can be, for
example, a menu or any other situation that doesn't require reading from any
collection. This approach is useful when we want to have a more complex settings
in our app. iOS uses TableView
for the device settings on every
step.
Drop Table View Controller
into the project, it's necessary for
a static TableView
. The only thing we have to do now is to select
the TableView
and set Content
to
Static Cells
in the Attributes Inspector. Then you can set the
number of sections. Every section can have its own title and footer. Select the
sections and set everything needed, including the individual rows.
The cells don't have to be single text lines only. You can set
Style
of individual cells and have e.g. a label and a sublabel, add
an image and so on. The Accessory
property is often set as well
(again, in Attributes Inspector). This property can, for example, display an
arrow on the left side of the cell, informing the user that the option leads to
another screen, and so on.
We also need a new class for our new controller. We'll add a new file, but
choose Cocoa Touch Class
instead of a Swift file. A dialog pops up.
In it, we'll set the new class to be a subclass of
UITableViewController
and name it e.g.
SettingsTableViewController
. Now, we'll just select our new
TableViewController
in Main.storyboard
and set its
Class
to SettingsTableViewController
in
Identity Inspector
.
We'll finish our static TableView
by reacting to selecting a
cell. We don't have to deal with DataSource
or
Delegate
in the SettingsTableViewController
, because
all that is handled by the UITableViewController
class. So we only
have to implement the didSelectRowAt
method. We'll mark the method
by override
so we can provide our own implementation:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { print("Selected row \(indexPath.row) in section \(indexPath.section)") }
Again, let's just print something to test it. In a real-life app, a
switch
would be quite useful (one of few places where it's actually
useful), especially if you had a lot of sections and rows. In this method,
switch
could only handle sections and the individual sections would
have its own methods, such as handleFirstSection(row: Int)
and so
on. It could look like this:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { print("Selected row \(indexPath.row) in section \(indexPath.section)") switch indexPath.section { case 0: handleFirstSection(rowIndex: indexPath.row) case 1: handleSecondSection(rowIndex: indexPath.row) default: break } } func handleFirstSection(rowIndex: Int) { switch rowIndex { case 0: // Show account detail break case 1: // Navigate to settings break default: break } } func handleSecondSection(rowIndex: Int) { }
Now we finished the introduction to the TableView
component.
We'll use this new component to create an actual TODO app, including a
database.
In the next lesson, Don't reinvent the wheel, use CocoaPods, we'll learn how to use CocoaPods that allows us to use a package system and to install libraries easily.