Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

20. Table Views
Written by Eli Ganim

Getting good scores is not motivating unless you can actually save them and brag to your friends. In this chapter you’ll learn how to save the high scores and present them.

This is how the screen will look like when you’re finished:

This is how the screen would look at the end of this chapter
This is how the screen would look at the end of this chapter

This chapter covers the following:

  • Table views and navigation controllers: A basic introduction to navigation controllers and table views.
  • Add a table view: Create your first UIKit table view and add a prototype cell to display data.
  • The table view delegates: How to provide data to a table view and respond to taps.

Table views and navigation controllers

This screen will introduce you to two of the most commonly used UI elements in iOS apps: the table view and the navigation controller.

A table view is UIKit’s equivalent of SwiftUI’s List. This component is extremely versatile and the most important one to master in iOS development.

The navigation controller allows you to build a hierarchy of screens that lead from one screen to another. It adds a navigation bar at the top with a title and a back button.

In this screen, tapping an entry slides in the screen containing the information about the high score, like who made it and when it was achieved. Navigation controllers and table views are often used together.

The grey bar at the top is the navigation bar. The list of items is the table view.
The grey bar at the top is the navigation bar. The list of items is the table view.

Adding a table view

As table views are so important, you will start out by examining how they work.

Because smart developers split up the workload into small, simple steps, this is what you’re going to do in this chapter:

  1. Put a table view on the app’s screen.
  2. Put data into that table view.
  3. Allow the user to tap a row in the table to show when that high score was reached.

Once you have these basics up and running, you’ll keep adding new functionality over the next few chapters until you end up with a fully working high scores screen.

Creating a new screen

➤ Go to Xcode’s File menu and choose New ▸ File…

➤ Choose the Cocoa Touch Class template. Click Next. Call the file HighScoresViewController and make it a subclass of UITableViewController.

A table view controller is a special type of view controller that makes working with table views easier.

➤ Click on Main.storyboard to open Interface Builder and drag a new Table View Controller from the Objects Library into the storyboard.

➤ Select the View Controller of the new scene you just added. Open the Identity Inspector from the right pane and update the class name to HighScoresViewController.

The name of the scene in the Document Outline on the left should change to “High Scores View Controller Scene”. As its name implies, and as you can see in the storyboard, the view controller contains a Table View object. We’ll go into the difference between controllers and views soon, but for now, remember that the controller is the whole screen while the table view is the object that actually draws the list.

Connecting the new view controller

Right now there’s no way to reach the new screen you just added. In order to fix that, you’ll add a new button to the main screen of the game.

➤ Open the storyboard and add a new button to the main screen, just above the about button.

➤ Use this settings for the button. Type: Custom; Background: SmallButton.

➤ Change the text inside the button to be a trophy symbol: 🏆. You can find it under EditEmoji & Symbols and then search for the word ‘Trophy’.

➤ Set the button size to be 32x32.

The arrow points at the initial view controller
The arrow points at the initial view controller

Now you’re going to hook this button up to the high scores screen.

➤ Click the 🏆 button to select it. Then hold down Control and drag over to the High Scores screen.

➤ Let go of the mouse button and a pop-up appears with several options. Choose Show.

➤ Run the app on the Simulator and click on the trophy button.

You should see an empty list. This is the table view. You can drag the list up and down but it doesn’t contain any data yet.

By the way, it doesn’t really matter which Simulator you use. Table views resize themselves to fit the dimensions of the device, and the app will work equally well on the small iPhone 8 or the huge iPhone X.

Note: When you build the app, Xcode gives the warning “Prototype table cells must have reuse identifiers.” Don’t worry about this for now, you’ll fix it soon.

The anatomy of a table view

First, let’s talk a bit more about table views. A UITableView object displays a list of items.

There are two styles of tables: “plain” and “grouped.” They work mostly the same, but there are a few small differences. The most visible difference is that rows in the grouped style table are placed into boxes (the groups) on a light gray background.

A plain-style table (left) and a grouped table (right)
A plain-style table (left) and a grouped table (right)

Note: I’m not sure why it’s named a table, because a table is commonly thought of as a spreadsheet-type object that has multiple rows and multiple columns, whereas the UITableView only has rows. It’s more of a list than a table, but I guess we’re stuck with the name now. UIKit also provides a UICollectionView object that works similar to a UITableView but allows for multiple columns.

