Chapters

Hide chapters

UIKit Apprentice

First Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

11. Navigation Controllers
Written by Matthijs Hollemans & Fahim Farook

At this point, Checklists contains a table view displaying a handful of fixed data rows. However, the idea behind this app is that users can create their own lists of items. Therefore, you need to give the user the ability to add to-do items.

In this chapter you’ll expand the app to have a navigation bar at the top. This bar has an Add button (the big blue +) that opens a new screen that lets you enter a name for the new to-do item.

When you tap Done, the new item will be added to the list.

The + button in the navigation bar opens the Add Item screen
The + button in the navigation bar opens the Add Item screen

Presenting a new screen to add items is a common pattern in a lot of apps. Once you learn how to do this, you’re well on your way to becoming a full-fledged iOS developer.

This chapter covers the following:

  • Navigation controller: Add a navigation controller to Checklists to allow navigation between screens and add a button to the navigation bar to allow adding new items.
  • Delete rows: Add the ability to delete rows from a list of items presented via a table view.
  • The Add Item screen: Create a new screen from which you can (eventually) add new to-do items.

Navigation controller

First, let’s add the navigation bar. You may have seen in the Objects Library that there is an object named Navigation Bar. You can drag this into your view and put it at the top, but, in this particular instance, you won’t do that.

Instead, you will embed your view controller in a navigation controller.

Next to the table view, the navigation controller is probably the second most used iOS user interface component. It is the thing that lets you go from one screen to another:

A navigation controller in action
A navigation controller in action

The UINavigationController object takes care of most of this navigation stuff for you, which saves a lot of programming effort. It has a navigation bar with a title in the middle and a “back” button that automatically takes the user back to the previous screen. You can put a button — or several buttons — of your own on the right.

Add a navigation controller

Adding a navigation controller is really easy.

➤ Open Main.storyboard and select the Checklist View Controller.

➤ From the menu bar at the top of the screen, choose Editor ▸ Embed In ▸ Navigation Controller.

Putting the view controller inside a navigation controller
Putting the view controller inside a navigation controller

That’s it. Interface Builder has now added a new Navigation Controller scene and made a relationship between it and your view controller.

The navigation controller is now linked with your view controller
The navigation controller is now linked with your view controller

When the app starts up, the Checklist View Controller is automatically put inside a navigation controller.

➤ Run the app and try it out.

The app now has a navigation bar at the top
The app now has a navigation bar at the top

The only thing different (visually) is that the app now has a navigation bar at the top.

Set the navigation bar title

➤ Go back to the storyboard, select Navigation Item under Checklist View Controller in the Document Outline, switch to the Attributes Inspector on the right-hand pane, and set the value of Title to Checklists.

Changing the title in the navigation bar
Changing the title in the navigation bar

What you’re doing here is changing a Navigation Item object that was automatically added to the view controller when you chose the Embed In command.

The Navigation Item object contains the title and buttons that appear in the navigation bar when this view controller becomes active. Each embedded view controller has its own Navigation Item that it uses to configure what shows up in the navigation bar.

When the navigation controller slides a new view controller in, it replaces the contents of the navigation bar with the new view controller’s Navigation Item.

Run your app and your screen should look something like this:

Navigation bar with title
Navigation bar with title

Display large titles

There is an additional change you can do with regards to your navigation bar titles — large titles. Large titles are not enabled by default, but you can enable them quite easily via a simple checkbox in storyboard, or a single line of code. So, let’s do that!

➤ Switch to ChecklistViewController.swift and add the following line to viewDidLoad, right after the existing super.viewDidLoad() line:

navigationController?.navigationBar.prefersLargeTitles = true

There are a few interesting things in that bit of code but we don’t want to get into all of it now. For now, the important things to remember are these:

  1. Generally, there is a single navigation controller for a given navigation flow.

  2. A single navigation controller could present multiple view controllers as part of its navigation flow.

  3. Each view controller in a navigation hierarchy has a reference to the navigation controller which presented it.

