Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

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: 12 chapters
Show chapters Hide chapters

16. Lists
Written by Eli Ganim

Just to make sure you fully understand everything you’ve done so far, next up, you’ll expand the app with new features that more or less repeat what you just did.

But I’ll also throw in a few twists to keep it interesting…

The app is named Checklists for a reason: it allows you to keep more than one list of to-do items. So far though, the app has only supported a single list. Now you’ll add the capability to handle multiple checklists.

In order to complete the functionality for this chapter, you will need two new screens, and that means two new view controllers:

  1. AllListsViewController shows all the user’s lists.
  2. ListDetailViewController allows adding a new list and editing the name and icon of an existing list.

This chapter covers the following:

  • The All Lists view controllers: Add a new view controller to show all the lists of to-do items.
  • The All Lists UI: Complete the user interface for the All Lists screen.
  • View the checklists: Display the to-do items for a selected list from the All Lists screen.
  • Manage checkists: Add a view controller to add/edit checklists.

The All Lists view controller

You will first add AllListsViewController. This becomes the new main screen of the app.

When you’re done, this is what it will look like:

The new main screen of the app
The new main screen of the app

This screen is very similar to what you created before. It’s a table view controller that shows a list of Checklist objects (not ChecklistItem objects).

From now on, you will refer to this screen as the “All Lists” screen, and to the screen that shows the to-do items from a single checklist as the “Checklist” screen.

Add the new view controller

➤ Right-click the Checklists group in the project navigator and choose New File. Choose the Cocoa Touch Class template (under iOS, Source).

In the next step, choose the following options:

  • Class: AllListsViewController.

  • Subclass of: UITableViewController.

  • Also create XIB file: Make sure this is not checked.

  • Language: Swift.

Choosing the options for the new view controller
Choosing the options for the new view controller

Note: Make sure the “Subclass of” field is set to UITableViewController, not “UIViewController.” Also be careful that Xcode didn’t rename what you typed into Class to “AllListsTableViewController” with the extra word “Table” when you change the “Subclass of” value. It can be sneaky like that…

➤ Press Next and then Create to finish.

As you might remember from a previous chapter, the Xcode template for a table view controller puts a lot of boilerplate code that you don’t need. Let’s clean that up first.

You’ll also put some fake data in the table view just to get it up and running. As you know by now, it’s preferred to take as small a step as possible and then run the app to see if it’s working. Once everything works, you can move forward and put in the real data.

Clean up the boilerplate code

➤ In AllListsViewController.swift, remove all the commented out code from viewDidLoad.

➤ Remove the numberOfSections(in:) method. Without it, there will always be a single section in the table view.

➤ Change the tableView(_:numberOfRowsInSection:) method to:

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

➤ Implement the tableView(_:cellForRowAt:) method to put some text into the cells, just so there is something to see.

Note that the boilerplate code already contains a commented-out version of this method. You can uncomment it by removing the /* and */ surrounding the method and make your changes there.

override func tableView(_ tableView: UITableView,
         cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(
                withIdentifier: cellIdentifier, for: indexPath)
  cell.textLabel!.text = "List \(indexPath.row)"
  return cell
}

In ChecklistViewController the table view used prototype cells that you designed in Interface Builder. Just for the fun of it, in AllListsViewController you will take a different approach where you’ll create the cells in code instead.

The code approach is simplicity itself — you simply dequeue the cell from the table view and then set the cell up as normal.

➤ At this point you’ll get an error about cellIdentifier being unknown. Let’s add a constant to the class for the cell identifier — you’ll see why in a moment. Add the following to at the top of the class implementation (where you normally add instance variables):

let cellIdentifier = "ChecklistCell"

You’re using dequeueReusableCell(withIdentifier:) here too, just as you did with prototype cells. However, we don’t have a prototype cell here. So, we need a way to let the app know what type of table view cell (or rather cell class) is to be created for a call to dequeueReusableCell(withIdentifier:) with our custom cell identifier specified in the cellIdentifier constant.

To do that, we need to add a bit of code to viewDidLoad to register the cell identifier with the underlying table view.

➤ Add the following code to the end of viewDidLoad:

tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)

The above registers our cell identifier with the table view so that the table view knows which cell class should be used to create a new table view cell instance when a dequeue request comes in with that cell identifier. Also, in this case, we have registered the standard table view cell class as the one to be used for creating new cells, But if you wanted, you could also register custom table view cell classes here.