The plain style is used for rows that all represent something similar, such as contacts in an address book where each row contains the name of one person.

The grouped style is used when the items in the list can be organized by a particular attribute, like book categories for a list of books. The grouped style table could also be used to show related information which doesn’t necessarily have to stand together — like the address information, contact information, and e-mail information for a contact.

You will use both table styles in the upcoming chapters.

The data for a table comes in the form of rows. You can potentially have many rows (even tens of thousands) but that kind of design isn’t recommended. Most users will find it incredibly annoying to scroll through ten thousand rows to find the one they want. And who can blame them?

Tables display their data in cells. A cell is related to a row but it’s not exactly the same. A cell is a view that shows a row of data that happens to be visible at that moment. If your table can show 10 rows at a time on the screen, then it only has 10 cells, even though there may be hundreds of rows of actual data.

Whenever a row scrolls off the screen and becomes invisible, its cell will be re-used for a new row that becomes visible.

Cells display the contents of rows
Cells display the contents of rows

Adding a prototype cell

Xcode has a very handy feature named prototype cells that lets you design your cells visually in Interface Builder. ➤ Open the storyboard and click the empty cell (the white row below the Prototype Cells label) to select it.

Selecting the prototype cell
Selecting the prototype cell

Sometimes it can be hard to see exactly what is selected, so keep an eye on the Document Outline to make sure you’ve picked the right thing. (Or use the Document Outline to select the cell directly.)

➤ Drag a Label from the Objects Library on to the white area in the table view representing the cell. Make sure the label spans from the left edge (with a small margin) until the middle of the cell.

➤ Drag another Label and place it next to the previous label, so it spans from it’s right edge to the cell’s right edge.

➤ In the Attributes inspector, change the alignment to Right.

The result should look similar to this:

Adding the label to the prototype cell
Adding the label to the prototype cell

Note: If you simply drag the label on to the table view, it might not work. You need to drag the label on to the cell itself. You can check where the label ended up using the Document Outline. It has to be inside the Content View for the table view cell.

You also need to set a reuse identifier on the cell. This is an internal name that the table view uses to find free cells to reuse when rows scroll off the screen and new rows must become visible.

The table needs to assign cells for those new rows, and recycling existing cells is more efficient than creating new cells. This technique is what makes table views scroll smoothly.

Reuse identifiers are also important for when you want to display different types of cells in the same table. For example, one type of cell could have an image and a label and another could have a label and a button. You would give each cell type its own identifier, so the table view can assign the right cell for a given row type.

This screen has only one type of cell but you still need to give it an identifier.

➤ Type HighScoreItem into the Table View Cell’s Identifier field (you can find this in the Attributes inspector).

Giving the table view cell a reuse identifier
Giving the table view cell a reuse identifier

Compiler warnings

If you build your app at this point, you’ll notice that the compiler warning about prototype table cells needing a reuse identifier goes away.

But… you’ve got a new warning — one about views without any layout constraints clipping or overlapping other views. Sounds familiar?

Yes, this is the same warning you saw previously when you had views without any Auto Layout constraints! And you know how to find the affected views now, right?

➤ In the storyboard, click on the yellow warning circle for the table view to see the list of views with issues. It is the new label you just added to the prototype table cell.

That’s simple enough to fix, right? Simply select the label, select the Add New Constraints icon at the bottom of the Interface Builder window, and add 4 constraints for the left, top, right, and bottom of the label. (You can go with the current defaults as long as you have the label positioned correctly.)

➤ Run the app and you’ll see… nothing — exactly the same as before. The table is still empty.

This is because you only added a cell design to the table, not actual data. Remember that the cell is just the visual representation of the row, not the actual data. To add data to the table, you have to write some code.

The table view delegates

➤ Switch to HighScoresViewController.swift and add the following methods just before the closing bracket at the bottom of the file:

// MARK:- Table View Data Source
override func tableView(_ tableView: UITableView,
      numberOfRowsInSection section: Int) -> Int {
  return 1
}

override func tableView(_ tableView: UITableView, 
             cellForRowAt indexPath: IndexPath) -> 
             UITableViewCell {
  let cell = tableView.dequeueReusableCell(
                        withIdentifier: "HighScoreItem", 
                                   for: indexPath)
  return cell
}