Given the above information, the previous code snippet simply uses the view controller’s reference to the navigation controller to access the navigation bar for the app. Then, it sets the prefersLargeTitles property on the navigation bar to true. And it is this property, as the name implies, which enables large titles.

Note: If you wanted to make the same change via storyboard instead of code, you’d select the Navigation Bar under your Navigation Controller in your storyboard and set the Prefers Large Titles checkbox in the Attributes inspector.

Run your app again. Do you see a difference?

Navigation bar with large title
Navigation bar with large title

Note: Apple does not recommend using large titles for all of your screens. Rather, their recommendation is to use large titles on your main screen and any other subsequent screens where it might make sense to have a prominent title. You will learn how to turn off large titles for secondary views later on.

Interesting, huh? Of course, you might wonder why there is so much space above the title — that seems like a waste of space, right? That space will be utilized by the navigation items — the back button on the left (if you are in a secondary screen), and any other button(s) you assign to the right.

Add a navigation button to add items

Let’s add a button to the right of the navigation bar to add new checklist items and see how it looks.

➤ Open your main storyboard.

➤ Go to the Objects Library and look for Bar Button Item. Drag it into the right-side slot of the navigation bar — be sure to use the navigation bar on the Checklist View Controller, not the one from the navigation controller!

Dragging a Bar Button Item into the navigation bar
Dragging a Bar Button Item into the navigation bar

Note: If you enabled large titles via the storyboard, your navigation bar will look taller than the above screenshot. This is normal — it’s not a bug :]

By default, this new button is named “Item” but for this app you want it to have a big + sign.

➤ In the Attributes inspector for the bar button item, choose System Item: Add.

Bar Button Item attributes
Bar Button Item attributes

If you look through the list for the System Item dropdown, you’ll see a lot of predefined bar button types: Add, Compose, Reply, Camera, and so on. You can use these in your own apps, but be sure to use them only for their intended purpose — you shouldn’t use the camera icon on a button that sends an email, for example. Improper use of these icons may lead Apple to reject your app from the App Store. And that sucks.

OK, that gives us a button. If you run the app, it should look like this:

The app with the Add button
The app with the Add button

Now it looks a little less bare, right? If you’re still not happy with the amount of space taken up by large titles, you can always turn off large titles, but do note that when you have a screenful of items and you need to scroll to see more information, the large title will retract into the top navigation bar and give you the “classic”-look navigation bar. So you might want to try this out a bit before deciding to disable it.

Make the navigation button do something

If you tap on your new add button, it doesn’t actually do anything. That’s because you haven’t hooked it up to an action. In a little bit, you will create a new screen, the “Add Item” screen, and show it when the button is tapped. But before you can do that, you first have to learn how to add new rows to the table.

Let’s hook up the Add button to an action. You got plenty of exercise with this for Bull’s Eye, so it should be child’s play for you by now.

➤ Add a new action method to ChecklistViewController.swift:

// MARK: - Actions
@IBAction func addItem() {
}

You’re leaving the method empty for the moment, but it needs to be there so you have something to connect the button to.

Note: As you might have noticed, I have added a new jumpbar section called Actions along with the above action. I usually put all my action methods in one place. You don’t really have to follow what I’ve done, but it’s always a good idea to organize your code in some way to make finding a particular method easy.

➤ Open the storyboard and connect the Add button to this action. To do this, Control-drag from the + button to the yellow circle in the bar above the view (this circle represents the Checklist View Controller):

Control-drag from Add button to Checklist View Controller
Control-drag from Add button to Checklist View Controller

Actually, you can Control-drag from the Add button to almost anywhere in the same scene to make the connection.

➤ After dragging, pick addItem from the popup (under Sent Actions):

Connecting to the addItem action
Connecting to the addItem action

➤ Let’s give addItem() something to do. Back in ChecklistViewController.swift, add some code to the method as follows:

@IBAction func addItem() {
  let newRowIndex = items.count

  let item = ChecklistItem()
  item.text = "I am a new row"
  items.append(item)

  let indexPath = IndexPath(row: newRowIndex, section: 0)
  let indexPaths = [indexPath]
  tableView.insertRows(at: indexPaths, with: .automatic)
}