You should now see why we set up the cell identifier to be at the class level — because we need access to the identifier from at least two different methods.

➤ Remove all the other commented-out cruft from AllListsViewController.swift. Xcode puts it there to be helpful, but it also makes a mess of things.

Storyboard changes

The final step is to add the new view controller to the storyboard.

➤ Open the storyboard and drag a new Table View Controller onto the canvas. Put it somewhere near the initial navigation controller.

Control-drag from the navigation controller to this new table view controller:

Control-drag from the navigation controller to the new table view controller
Control-drag from the navigation controller to the new table view controller

From the pop-up menu choose Relationship Segue — root view controller:

Relationships are also segues
Relationships are also segues

This will break the existing connection between the navigation controller and the ChecklistViewController so that “Checklists” is no longer the app’s main screen.

➤ Select the new table view controller and set its Class in the Identity inspector to AllListsViewController.

➤ Select the new view controller’s Navigation Item in the Document Outline and then change its title to Checklists via the Attributes Inspector.

This may make Xcode rename the view controller in the Document Outline from All Lists View Controller to just Checklists. Sometimes it won’t happen till you restart Xcode. This is a bit confusing because there’s a Checklists view controller already.

It’s simple enough to fix the scene names. Normally, the scene name is based on either the underlying view controller name or the navigation item title. But you can set whatever you want as the scene name by simply changing the displayed title on the Document Outline.

➤ Tap the new view controller in the Document Outline (the yellow circle, not the rectangle representing the scene) and then tap it again to put the title into edit mode. Then, just rename it to All Lists.

Rename, here:

Rename scene
Rename scene

➤ Repeat the above step to rename the remaining Checklists scene to Checklist (note the missing “s” at the end).

You may want to reorganize your storyboard at this point to make everything look neat again. The All Lists scene goes in between the other scenes.

As was mentioned, you’re not going to use prototype cells for this table view. It would be perfectly fine if you did, and, as an exercise, you could rewrite the code to use prototype cells later, but let’s see another way of making table view cells.

➤ Delete the empty prototype cell from the All Lists scene. Simply select the Table View Cell and press delete on your keyboard.

Control-drag from the yellow circle icon at the top of All Lists scene on to the Checklist scene and create a Show segue.

Control-dragging from the All Lists scene to the Checklist scene
Control-dragging from the All Lists scene to the Checklist scene

This adds a “push” transition from the All Lists screen to the Checklist screen. It also puts the navigation bar back on the Checklist scene (the one on the right).

➤ Double-click the navigation bar on the Checklist scene to change its title to (Name of the Checklist). This is just placeholder text.

➤ If you enabled/disabled large titles via the storyboard, then disable large titles for the Checklist scene by setting the Navigation Item’s Large Title attribute to Never.

Note that the new segue isn’t attached to any button or table view cell.

There is nothing on the All Lists screen that you can tap or otherwise interact with in order to trigger this segue. That means you have to perform the segue programmatically.

Performing a segue via code

➤ Click on the new segue to select it, go to the Attributes inspector and give it the identifier ShowChecklist.

The segue Kind should be Show (e.g. Push) because you’re pushing the Checklist View Controller onto the navigation stack when performing this segue.

➤ In AllListsViewController.swift, add the tableView(_:didSelectRowAt:) method:

override func tableView(_ tableView: UITableView,
           didSelectRowAt indexPath: IndexPath) {
  performSegue(withIdentifier: "ShowChecklist", sender: nil)
}

Recall that this table view delegate method is invoked when you tap a row.

Previously, a tap on a row would automatically perform the segue because you had hooked up the segue to the prototype cell. However, the table view for this screen isn’t using prototype cells. Therefore, you have to perform the segue manually.

That’s simple enough: just call performSegue(withIdentifier:sender:) with the name of the segue and things will start moving.

➤ Run the app. It might now look like this (or it might be slightly different depending on whether you set up large title enabling/dsiabling via code or storyboards):

The first version of the All Lists screen (left). Tapping a row opens the Checklist screen (right).
The first version of the All Lists screen (left). Tapping a row opens the Checklist screen (right).

Tap a row and the familiar ChecklistViewController slides into the screen.

You can tap the “Back” button in the top-left to go back to the main list. Now you’re truly using the power of the navigation controller!

Fixing the titles (maybe?)