These methods look a bit more complicated than the ones you’ve seen in Bullseye, but that’s because each takes two parameters and returns a value to the caller. Other than that, they work the same way as the methods you’ve dealt with before.

Protocols

The above two methods are part of UITableView’s data source protocol.

The data source is the link between your data and the table view. Usually, the view controller plays the role of data source and implements the necessary methods. So, essentially, the view controller is acting as a delegate on behalf of the table view. (This is the delegate pattern that we’ve talked about before — where an object does some work on behalf of another object.)

The table view needs to know how many rows of data it has and how it should display each of those rows. But you can’t simply dump that data into the table view’s lap and be done with it. You don’t say: “Dear table view, here are my 100 rows, now go show them on the screen.”

Instead, you say to the table view: “This view controller is now your data source. You can ask it questions about the data anytime you feel like it.”

Once it is hooked up to a data source – i.e. your view controller – the table view sends a numberOfRowsInSection message to find out how many data rows there are.

And when the table view needs to draw a particular row on the screen it sends a cellForRowAt message to ask the data source for a cell.

You see this pattern all the time in iOS: one object does something on behalf of another object. In this case, the HighScoresViewController works to provide the data to the table view, but only when the table view asks for it.

The dating ritual of a data source and a table view
The dating ritual of a data source and a table view

Your implementation of tableView(_:numberOfRowsInSection:) – the first method that you added – returns the value 1. This tells the table view that you have just one row of data.

The return statement is very important in Swift. It allows a method to send data back to its caller. In the case of tableView(_:numberOfRowsInSection:), the caller is the UITableView object and it wants to know how many rows are in the table.

The statements inside a method usually perform some kind of computation using instance variables and any data received through the method’s parameters. When the method is done, return says, “Hey, I’m done. Here is the answer I came up with.” The return value is often called the result of the method.

For tableView(_:numberOfRowsInSection:) the answer is really simple: there is only one row, so return 1. Now that the table view knows it has one row to display, it calls the second method you added – tableView(_:cellForRowAt:) – to obtain a cell for that row. This method grabs a copy of the prototype cell and gives that back to the table view, again with a return statement.

Inside tableView(_:cellForRowAt:) is also where you would normally put the row data into the cell, but the app doesn’t have any row data yet.

➤ Run the app and go to the high scores screen. You’ll see there is a single cell in the table:

The table now has one row
The table now has one row

Method signatures

In the above text, you might have noticed some special notation for the method names, like tableView(_:numberOfRowsInSection:) or tableView(_:cellForRowAt:). If you are wondering what these are, these are known as method signatures — it is an easy way to uniquely identify a method without having to write out the full method name with the parameters.

The method signature identifies where each parameter would be (and the parameter name, where necessary) by separating out the parameters with a colon.

In the method for tableView(_:numberOfRowsInSection:) for example, you might notice an underscore for the first parameter — that means, that method does not need to have the parameter name specified when calling the method — it is simply a convenience in Swift where the parameter can generally be inferred from the method name. You might have more questions about this — but we’ll come back to that later.

If you are not sure about the signature for a method, take a look at the Xcode Jump bar (the tiny toolbar right above the source editor) and click on the last item of the file path elements to get a list of methods (and properties) in the current source file.

The Jump Bar shows the method signatures
The Jump Bar shows the method signatures

Also, do note that in the above examples, tableView is not the method name — or rather, tableView by itself is not the method name. The method name is the tableView plus the parameter list — everything up to the closing bracket for the parameter list. That’s how you get multiple unique methods such as tableView(_:numberOfRowsInSection:) and tableView(_:cellForRowAt:) even though they all look as if they are methods called tableName — the complete signature uniquely identifies the method.

Special comments

You might have noticed the following line in the code you just added:

// MARK:- Table View Data Source

If you were wondering what that was for, here’s the scoop. Of course, you already know that line is a comment, because the line begins with //, but it’s not just a comment. As the keyword at the beginning of the comment line, MARK, indicates, it is a marker. But a marker for what?

It’s a marker to organize the code and for you to find a section of code (for example, a set of related methods, like for the table view data source) via the Xcode Jump Bar.

Take a look at the previous screenshot showing the Xcode Jump Bar. Do you notice the separator line in the middle of the list of methods? Do you notice the bolded text title right after? Does that title seem familiar?