The new code creates a new ChecklistItem object and adds it to the data model — the items array. You also have to tell the table view, “I’ve inserted a row at this index, please update yourself.”

Let’s review the code section-by-section:

  let newRowIndex = items.count

You need to know what the index of the new row in your array would be. This is necessary in order to properly update the table view later.

When you start the app there are 5 items in the array and 5 rows on the screen. Computers start counting at 0, so the existing rows have indexes 0, 1, 2, 3 and 4. To add the new row to the end of the array, the index for that new row must be 5.

In other words, when you add a row to the end of an array, the index for the new row is always equal to the number of items currently in the array. Let that sink in for a second.

You store the index for the new row in the local constant newRowIndex. This can be a constant instead of a variable because it never has to change.

The following few lines should look familiar:

  let item = ChecklistItem()
  item.text = "I am a new row"
  items.append(item)

You have seen this code before in viewDidLoad. It creates a new ChecklistItem object and adds it to the end of the array.

The data model now consists of 6 ChecklistItem objects inside the items array. Note that at this point newRowIndex is still 5 even though items.count is now 6. That’s why you read the item count and stored this value in newRowIndex before you added the new item to the array.

Just adding the new ChecklistItem object to the data model’s array isn’t enough. You have to tell the table view about this new row so it can add a new cell for that row.

  let indexPath = IndexPath(row: newRowIndex, section: 0)

As you know by now, table views use index-paths to identify rows. So, you first make an IndexPath object that points to the new row, using the row number from the newRowIndex variable. This index-path object now points to row 5 (in section 0).

The next line creates a new, temporary array holding just the one index-path item:

  let indexPaths = [indexPath]

You use the table view method insertRows(at:with:) to tell the table view about the new row. While you only have one inserted row here, as its name implies, this method actually lets you insert multiple rows at the same time, if you wanted to.

So, instead of a single IndexPath object, you need to pass an array of index-paths to the method. Fortunately, it is easy to create an array that contains a single index-path object by writing [indexPath]. The notation [] creates a new Array object that contains the objects between the brackets. Of course, if the array has more than one item, you have to separate the items with commas.

Finally, you tell the table view to insert this new row. The with: .automatic parameter makes the table view use a nice animation when it inserts the row:

  tableView.insertRows(at: indexPaths, with: .automatic)

To recap, you:

  1. Created a new ChecklistItem object.
  2. Added it to the data model.
  3. Inserted a new row for it in the table view.

When you call tableView.insertRows(at:with:) to insert a new row, the table view makes a cell for this new row by calling your tableView(_:cellForRowAt:) data source method — but it only does this if the new row is actually in the visible portion of the table view.

➤ Try it out. You can now add many new rows to the table. You can also tap these new rows to turn their checkmarks on and off again. When you scroll the table up and down, the checkmarks stay with the proper rows.

After adding new rows with the + button
After adding new rows with the + button

Note: If you were concerned by the change to large titles, also notice how the large title becomes a smaller title (and vice versa) when you scroll up and down.

Remember, the rows always have to be added to both your data model and the table view. When you send the insertRows(at:with:) message to the table view, you say: “Hey table, my data model has a bunch of new items added to it.”

This is important! If you forget to tell the table view about your new items or if you tell the table view there are new items, but you don’t actually add them to your data model, then your app will crash. The data model and the table view always have to be in sync.

Exercise: Give the new items checkmarks by default.

Delete rows

While you’re at it, you might as well give users the ability to delete rows.

A common way to do this in iOS apps is “swipe-to-delete”. You swipe your finger over a row and a Delete button slides into view.

A tap on the Delete button confirms the removal, tapping anywhere else will cancel.

Swipe-to-delete in action
Swipe-to-delete in action

Swipe-to-delete

Swipe-to-delete is very easy to implement.

➤ Add the following method to ChecklistViewController.swift. Just to keep things organized, I suggest you put this with the other table view delegate methods.