If you configured large titles via code, the second screen, Checklist, might have the large title while the first one doesn’t! This would be because you originally set up large titles for ChecklistViewController.swift.

Exercise: Can you fix the titles on your own so that the large titles are enabled by AllListsViewController.swift and the Checklist screen does not show a large title?

The change is simple enough to implement.

➤ Move the following lines of code from viewDidLoad in ChecklistViewController.swift to viewDidLoad in AllListsViewController.swift:

// Enable large titles
navigationController?.navigationBar.prefersLargeTitles = true

➤ Add this code to viewDidLoad in ChecklistViewController.swift:

// Disable large titles for this view controller
navigationItem.largeTitleDisplayMode = .never

In each case, the comments explain what the code does.

Run the app again and verify that the titles now display correctly.

The All Lists UI

You’re going to duplicate most of the functionality from the Checklist View Controller for this new All Lists screen.

There will be a + button at the top that lets users add new checklists, they can do swipe-to-delete, and they can tap the disclosure button to edit the name of the checklist.

Of course, you’ll also save the array of Checklist objects to the Checklists.plist file.

As you’ve already seen how this works, we’ll go through the steps a bit quicker this time.

The data model

You begin by creating a data model object that represents a checklist.

➤ Add a new file to the project based on the Cocoa Touch Class template. Name it Checklist and make it a subclass of NSObject. (Also make sure that the language is set to Swift.)

This adds the file Checklist.swift to the project.

Just like ChecklistItem, you’re building Checklist on top of NSObject.

As you found out previously, this is a requirement when you need to compare objects (in order to find a list item in an array of lists).

➤ Give Checklist.swift a name property:

import UIKit

class Checklist: NSObject {
  var name = ""
}

Next, you’ll give AllListsViewController an array that will store these new Checklist objects.

➤ Add a new instance variable to AllListsViewController.swift:

var lists = [Checklist]()

This is an array that will hold the Checklist objects.

Note: You can also write the above as follows:

var lists = Array<Checklist>()

The version with the square brackets is what’s known as syntactic sugar for the complete notation, which is Array<type of the objects to put in the array>.

You will see both forms used in Swift programs and they do exactly the same thing. Because arrays are used a lot, the designers of Swift included the handy shorthand with the square brackets.

As a first step, you will fill this new array with test data, which you’ll do from viewDidLoad() as before. Remember that UIKit automatically invokes this method when the view controller is first loaded.

Dummy data

In AllListsViewController.swift you could add the following to viewDidLoad() (don’t actually add it just yet, just read along with the description):

// 1
var list = Checklist()
list.name = "Birthdays"
lists.append(list)

// 2
list = Checklist()
list.name = "Groceries"
lists.append(list)

list = Checklist()
list.name = "Cool Apps"
lists.append(list)

list = Checklist()
list.name = "To Do"
lists.append(list)

You’ve seen something very much like it a while ago when you added the fake test data to ChecklistViewController. Here is what it does step-by-step:

  1. Create a new Checklist object, give it a name and add it to the array.

  2. You create three more Checklist objects. Because you declared the local variable list as var instead of let, you can re-use it.

Notice how this is performing the same two steps for every new Checklist object you’re creating?

list = Checklist()
list.name = "Name of the checklist"

It seems likely that every Checklist you’ll ever make will also have a name. You can make this a requirement by writing your own init method that takes the name as a parameter. Then you can simply write:

list = Checklist(name: "Name of the checklist")

➤ Go to Checklist.swift and add the new init method:

init(name: String) {
  self.name = name
  super.init()
}

This initializer takes one parameter, name, and places it into the property called name.

Notice that while the parameter and property are both named name — they are two distinct entities. So, you use self.name to refer to the property (or instance variable, if you prefer that term).

If you used this code instead:

init(name: String) {
  name = name
  super.init()
}

Then Swift wouldn’t understand that the first name referred to the property.

To disambiguate, you use self. Recall that self refers to the object that you’re in, so self.name means the name variable of the current Checklist object.

➤ Go back to AllListsViewController.swift and add the following code to the end of viewDidLoad(), for real this time:

override func viewDidLoad() {
  . . .
  // Add placeholder data
  var list = Checklist(name: "Birthdays")
  lists.append(list)

  list = Checklist(name: "Groceries")
  lists.append(list)

  list = Checklist(name: "Cool Apps")
  lists.append(list)

  list = Checklist(name: "To Do")
  lists.append(list)
}