The text you provide after the MARK: keyword defines how the section title is displayed in the menu. If you put in a hyphen (-), you get a separator line followed by any text after the hyphen as the section title.

If you don’t provide a hyphen but provide some text, then you simply get a section title but no separator. If you provide neither, then you just get a section icon with no text and no separator. (Try these out.)

There are a couple of other comment tags besides MARK: that you can use in your Swift files. These are TODO: and FIXME:. The first is generally used to indicate portions of your code that need to be completed, while the latter is used to mark portions of code that need re-writing or fixing.

Consider using these tags to organize your code better. When you are in a hurry and need to find that particular bit of code in a long source file, they come in handy. I certainly use them all the time in my own code.

Testing the table view data source

Exercise: Modify the app so that it shows five rows.

That shouldn’t have been too hard:

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

If you were tempted to go into the storyboard and duplicate the prototype cell five times, then you were confusing cells with rows.

When you make tableView(_:numberOfRowsInSection:) return the number 5, you tell the table view that there will be five rows.

The table view then sends the cellForRowAt message five times, once for each row. Because tableView(_:cellForRowAt:) currently just returns a copy of the prototype cell, your table view will show five identical rows:

The table now has five identical rows
The table now has five identical rows

There are several ways to create cells in tableView(_:cellForRowAt:), but by far the easiest approach is what you’ve done here:

  1. Add a prototype cell to the table view in the storyboard.
  2. Set a reuse identifier on the prototype cell.
  3. Call tableView.dequeueReusableCell(withIdentifier:for:). This makes a new copy of the prototype cell if necessary, or, recycles an existing cell that is no longer in use.

Once you have a cell, you should set it up with the data from the corresponding row and give it back to the table view. That’s what you’ll do in the next section.

Putting row data into the cells

Currently, the rows (or rather the cells) all contain the placeholder text “Label.” Let’s add some unique text for each row.

➤ Open the storyboard and select the left Label inside the table view cell. Go to the Attributes inspector and set the Tag field to 1000.

Set the label’s tag to 1000
Set the label’s tag to 1000

A tag is a numeric identifier that you can give to a user interface control in order to uniquely identify it later. Why the number 1000? No particular reason. It should be something other than 0, as that is the default value for all tags. 1000 is as good a number as any.

➤ Do the same for the right label, but use 2000 as the tag instead.

Double-check to make sure you set the tag on the Labels, not on the Table View Cell or its Content View. It’s a common mistake to set the tag on the wrong view and then the results won’t be what you expected!

➤ In HighScoresViewController.swift, change tableView(_:cellForRowAt:) to the following:

override func tableView(_ tableView: UITableView, 
             cellForRowAt indexPath: IndexPath) 
             -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(
                        withIdentifier: "HighScoreItem", 
                                   for: indexPath)
                        
  // Add the following code
  let nameLabel = cell.viewWithTag(1000) as! UILabel
  let scoreLabel = cell.viewWithTag(2000) as! UILabel

  if indexPath.row == 0 {
    nameLabel.text = "The reader of this book"
    scoreLabel.text = "50000"
  } else if indexPath.row == 1 {
    nameLabel.text = "Manda"
    scoreLabel.text = "10000"
  } else if indexPath.row == 2 {
    nameLabel.text = "Joey"
    scoreLabel.text = "5000"
  } else if indexPath.row == 3 {
    nameLabel.text = "Adam"
    scoreLabel.text = "1000"
  } else if indexPath.row == 4 {
    nameLabel.text = "Eli"
    scoreLabel.text = "500"
  }
  // End of new code block
  
  return cell
}

You’ve already seen the first line. It gets a copy of the prototype cell — either a new one or a recycled one — and puts it into a local constant named cell. (Recall that this is a constant because it’s declared with let, not var. It is local because it’s defined inside a method.)

The first new line that you’ve just added is:

  let nameLabel = cell.viewWithTag(1000) as! UILabel

Here you ask the table view cell for the view with tag 1000. That is the tag you just set on the label in the storyboard. So, this returns a reference to the corresponding UILabel. Using tags is a handy trick to get a reference to a UI element without having to make an @IBOutlet variable for it.

Exercise: Why can’t you simply add an @IBOutlet variable to the view controller and connect the cell’s label to that outlet in the storyboard? After all, that’s how you created references to the labels in Bullseye… so why won’t that work here?

