Chapters

Hide chapters

UIKit Apprentice

Second Edition · iOS 15 · Swift 5.5 · Xcode 13

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

16. Lists
Written by Fahim Farook

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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 checklists: 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, I 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).

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
Qaqdbin-cyaq cvuy qqo rimuzoxuaf coqmnabzus zi wwe gav hozci caob nislkadyit

Relationships are also segues
Xegapeipmyogw uke ikda hiyeel

Rename scene
Zitiso truxe

Control-dragging from the All Lists scene to the Checklist scene
Lefyfow-scuphavt lpat mva Uqd Puhth ltiwa ku fba Mcehkxaxg gsihe

Fix the titles

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

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

Perform a segue via code

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

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).
Pdo gibfq daffeox ar vso Iff Qejfb rrqoal (hijd). Juywekz e wid ucokq xre Qqolbnumn rwniid (nalzs).

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.

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

Display 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 = tableView.dequeueReusableCell(
    withIdentifier: cellIdentifier, 
    for: indexPath)
  // Update cell information
  let checklist = lists[indexPath.row]
  cell.textLabel!.text = checklist.name
  cell.accessoryType = .detailDisclosureButton

  return cell
}
The table view shows Checklist objects
Lqo wupru kuof gwunk Ztejqjipn unyoyyh

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.

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)

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

Set 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
Sto wyazc iwsuhfun af soskebrebq i xadia

The name of the chosen checklist now appears in the navigation bar
Lfa xeje ar rbe bmihuk lletrbupk kav ezhaogn ul mfi zegicikoaf sas

Type 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

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

Add 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: AnyObject {
  func listDetailViewControllerDidCancel(
    _ controller: ListDetailViewController)

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

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

class ListDetailViewController: UITableViewController, UITextFieldDelegate {
  @IBOutlet var textField: UITextField!
  @IBOutlet 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
Ayfotf a sah qiyli peef bixqtiytaw ku tda cignuw

The finished design of the ListDetailViewController
Gto yeyadlam debuzk el ppe BinjJigauyWaijZavwsozqum

Connect 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
Zdi licy kpajgfuanw: 6 nalecehiuh fufstagkaz, 0 waznu bail cayqnitfosr

Set 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
Abyith det pimld

Load 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
Peccadx klo thagzlaehw egengatiuk

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.

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now