That’s a bit shorter than what was shown before, and it guarantees that new Checklist objects will now always have their name property filled in.

Note that you don’t write:

var list = Checklist.init(name: "Birthdays")

Even though the method is named init, it’s not a regular method. Initializers are only used to construct new objects and you write that as:

var object = ObjectName(parameter1: value1, parameter2: value2, . . .)

Depending on the parameters that you specified, Swift will locate the corresponding init method and call that.

Clear? Great! Let’s continue building the All Lists screen.

Displaying data in table view

➤ Change the tableView(_:numberOfRowsInSection:) method to return the number of objects in the new array:

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

➤ Finally, change tableView(_:cellForRowAt:) to fill in the cells for the rows:

override func tableView(_ tableView: UITableView,
             cellForRowAt indexPath: IndexPath)
             -> UITableViewCell {
  let cell = makeCell(for: tableView)
  // Update cell information
  let checklist = lists[indexPath.row]
  cell.textLabel!.text = checklist.name
  cell.accessoryType = .detailDisclosureButton

  return cell
}

➤ Run the app. It should look like this:

The table view shows Checklist objects
The table view shows Checklist objects

You now have a table view with cells representing Checklist objects. The rest of the screen doesn’t do much yet, but it’s a start.

The many ways to make table view cells

Creating a new table view cell in AllListsViewController is a little more involved than how it was done in ChecklistViewController. There you just did the following to obtain a new table view cell:

let cell = tableView.dequeueReusableCell(
              withIdentifier: "ChecklistItem", for: indexPath)

But here you have three separate bits of code to accomplish the same:

// At the top of the class implementation
let cellIdentifier = "ChecklistCell"
// In viewDidLoad
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
// In tableView(_:cellForRowAt:)
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)

The call to dequeueReusableCell(withIdentifier:for:) is still there, except that previously the storyboard had a prototype cell with that identifier and now it doesn’t.

There are four ways that you can make table view cells:

  1. Using prototype cells. This is the simplest and quickest way. You did this in ChecklistViewController.

  2. Using static cells. You did this for the Add/Edit Item screen. Static cells are limited to screens where you know in advance which cells you’ll have. The big advantage with static cells is that you don’t need to provide any of the data source methods (cellForRowAt etc.).

  3. Using a nib file. A nib (also known as a XIB) is like a mini storyboard that only contains a single customized UITableViewCell object. This is very similar to using prototype cells, except that you can do it outside of a storyboard.

  4. By hand, like what you did above. This is fairly similar to how you were supposed to do it in the early days of iOS, but you can get a little bit more closer to the metal, so to speak.

    When you create a cell by hand, you can specify a certain cell style, which gives you a cell with a preconfigured layout that already has labels and an image view.

    For the All Lists scene you’re using the “Default” style. Later on you’ll switch it to “Subtitle,” which gives the cell a second, smaller label below the main label. Then you’ll see how to go really old school.

Using standard cell styles means you don’t have to design your own cell layout. For many apps these standard layouts are sufficient, so that saves you some work.

Prototype cells and static cells can also use these standard cell styles. The default style for a prototype or static cell is “Custom,” which requires you to use your own labels, but you can change that to one of the built-in styles via Interface Builder.

And finally, a gentle warning: Sometimes you might see code that creates a new cell for every row rather than trying to reuse cells by dequeuing them. Don’t do that! Always ask the table view first whether it has a cell available that can be recycled, using one of the dequeueReusableCell methods. In case you hadn’t noticed, there are two dequeueReusableCell variants — you’ll learn about the second one later.

Creating a new cell for each row will cause your app to slow down, as object creation is slower than simply re-using an existing object. Creating all these new objects also takes up more memory, a precious commodity on mobile devices. For the best performance, reuse those cells!

Viewing the checklists

Right now, the data model consists of the lists array from AllListsViewController that contains a handful of Checklist objects. There is also a separate items array in ChecklistViewController with ChecklistItem objects.

You may have noticed that when you tap the name of a list, the Checklist screen slides into view but it currently always shows the same to-do items, regardless of which list you tapped on.

Each checklist should really have its own to-do items. You’ll work on that later on, as this requires a significant change to the data model.

As a start, let’s set the title of the Checklist screen to reflect the chosen checklist.

Setting the title of the screen

