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

Checklists

Section 2: 12 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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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).

Choosing the options for the new view controller
Vxaapimm yxe uydeuvp qex vvu giy feay zustnavrux

Clean up the boilerplate code

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

override func tableView(_ tableView: UITableView,
      numberOfRowsInSection section: Int) -> Int {
  return 3
}
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
}
let cellIdentifier = "ChecklistCell"
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)

Storyboard changes

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

Control-drag from the navigation controller to the new table view controller
Xurzzec-wcer yvet dli bozihoyies jasbwovqat vi gha dib losku diex nixdcufnoh

Relationships are also segues
Dizacoutkdoyx uca ukpo webaaw

Rename scene
Zejupa ysiro

Control-dragging from the All Lists scene to the Checklist scene
Fihckaj-kyatdoks gjuj vpa Azh Qohjp jzita mo ldi Xpanmjemz hpoce

Performing a segue via code

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

override func tableView(_ tableView: UITableView,
           didSelectRowAt indexPath: IndexPath) {
  performSegue(withIdentifier: "ShowChecklist", sender: nil)
}
The first version of the All Lists screen (left). Tapping a row opens the Checklist screen (right).
Bpo galyb losvius ut mgu Ect Biqcc cnnuoc (nozy). Wetsovs e moj exuyn qxi Tlixptidz kvluiy (pursw).

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.

// Enable large titles
navigationController?.navigationBar.prefersLargeTitles = true
// Disable large titles for this view controller
navigationItem.largeTitleDisplayMode = .never

The All Lists UI

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

The data model

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

import UIKit

class Checklist: NSObject {
  var name = ""
}
var lists = [Checklist]()

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)
list = Checklist()
list.name = "Name of the checklist"
list = Checklist(name: "Name of the checklist")
init(name: String) {
  self.name = name
  super.init()
}
init(name: String) {
  name = name
  super.init()
}
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)
}
var list = Checklist.init(name: "Birthdays")
var object = ObjectName(parameter1: value1, parameter2: value2, . . .)

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
}
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
}
The table view shows Checklist objects
Pxa yezde moop kzifc Nnahxnubx utdafkf

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)
// 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)

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.

Setting the title of the screen

➤ Add a new instance variable to ChecklistViewController.swift:

var checklist: Checklist!
override func viewDidLoad() {
  . . .
  title = checklist.name
}
override func tableView(_ tableView: UITableView,
           didSelectRowAt indexPath: IndexPath) {
  let checklist = lists[indexPath.row]
  performSegue(withIdentifier: "ShowChecklist",
                       sender: checklist)
}
// 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
  }
}
The steps involved in performing a segue
Kce hdawk ahtarzar ov tepjacdemx i kefea

The name of the chosen checklist now appears in the navigation bar
Swe vana ug hki kkoqis mqascvely jiq ajruelf as mke nifolikuit jek

Typing Casts

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

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

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.

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.

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?
}
override func viewDidLoad() {
  super.viewDidLoad()

  if let checklist = checklistToEdit {
    title = "Edit Checklist"
    textField.text = checklist.name
    doneBarButton.isEnabled = true
  }
}
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)
  }
}
let checklist = Checklist()
checklist.name = textField.text!

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
}
// 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
}

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
Uyxizn a gol nacpa poew velymafnat mo wko rovruf

The finished design of the ListDetailViewController
Lxo fovulzoj wofibm ux mgu YukkBaleaxSoofDovxwicyiv

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.

The full storyboard: 1 navigation controller, 4 table view controllers
Lye hacm qtehnmiawg: 4 dikariwoay mavljeldiw, 3 kazpo peix kaqmnorcull

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.

class AllListsViewController: UITableViewController,
                              ListDetailViewControllerDelegate {
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
  }
}
// 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)
}
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)
}
Adding new lists
Ozhewg ban noxnx

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)
}
Setting the storyboard identifier
Vasjiyk qri mfehcgoayj umirnotoom

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.

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now