override func tableView(
  _ tableView: UITableView, 
  commit editingStyle: UITableViewCell.EditingStyle, 
  forRowAt indexPath: IndexPath
) {
  // 1
  items.remove(at: indexPath.row)

  // 2  
  let indexPaths = [indexPath]
  tableView.deleteRows(at: indexPaths, with: .automatic)
}

When the commitEditingStyle method is present in your view controller — it is a method defined by the table view data source protocol —, the table view will automatically enable swipe-to-delete. All you have to do is:

  1. Remove the item from the data model.
  2. Delete the corresponding row from the table view.

This mirrors what you did in addItem(). Again, you make a temporary array with the index-path object and then tell the table view to remove the rows with an animation.

➤ Run the app to try it out!

Destroying objects

When you call items.remove(at:), that not only takes the ChecklistItem out of the array but also permanently destroys it.

We’ll talk more about this later on, but if there are no more references to an object, it is automatically destroyed. As long as a ChecklistItem object sits inside an array, that array has a reference to it.

But when you pull that ChecklistItem out of the array, the reference goes away and the object is destroyed. Or in computer-speak, it is deallocated.

What does it mean for an object to be destroyed? Each object occupies a small section of the computer’s memory. When you create an object instance, a chunk of memory is reserved to hold the object’s data. If the object is deallocated, that memory becomes available again and will eventually be occupied by new objects. After it has been deleted, the object does not exist in memory any more and you can no longer use it.

On older versions of iOS, you had to take care of this memory management by hand. Fortunately times have changed for the better. Swift uses a mechanism called Automatic Reference Counting, or ARC, to manage the lifetime of the objects in your app, freeing you from having to worry about it. I like not having to worry about things!

The Add Item screen

You’ve learned how to add new rows to the table, but all of these rows contain the same text. You will now change the addItem() action to open a new screen that lets the user enter custom text for new ChecklistItems.

The Add Item screen
The Add Item screen

Add a new view controller to the storyboard

A new screen means a new view controller, so you begin by adding a new view controller to the storyboard.

➤ Go to the Objects Library and drag a new Table View Controller (not a regular view controller) on to the storyboard canvas.

Dragging a new Table View Controller into the canvas
Dragging a new Table View Controller into the canvas

You may need to zoom out to fit everything properly. Right-click on the canvas to get a popup with zoom options, or use the - 100% + controls at the bottom of the Interface Builder canvas. Or, if you have a Trackpad, simply pinch with two fingers to zoom in or out.

➤ With the new view controller in place, select the Add button from the Checklist View Controller. Control-drag to the new view controller.

Control-drag from the Add button to the new table view controller
Control-drag from the Add button to the new table view controller

Let go of the mouse and a list of options pops up.

The Action Segue popup
The Action Segue popup

The options in this menu are the different types of connections you can make between the Add button and the new screen.

➤ Choose Show from the menu.

As I mentioned when adding the About screen for Bull’s Eye, this type of connection is named a segue.

The segue is represented by the arrow between the two view controllers:

A new segue is added between the two view controllers
A new segue is added between the two view controllers

➤ Run the app to see what it does.

When you press the Add button, a new empty table view slides in from the right. You can press the back button — the one that says “Checklists” —, to go back to the previous screen.

The screen that shows up after you press the Add button
The screen that shows up after you press the Add button

You didn’t even have to write much code and you now have yourself a working navigation controller where you can go from one screen to another!

Note: Xcode may be giving you the warning, “Prototype table cells must have reuse identifiers”. You might remember this issue from before — you will fix this issue soon.

Note that the Add button no longer adds a new row to the table. That connection has been broken and is replaced by the segue. Just in case, you should remove the button’s connection with the addItem action.

➤ Select the Add button, go to the Connections inspector and press X next to addItem.

Removing the addItem action from the Add button
Removing the addItem action from the Add button

Notice that this inspector also shows the connection with the segue that you’ve just made (under Triggered Segues).

Segue Types