➤ Add a new instance variable to ChecklistViewController.swift:

var checklist: Checklist!

I’ll explain why the exclamation mark is necessary in a moment.

➤ Change viewDidLoad in ChecklistViewController.swift to:

override func viewDidLoad() {
  . . .
  title = checklist.name
}

This changes the title of the screen, which is shown in the navigation bar, to the name of the Checklist object.

You’ll pass the necessary Checklist object to ChecklistViewController when the segue is performed.

➤ In AllListsViewController.swift, update tableView(_:didSelectRowAt:) to the following:

override func tableView(_ tableView: UITableView,
           didSelectRowAt indexPath: IndexPath) {
  let checklist = lists[indexPath.row]
  performSegue(withIdentifier: "ShowChecklist",
                       sender: checklist)
}

As before, you use performSegue() to start the segue. This method has a sender parameter that you previously set to nil. Now you’ll use it to send along the Checklist object from the row that the user tapped on.

You can put anything you want into sender. If the segue is performed by the storyboard (rather than manually like you do here) then sender will refer to the control that triggered it, for example, the UIBarButtonItem object for the Add button, or the UITableViewCell for a row in the table.

But because you start this particular segue by hand, you can put whatever is most convenient into sender.

Putting the Checklist object into the sender parameter doesn’t pass it to ChecklistViewController yet. That happens in “prepare-for-segue,” which you still need to add for this view controller.

➤ Add the prepare(for:sender:) method to AllListsViewController.swift:

// MARK:- Navigation
override func prepare(for segue: UIStoryboardSegue,
                         sender: Any?) {
  if segue.identifier == "ShowChecklist" {
    let controller = segue.destination
                     as! ChecklistViewController
    controller.checklist = sender as? Checklist
  }
}

You’ve seen this method before. prepare(for:sender:) is called right before a segue happens from a view controller. Here you get a chance to set the properties of the new view controller before it becomes visible.

Inside prepare(for:sender:), you need to pass the ChecklistViewController the Checklist object from the row that the user tapped. That’s why you put that object in the sender parameter earlier.

You could have temporarily stored the Checklist object in an instance variable instead, but passing it along in the sender parameter is much easier and cleaner.

All of this happens a short time after ChecklistViewController is instantiated but just before ChecklistViewController’s view is loaded. That means its viewDidLoad() method is called after prepare(for:sender:).

At this point, the view controller’s checklist property is set to the Checklist object from sender, and viewDidLoad() can set the title of the screen accordingly.

The steps involved in performing a segue
The steps involved in performing a segue

This sequence of events is why the checklist property is declared as Checklist! with an exclamation point. That allows its value to be temporarily nil until viewDidLoad() happens. nil is normally not an allowed value for non-optional variables in Swift, but by using the ! you override that.

Does this sound an awful lot like optionals? The exclamation point turns checklist into a special kind of optional. It’s very similar to optionals with a question mark, but you don’t have to write if let to unwrap it. Such implicitly unwrapped optionals should be used sparingly and with care, as they do not have any of the anti-crash protection that normal optionals do.

➤ Run the app and notice that when you tap the row for a checklist, the next screen properly displays the checklist title.

The name of the chosen checklist now appears in the navigation bar
The name of the chosen checklist now appears in the navigation bar

Note that passing the Checklist object to the ChecklistViewController does not make a copy of it.

You only pass the view controller a reference to that object — any changes the user makes to that Checklist object are also seen by AllListsViewController.

Both view controllers have access to the exact same Checklist object. You’ll use that to your advantage later in order to add new ChecklistItems to the selected Checklist.

Typing Casts

In prepare(for:sender:) you do this:

override func prepare(for segue: UIStoryboardSegue,
                         sender: Any?) {
  . . .
  controller.checklist = sender as? Checklist
  . . .
}

What is that as? Checklist bit?

If you’ve been paying attention — of course you have! — then you’ve seen this “as something” used quite a few times now. This is known as a type cast.

A type cast tells Swift to interpret a value as having a different data type.

(It’s the opposite of what happens to certain actors in the movies. For them, typecasting results in always playing the same character; in Swift, a type cast actually changes the character of an object.)

Here, sender has type Any?, meaning that it can be any sort of object: a UIBarButtonItem, a UITableViewCell, or in this case, a Checklist. Thanks to the question mark it can even be nil.