Answer: There will be more than one cell in the table and each cell will have its own label. If you connected the label from the prototype cell to an outlet on the view controller, that outlet could only refer to the label from one of these cells, not all of them. Since the label belongs to the cell and not to the view controller as a whole, you can’t make an outlet for it on the view controller. Confused? We’ll circle around to this topic soon, so don’t worry about it for now. Back to the code. What is this indexPath thing?

IndexPath is simply an object that points to a specific row in the table. When the table view asks the data source for a cell, you can look at the row number inside the indexPath.row property to find out the row for which the cell is intended.

Note: As was mentioned before, it is also possible for tables to group rows into sections. In an address book app you might sort contacts by last name. All contacts whose last name starts with “A” are grouped into their own section, all contacts whose last name starts with “B” are in another section, and so on.

To find out which section a row belongs to, you’d look at the indexPath.section property. This app has no need for this kind of grouping, so you’ll ignore the section property of IndexPath for now.

Now that you know about indexPath, the following code should make sense to you:

    if indexPath.row == 0 {
      nameLabel.text = "The reader of this book"
      scoreLabel.text = "50000"
    } else if indexPath.row == 1 {
      nameLabel.text = "Manda"
      scoreLabel.text = "10000"
    } else if indexPath.row == 2 {
      nameLabel.text = "Joey"
      scoreLabel.text = "5000"
    } else if indexPath.row == 3 {
      nameLabel.text = "Adam"
      scoreLabel.text = "1000"
    } else if indexPath.row == 4 {
      nameLabel.text = "Eli"
      scoreLabel.text = "500"
    }

You have seen this if — else if — else structure before. It simply looks at the value of indexPath.row, which contains the row number, and changes the label’s text accordingly. The cell for the first row gets the player name “The reader of this book” and the scores next to it would be “50000”. The cell for the second row gets the player name “Manda” with scores of “10000”, and so on. Look at that, you’re already ranked the highest!

Note: Computers generally start counting at 0 for lists of items. If you have a list of 4 items, they are counted as 0, 1, 2 and 3. It may seem a little silly at first, but that’s just the way programmers do things.

For the first row in the first section, indexPath.row is 0. The second row has row number 1, the third row is row 2, and so on.

Counting from 0 may take some getting used to, but after a while it becomes second nature and you’ll start counting at 0 even when you’re out for groceries.

➤ Run the app — it now has five rows, each with its own high score:

The rows in the table now have their own text
The rows in the table now have their own text

That is how you write the tableView(_:cellForRowAt:) method to provide data to the table. You first get a UITableViewCell object and then change the contents of that cell based on the row number of the indexPath.

Tapping on the rows

When you tap on a row, the cell color changes to indicate it is selected. The cell remains selected till you tap another row. You are going to change this behavior so that when you lift your finger the row is deselected.

A tapped row stays gray
A tapped row stays gray

Taps on rows are handled by the table view’s delegate. Remember you read before that in iOS you often find objects doing something on behalf of other objects? The data source is one example of this, but the table view also depends on another little helper, the table view delegate.

The concept of delegation is very common in iOS. An object will often rely on another object to help it out with certain tasks. This separation of concerns keeps the system simple, as each object does only what it is good at and lets other objects take care of the rest. The table view offers a great example of this.

Because every app has its own requirements for what its data looks like, the table view must be able to deal with lots of different types of data. Instead of making the table view very complex, or requiring that you modify it to suit your own apps, the UIKit designers have chosen to delegate the duty of providing the cells to display to another object, the data source.

The table view doesn’t really care who its data source is or what kind of data your app deals with, just that it can send the cellForRowAt message and receive a cell in return. This keeps the table view component simple and moves the responsibility for handling the data to where it belongs: your code.

Likewise, the table view knows how to recognize when the user taps a row, but what it should do in response depends on the app. In this app, you’ll transition to a different view controller; another app will likely do something totally different.

Using the delegation system, the table view can simply send a message that a tap occurred and let the delegate sort it out.

Usually, components will have just one delegate. But the table view splits up its delegate duties into two separate helpers: the UITableViewDataSource for putting rows into the table, and the UITableViewDelegate for handling taps on the rows and several other tasks.

➤ To see this, open the storyboard and Control-click on the table view to bring up its connections.