When showing the new view controller above, you opted for a Show segue. But what does it mean? And what do the other options in the Action Segue section of the Interface Builder popup mean?

Here is a brief explanation of each type of segue:

  • Show: Pushes the new view controller onto the navigation stack so that the new view controller is at the top of the navigation stack. It also provides a back button to return to the previous view controller. If the view controllers are not embedded in a navigation controller, then the new view controller will be presented modally — see Present Modally in the list below as to what this means.

    Example: Navigating folders in the Mail app

  • Show Detail: For use in a split view controller — you’ll learn more about those when developing the last app in this book. The new view controller replaces the detail view controller of the split view when in an expanded two-column interface. Otherwise, if in single-column mode, it will push in a navigation controller.

    Example: In Messages, tapping a conversation will show the conversation details — replacing the view controller on the right when in a two-column layout, or push the conversation when in a single column layout

  • Present Modally: Presents the new view controller to cover the previous view controller — most commonly used to present a view controller that covers the entire screen on iPhone, or on iPad it’s common to present it as a centered box that darkens the presenting view controller. Usually, if you had a navigation bar at the top or a tab bar at the bottom, those are covered by the modal view controller too.

    Example: Selecting Touch ID & Passcode in Settings

  • Present as Popover: When run on an iPad, the new view controller appears in a popover, and tapping anywhere outside of this popover will dismiss it. On an iPhone, will present the new view controller modally over the full screen.

    Example: Tapping the + button in Calendar

  • Custom: Allows you to implement your own custom segue and have control over its behavior. You will learn more about this in a later chapter.

Customize the navigation bar

So now you have a new table view controller that slides into the screen when you press the Add button. However, this is not quite what you want.

Data input screens usually have a navigation bar with a Cancel button on the left and a Done button on the right — in some apps, the button on the right is called Save or Send. Pressing either of these buttons will close the screen, but only Done will save your changes.

If you check the Document Outline, you will notice that the new table view controller scene has a Navigation Item — if it doesn’t, then drag one from the Object Library on to the new scene since we are not able to customize the navigation elements — such as the navigation buttons, or the title — for this table view controller, without the Navigation Item.

➤ Drag two Bar Button Items on to the navigation bar of the new scene, one to the left slot (removing the existing back button) and one to the right slot.

The navigation bar items for the new screen
The navigation bar items for the new screen

➤ In the Attributes inspector for the left button choose System Item: Cancel.

➤ For the right button choose Done for both System Item and Style attributes.

Don’t type anything into the button’s Title field. The Cancel and Done buttons are built-in button types that automatically use the proper text. If your app runs on an iPhone where the language is set to something other than English, these predefined buttons are automatically translated into the correct language.

➤ Double-click the navigation bar for the new table view controller to edit its title and change it to Add Item — you can also change this via the Attributes inspector as you did before.

➤ Run the app, tap the Add button on the main screen, and you’ll see that your new screen has Cancel and Done buttons.

The Cancel and Done buttons in the app
The Cancel and Done buttons in the app

The new buttons look good, but — as you would have noticed from the storyboard if you had enabled large titles from the storyboard — the title is huge! If Apple recommends using large titles only on main screens, we probably should change this screen to have smaller titles. But how do we do that?

While some view controller (or table view controller) customizations can be done via storyboard (and this one can too), some require writing some code. Our new view controller does not have a matching source file. So, in the next section we’ll create the source file and add the custom code instead of doing the changes via storyboard just so you know how to do it via code.

Note: If you’d prefer to make the change via storyboard, then simply select the Navigation Item for the new view controller, go to the Attributes inspector and select Never from the Large Title dropdown.

Make your own view controller class

You created a custom view controller in Bull’s Eye for the About screen. Do you remember how to do it on your own? If not, here are the steps:

➤ In the Project Navigator, right-click on the Checklists group (the yellow folder) in the project navigator and choose New File… Choose the Cocoa Touch Class template.