But the controller.checklist property always expects a Checklist object – it wouldn’t know what to do with a UITableViewCell… Hence, Swift demands that you only put Checklist objects into the checklist property.

By writing sender as? Checklist, you tell Swift that it can safely treat sender as a Checklist object, if it can be used as a Checklist object, or to send nil if if there is an issue.

Another example of a typecast is:

let controller = segue.destination as! ChecklistViewController

The segue’s destination property refers to the view controller on the receiving end of the segue. But obviously the engineers at Apple could not predict beforehand that we would call it ChecklistViewController. Unlike the previous as? type cast, this one force unwraps the value to be of the type that you specified, there is not supposed to be any posibility of the type cast failing.

So you have to cast it from its generic type (UIViewController) to the specific type used in this app (ChecklistViewController) before you can access any of the properties specific to ChecklistViewController.

Don’t worry if some of this goes over your head right now. You’ll see plenty more examples of type casting in action.

The main reason you need all these type casts is for interoperability with the iOS frameworks that are written in Objective-C. Swift is less forgiving about types than Objective-C and requires you to be much more explicit about specifying the types of the various data items you work with.

Managing checklists

Let’s quickly add the Add / Edit Checklist screen. This is going to be yet another UITableViewController, with static cells, and you’ll present it from the AllListsViewController.

If the previous sentence made perfect sense to you, then you’re getting the hang of this!

Adding the view controller

➤ Add a new file to the project, ListDetailViewController.swift. You can use the Swift File template for this since you’ll be adding the complete view controller implementation by hand.

➤ Add the following to ListDetailViewController.swift:

import UIKit

protocol ListDetailViewControllerDelegate: class {
  func listDetailViewControllerDidCancel(
           _ controller: ListDetailViewController)

  func listDetailViewController(
           _ controller: ListDetailViewController,
           didFinishAdding checklist: Checklist)

  func listDetailViewController(
           _ controller: ListDetailViewController,
           didFinishEditing checklist: Checklist)
}

class ListDetailViewController: UITableViewController,
                                UITextFieldDelegate {
  @IBOutlet weak var textField: UITextField!
  @IBOutlet weak var doneBarButton: UIBarButtonItem!

  weak var delegate: ListDetailViewControllerDelegate?

  var checklistToEdit: Checklist?
}

I simply took the contents of ItemDetailViewController.swift and changed the names. Also, instead of a property for a ChecklistItem you’re now dealing with a Checklist.

➤ Add the viewDidLoad() method:

override func viewDidLoad() {
  super.viewDidLoad()

  if let checklist = checklistToEdit {
    title = "Edit Checklist"
    textField.text = checklist.name
    doneBarButton.isEnabled = true
  }
}

This changes the title of the screen if the user is editing an existing checklist, and it puts the checklist’s name into the text field.

➤ Also add the viewWillAppear() method to pop up the keyboard:

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  textField.becomeFirstResponder()
}

The Cancel and Done buttons

➤ Add the action methods for the Cancel and Done buttons:

// MARK:- Actions
@IBAction func cancel() {
  delegate?.listDetailViewControllerDidCancel(self)
}

@IBAction func done() {
  if let checklist = checklistToEdit {
    checklist.name = textField.text!
    delegate?.listDetailViewController(self,
                     didFinishEditing: checklist)
  } else {
    let checklist = Checklist(name: textField.text!)
    delegate?.listDetailViewController(self,
                      didFinishAdding: checklist)
  }
}

This should look familiar as well. It’s essentially the same as what the Add/Edit Item screen does.

To create the new Checklist object in done(), you use its init(name:) method and pass the contents of textField.text as the name parameter.

You cannot write this the way you did for ChecklistItems – this won’t work:

let checklist = Checklist()
checklist.name = textField.text!

Because Checklist does not have an init() method that takes no parameters, writing Checklist() results in a compiler error. It only has an init(name:) method, and you must always use that initializer to create new Checklist objects.

Other functionality

➤ Also make sure the user cannot select the table cell with the text field:

// MARK:- Table View Delegates
override func tableView(_ tableView: UITableView,
          willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  return nil
}

➤ And, finally, add the text field delegate methods that enable or disable the Done button depending on whether the text field is empty or not.

// MARK:- Text Field Delegates
func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {

  let oldText = textField.text!
  let stringRange = Range(range, in:oldText)!
  let newText = oldText.replacingCharacters(in: stringRange,
                                          with: string)
  doneBarButton.isEnabled = !newText.isEmpty
  return true
}