The table’s data source and delegate are hooked up to the view controller
The table’s data source and delegate are hooked up to the view controller

You can see that the table view’s data source and delegate are both connected to the view controller. That is standard practice for a UITableViewController. (You can also use table views in a basic UIViewController but then you’ll have to connect the data source and delegate manually.)

➤ Add the following method to HighScoresViewController.swift:

// MARK:- Table View Delegate
override func tableView(_ tableView: UITableView,
           didSelectRowAt indexPath: IndexPath) {
  tableView.deselectRow(at: indexPath, animated: true)
}

The tableView(_:didSelectRowAt:) method is one of the table view delegate methods and gets called whenever the user taps on a cell. Run the app and tap a row – the cell briefly turns gray and then becomes de-selected again.

Currently the high scores are hard-coded and never update. You need some way to keep track of new high scores. That means it’s time to expand the data source and make it use a proper data model, which is the topic of the next section.

Methods with multiple parameters

Most of the methods you used in the Bullseye app took only one parameter or did not have any parameters at all, but these new table view data source and delegate methods take two:

override func tableView(
           _ tableView: UITableView,             // parameter 1
           numberOfRowsInSection section: Int)   // parameter 2
           -> Int {                              // return value
  . . .
}
override func tableView(
          _ tableView: UITableView,              // parameter 1
          cellForRowAt indexPath: IndexPath)     // parameter 2
          -> UITableViewCell {                   // return value
  . . .
}
override func tableView(
       _ tableView: UITableView,                 // parameter 1
       didSelectRowAt indexPath: IndexPath) {    // parameter 2
  . . .
}

The first parameter is the UITableView object on whose behalf these methods are invoked. This is done for convenience, so you won’t have to make an @IBOutlet in order to send messages back to the table view.

For numberOfRowsInSection the second parameter is the section number. For cellForRowAt and didSelectRowAt it is the index-path.

Methods are not limited to just one or two parameters, they can have many. But for practical reasons two or three is usually more than enough, and you won’t see many methods with more than five parameters.

In other programming languages a method typically looks like this:

Int numberOfRowsInSection(UITableView tableView, Int section) {
  . . .
}

In Swift we do things a little differently, mostly to be compatible with the iOS frameworks, which are all written in the Objective-C programming language. Let’s take a look again at numberOfRowsInSection:

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

The method signature for the above method, as discussed before, is tableView(_:numberOfRowsInSection:). If you say that out loud (without the underscores and colons, of course), it actually makes sense. It asks for the number of rows in a particular section of a particular table view.

The first parameter looks like this:

    _ tableView: UITableView

The name of this parameter is tableView. The name is followed by a colon and the parameter’s type, UITableView.

The second parameter looks like this:

    numberOfRowsInSection section: Int

This one has two names, numberOfRowsInSection and section.

You use the first name, numberOfRowsInSection, when calling the method. This is the external parameter name. Inside the method itself you use the second name, section, known as the local parameter name. The data type of this parameter is Int.

The _ underscore is used when you don’t want a parameter to have an external name. You’ll often see the _ on the first parameter of methods that come from Objective-C frameworks. With such methods the first parameter only has one name but the other parameters have two. Strange? Yes.

It makes sense if you’ve ever programmed in Objective-C, but no doubt it looks weird if you’re coming from another language. Once you get used to it, you’ll find that this notation is actually quite readable.

Sometimes people with experience in other languages get confused because they think that HighScoresViewController.swift contains three functions that are all named tableView(). But that’s not how it works in Swift: the names of the parameters are part of the full method name. That’s why these three methods are actually named:

    tableView(_:numberOfRowsInSection:)
    tableView(_:cellForRowAt:)
    tableView(_:didSelectRowAt:)

By the way, the return type of the method is at the end, after the -> arrow. If there is no arrow, as in tableView(_:didSelectRowAt:), then the method is not supposed to return a value.

Phew! That was a lot of new stuff to take in, so I hope you’re still with me. If not, then take a break and start at the beginning again. You’re being introduced to a whole bunch of new concepts all at once and that can be overwhelming.

But don’t worry, it’s OK if everything doesn’t make perfect sense yet. As long as you get the gist of what’s going on, you’re good to go.

If you want to check your work up to this point, you can find the project files for the app under 20-Table Views in the Source Code folder.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.