➤ In the next dialog, set the Class to AddItemViewController and Subclass to UITableViewController. When you change the subclass, the class name will automatically change — so either set the subclass first or change the class name back after the change. Leave the language at Swift (or change it if it is not set to Swift).

➤ Save the file to your project folder, which should be the default location.

➤ The file should have a lot of source and commented code — this is known as boilerplate code, or code that is generally always needed. In this particular case, you don’t need most of it. So remove everything except for viewDidLoad (and remove the comments from inside viewDidLoad as well) so that your code looks like this:

import UIKit

class AddItemViewController: UITableViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
  }
}

This tells Swift that you have a new object for a table view controller that goes by the name of AddItemViewController. You’ll add the rest of the code soon. First, you have to let the storyboard know about this new view controller.

➤ In the storyboard, select the Add Item view controller and go to the Identity inspector. Under Custom Class, type AddItemViewController.

This tells the storyboard that the view controller from this scene is actually your new AddItemViewController object.

Changing the class name of the AddItemViewController
Changing the class name of the AddItemViewController

Don’t forget this step! Without it, the Add Item screen will simply not work.

Make sure that it is really the view controller that is selected before you change the fields in the Identity inspector (the scene needs to have a blue border). A common mistake is to select the table view and change that.

Turn off large titles

Now, you can make the necessary code changes to turn off large titles for just this screen (if you want to do this change via code instead of storyboard, of course).

➤ Add the following line to the end of viewDidLoad in AddItemViewController.swift:

navigationItem.largeTitleDisplayMode = .never

The above code customizes the Navigation Item for the Add Item screen to never show large titles. Try running the app now.

Large titles begone!
Large titles begone!

Make the navigation buttons work

Much better, right? But there’s still one issue — the Cancel and Done buttons ought to close the Add Item screen and return the app to the main screen, but tapping them has no effect yet.

Exercise: Do you know why the Cancel and Done buttons do not return you to the main screen?

Answer: Because those buttons have not yet been hooked up to any actions!

You will now implement the necessary action methods in AddItemViewController.swift.

➤ Add these new cancel() and done() action methods:

// MARK: - Actions
@IBAction func cancel() {
  navigationController?.popViewController(animated: true)
}

@IBAction func done() {
  navigationController?.popViewController(animated: true)
}

This tells the navigation controller to close the Add Item screen with an animation and to go back to the previous screen, which in this case is the main screen.

You still need to hook up the Cancel button to the cancel() action and the Done button to the done() action.

➤ Open the storyboard and find the Add Item View Controller. Control-drag from the bar buttons to the yellow circle icon and pick the proper action from the popup menu.

Control-dragging from the bar button to the view controller
Control-dragging from the bar button to the view controller

➤ Run the app to try it out. The Cancel and Done buttons now return the app to the main screen.

What do you think happens to the AddItemViewController object when you dismiss it? After the view controller disappears from the screen, its object is destroyed and the memory it was using is reclaimed by the system.

Every time the user opens the Add Item screen, the app makes a new instance of it. This means a view controller object is only alive for the duration that the user is interacting with it; there is no point in keeping it around afterwards.

Container view controllers

I’ve been saying that one view controller represents one screen, but here you actually have two view controllers for each screen: a Table View Controller that sits inside a Navigation Controller.

The Navigation Controller is a special type of view controller that acts as a container for other view controllers. It comes with a navigation bar and has the ability to easily go from one screen to another, by sliding them in and out of sight. The container essentially “wraps around” these screens.

The Navigation Controller is just the frame that contains the view controllers that do the real work, which are known as the “content” controllers. Here, the ChecklistViewController provides the content for the first screen; the content for the second screen comes from the AddItemViewController.

Another often-used container is the Tab Bar Controller, which you’ll see in the next app.

On the iPad, container view controllers are even more commonplace. View controllers on the iPhone are full-screen but on the iPad they often occupy only a portion of the screen, such as the content of a popover or one of the panes in a split-view.

This completes the implementation of the navigation functionality for your app’s two screens. If at any point you got stuck, you can refer to the project files for the app from the 11-Navigation-controllers folder 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.