func textFieldShouldClear(_ textField: UITextField) -> Bool {
  doneBarButton.isEnabled = false
  return true
}

Again, this is the same as what you did in ItemDetailViewController. Let’s create the user interface for this new view controller in Interface Builder.

The storyboard

➤ Open the storyboard. Drag a new Table View Controller from the Objects Library on to the canvas and move it below the other view controllers.

Adding a new table view controller to the canvas
Adding a new table view controller to the canvas

➤ Select the new Table View Controller and go to the Identity inspector. Change its class to ListDetailViewController.

Control-drag from the yellow cirlce at the top of the All Lists scene to the new scene. Select Show from the Manual Segue section of the pop-up menu.

➤ Add a Navigation Item to the new scene.

➤ Change the navigation bar title from “Title” to Add Checklist. (The new scene should now appear as Add Checklist scene in the Document Outline.)

➤ Select the Navigation Item and set Large Title in the Attributes inspector to Never.

➤ Add Cancel and Done bar button items to the navigation item and hook them up to the action methods in the Add Checklist scene. Also connect the Done button to the doneBarButton outlet and uncheck its Enabled option.

Remember, you can Control-drag from a button to the view controller to connect it to an action method. To connect an outlet, do it the other way around: Control-drag from the view controller to the button.

Tip: My Xcode acted a bit buggy and wouldn’t let me drop the bar buttons on the navigation bar. If this happens to you too, drop them on the navigation item – now called Add Checklist – in the Document Outline. You can also Control-drag in the Document Outline to make the connections to the actions and the outlet.

➤ Change the table view to Static Cells, style Grouped. You only need one cell, so remove the bottom two.

➤ Drop a new Text Field on to the cell, adjust it’s size and position and then set up left, top, right and bottom Auto Layout constraints. Then, set the following configuration options via the Attributes inspector:

  • Border Style: none
  • Font size: 17
  • Placeholder text: Name of the List
  • (Optional) Clear Button: Appears while editing
  • Adjust to Fit: disabled
  • Capitalization: Sentences
  • Return Key: Done
  • Auto-enable Return key: check

➤ Control-drag from the view controller to the Text Field and connect it to the textField outlet.

➤ Then Control-drag the other way around, from the Text Field back to the view controller and choose delegate under Outlets. Now the view controller is the delegate for the text field.

➤ Connect the text field’s Did End on Exit event to the done action on the view controller.

This completes setting up the new view controller to be the Add / Edit Checklist screen:

The finished design of the ListDetailViewController
The finished design of the ListDetailViewController

Connecting the view controllers

➤ Go to the All Lists scene (the one titled “Checklists”) and drag a Bar Button Item on to its right navigation item. Change it to an Add button.

Control-drag from this new bar button to the Add Checklist scene below to add a new Show segue.

➤ Click on the new segue and name it AddChecklist.

➤ Click on the other segue (the one not connected the the Add buton) and name it EditChecklist.

Your storyboard should now look something like this:

The full storyboard: 1 navigation controller, 4 table view controllers
The full storyboard: 1 navigation controller, 4 table view controllers

Setting up the delegates

Almost there. You still have to make the AllListsViewController the delegate for the ListDetailViewController and then you’re done. Again, it’s very similar to what you did before.

➤ Declare the All Lists view controller to conform to the delegate protocol by adding ListDetailViewControllerDelegate to its class line.

You do this in AllListsViewController.swift:

