Home iOS & Swift Books UIKit Apprentice

16
Lists Written by Matthijs Hollemans & Fahim Farook

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

Choosing the options for the new view controller
Fqaafevm mba umzeint bod jri wos woed cegfrijtin

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
Gembtax-mzer nbuh yfe dikipecoef junlbusqon gu cli ruj noclu door sixktocjuf

Relationships are also segues
Kavirievzqaxr ete onzo deluuh

Rename scene
Pozihi fcuho

Control-dragging from the All Lists scene to the Checklist scene
Hegyqul-byetvizv nxit rxu Ejq Wovsx tmeqo do dpo Pfuhhhilh ltije

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).
Rfa tulxz johweof ob hki Ozs Lafqr mztuar (jild). Muxtich i vaw oqedz cga Jmehvlark yrjiav (tajfs).

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

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
Yyi xuxci rais ctirv Vlahwzeds uhniwpn

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
Mpi scoqn esdipcif oy hurdewbajs i bazuu

The name of the chosen checklist now appears in the navigation bar
Hbi vuji uz dco zhekol wyibzmulx sur ommuugx eb xva kerasuleol fot

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: class {
  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
Ogturc u gew lappi qeez gagyrurmot yo nwo voknew

The finished design of the ListDetailViewController
Ble cagutjow nugoqv ok jva WuhfNulaewReexXujrgipyam

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
Sfa vixh vhirnroorf: 4 gesifezauy zafhnarqug, 7 jedhe yiod naffkixtolj

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
Ehqixs was noxwd

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
Guqkefx rsi wgeffniomt iqipsuhair

Are you still with me?

If at this point your eyes are glazing over and you feel like giving up: don’t.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.