class AllListsViewController: UITableViewController,
                              ListDetailViewControllerDelegate {

➤ Still in AllListsViewController.swift, extend prepare(for:sender:) to:

override func prepare(for segue: UIStoryboardSegue,
                         sender: Any?) {
  if segue.identifier == "ShowChecklist" {
    . . .
  } else if segue.identifier == "AddChecklist" {
    let controller = segue.destination
                     as! ListDetailViewController
    controller.delegate = self
  }
}

The first if doesn’t change. You’ve added a second if for the new “AddChecklist” segue that you just defined in the storyboard. As before, you look for the view controller and set its delegate property to self.

➤ Next, implement the following delegate methods in AllListsViewController.swift:

// MARK:- List Detail View Controller Delegates
func listDetailViewControllerDidCancel(
                  _ controller: ListDetailViewController) {
  navigationController?.popViewController(animated: true)
}

func listDetailViewController(
                  _ controller: ListDetailViewController,
     didFinishAdding checklist: Checklist) {
  let newRowIndex = lists.count
  lists.append(checklist)

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

  navigationController?.popViewController(animated: true)
}

func listDetailViewController(
                 _ controller: ListDetailViewController,
   didFinishEditing checklist: Checklist) {
  if let index = lists.firstIndex(of: checklist) {
    let indexPath = IndexPath(row: index, section: 0)
    if let cell = tableView.cellForRow(at: indexPath) {
      cell.textLabel!.text = checklist.name
    }
  }
  navigationController?.popViewController(animated: true)
}

These methods are called when the user presses Cancel or Done inside the new Add/Edit Checklist screen.

None of this code should surprise you. It’s exactly what you did before but now for the ListDetailViewController and Checklist objects.

➤ Also add the table view data source method that allows the user to delete checklists:

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

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

➤ Run the app. Now you can add new checklists and delete them again:

Adding new lists
Adding new lists

Note: If the app crashes, then go back and make sure you made all the connections properly in Interface Builder. It’s really easy to miss just one tiny thing, but even the tiniest of mistakes can bring the app crashing down in flames…

You can’t edit the names of existing lists yet though. That requires one last addition to the code.

To bring up the Edit Checklist screen, the user taps the blue accessory button in the ChecklistViewController that triggered a segue. You could use a segue here too. If you want to go that route, you’ve already set up a segue named “EditChecklist” on the storyboard that you can use for this purpose. But there’s another way.

This time you’re not going to use a segue at all, but load the new view controller by hand from the storyboard. Just because you can — and because it is good to know multiple ways to do the same thing.

Loading a view controller via code

➤ Add the following tableView(_:accessoryButtonTappedForRowWith:) method to AllListsViewController.swift. This method comes from the table view delegate protocol and the name is hopefully obvious enough for you to guess what it does.

override func tableView(_ tableView: UITableView,
   accessoryButtonTappedForRowWith indexPath: IndexPath) {

  let controller = storyboard!.instantiateViewController(
                   withIdentifier: "ListDetailViewController")
                   as! ListDetailViewController
  controller.delegate = self

  let checklist = lists[indexPath.row]
  controller.checklistToEdit = checklist

  navigationController?.pushViewController(controller,
                                 animated: true)
}

In this method, you create the view controller object for the Add/Edit Checklist screen and push it on to the navigation stack. This is roughly equivalent to what a segue would do behind the scenes. The view controller is embedded in a storyboard and you have to ask the storyboard object to load it.

Where did you get that storyboard object? As it happens, each view controller has a storyboard property that refers to the storyboard the view controller was loaded from. You can use that property to do all kinds of things with the storyboard, such as instantiating other view controllers.

The storyboard property is optional because view controllers are not always loaded from a storyboard. But this one is, which is why you can use ! to force unwrap the optional. It’s like using if let, but because you can safely assume storyboard will not be nil in this app, you don’t have to unwrap it inside an if statement.

The call to instantiateViewController(withIdentifier:) takes an identifier string, ListDetailViewController. That is how you ask the storyboard to create the new view controller. In your case, this will be the ListDetailViewController. Note that the identifier does not have to match the view controller class name — it could be any unique string value — even though we opted to use the view controller class name here.

You still have to set this identifier on the navigation controller; otherwise the storyboard won’t be able to find it. (And if you try to run the app without setting the identifier, it will crash.)

➤ Open the storyboard and select the List Detail View Controller. Go to the Identity inspector and set Storyboard ID to ListDetailViewController:

Setting the storyboard identifier
Setting the storyboard identifier

➤ That should do the trick. Run the app and tap some detail disclosure buttons.

(If the app crashes, make sure the storyboard is saved before you press Run.)

Are you still with me?

If at this point your eyes are glazing over and you feel like giving up: don’t. Learning new things is hard and programming doubly so. Set the book aside, sleep on it and come back in a few days. Chances are that in the mean time you’ll have an a-ha! moment where the thing that didn’t make any sense suddenly becomes clear as day.

If you have specific questions, join us on the forums at: forums.raywenderlich.com. We’re around most of the time and respond to questions fast. Many of our community members do as well. Don’t be embarrassed to ask for help!

You can find the project files for the app up to this point under 16 - Lists 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.