Home iOS & Swift Books iOS Apprentice

10
A “Checkable” List Written by Joey deVilla

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

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Even though the app isn’t complete, it’s still a problem that it’s not living up to its name. It displays a list of items, but it doesn’t show if they’re checked or not. It doesn’t even track if an item is checked or not. And it most certainly doesn’t let the user check or uncheck items!

In this chapter, the goal is to fix these problems by:

  • Creating checklist item objects: Swift is an object-oriented programming language, which means that sooner or later, you’re going to have to “roll your own” objects. And by “sooner or later,” I mean now. These objects will store each checklist item’s name, “checked” status and possibly more.
  • Toggling checklist items: It’s not a checklist app until the user can check and uncheck items. It’s time to make this app live up to its name!
  • Key points: A quick review of what you’ve learned in this chapter.

Creating checklist item objects

Arrays: A review

In its current state, the app stores the checklist items in an array named checklistItems. If you open the starter project for this chapter and look inside the file ContentView.swift, you’ll see this at the start of ContentView:

@State var checklistItems = [
  "Walk the dog",
  "Brush my teeth",
  "Learn iOS development",
  "Soccer practice",
  "Eat ice cream",
]
The checklistItems array with its current contents
Zwu mfuwgvevvOxivm usjif wabx oqf loyjanc buhxaxrd

The checklistItems array with each element holding two values
Nhi spoltlohnOlecw oxpob pulz oiwp awexuwy nigzetq lbe gewaiy

The checklistItems array with each element holding a single package containing all the value for a checklist item
Pmu xvacfxorrAmaqj uvtub wijp iarw aqogefx levzaqt o bextpu yagcoja jafkaexicg uwg vko xoyea kek u lnugqpewg ewaw

structs vs. objects or instances

Until now, I’ve been referring to structs as objects to keep things simple. This isn’t technically correct, but it was good enough to get you started. After all, you’ve managed to build both Bullseye and a rudimentary checklist app without using the technically correct terms, right?

A struct and its object instances
A sxzibf ivk eyw ubvewb asbqejnit

Creating a struct for checklist items

Let’s define the ChecklistItem struct. It will specify that its instances will have two properties:

struct ChecklistItem {
  var name: String
  var isChecked: Bool = false
}
ChecklistItem(name: "Learn iOS development", isChecked: true)
ChecklistItem(name: "Walk the dog", isChecked: false)
ChecklistItem(name: "Walk the dog")
Xcode will try to help you when you’re instantiating an object
Dyogu pedv czx me vuhd foa grel piu’ti aqrkixcoewuqm et isyavf

@State var checklistItems = [
  ChecklistItem(name: "Walk the dog"),
  ChecklistItem(name: "Brush my teeth"),
  ChecklistItem(name: "Learn iOS development", isChecked: true),
  ChecklistItem(name: "Soccer practice"),
  ChecklistItem(name: "Eat ice cream", isChecked: true),
]

Showing an item’s “checked” status

Now that the checklistItems array is filled with checklistItem instances instead of Strings, we need to update the way that ContentView displays checklist items. Currently, it’s set up to display the contents of an array of strings, and it has no sense of whether an item is checked or not.

List {
  ForEach(checklistItems, id: \.self) { item in
    Text(item)
  }
  .onDelete(perform: deleteListItem)
  .onMove(perform: moveListItem)
}
The HStack containing the items in a checklist row
Cve XNpapk motxaequgy nho ebodw id a qzerjsamw haf

ForEach(checklistItems, id: \.self) { checklistItem in
  HStack {
    Text(checklistItem.name)
    Spacer()
    if checklistItem.isChecked {
      Text("✅")
    } else {
      Text("🔲")
    }
  }
}
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)
What does this error message mean?
Xfup jauw rgeg abroc xagkufa niov?

ForEach(checklistItems, id: \.self) { checklistItem in
ForEach(checklistItems, id: \.self.name) { checklistItem in
The app now displays items’ “checked” status
Xqu awd ton silcvemz abumy’ “tquzxoc” dtolug

What happens when two checklist items have the same name?

Let’s look at the ForEach line again:

ForEach(checklistItems, id: \.self.name) { checklistItem in
@State var checklistItems = [
  ChecklistItem(name: "Walk the dog"),
  ChecklistItem(name: "Brush my teeth"),
  ChecklistItem(name: "Walk the dog", isChecked: true),
  ChecklistItem(name: "Soccer practice"),
  ChecklistItem(name: "Walk the dog", isChecked: true),
]
The checklist, with multiple “Walk the dog” items, all unchecked
Hne svaplyavp, cayv varxiyyu “Vics cge dam” ahadh, ijj okdtebsam

A better identifier for checklist items

There’s a simple fix for this, and it involves giving each ChecklistItem instance a unique “fingerprint” so that it can be distinguished from other instances, even those with identical name and isChecked properties.

struct ChecklistItem: Identifiable {
  let id = UUID()
  var name: String
  var isChecked: Bool = false
}
struct ChecklistItem: Identifiable {
let id = UUID()
There’s no option to set a predefined constant of a struct during instantiation
Pwizi’b pa ejbeoy cu haf a zlituvuyon mobtcemz ok e qmmath juhuhl edhyelyoeseoy

ForEach(checklistItems) { checklistItem in
The checklist with properly identified multiple “Walk the dog” items
Qsa wpehyyuzz lerd bpotistj obipdofiaf parrokju “Qusx mvu tec” ozoyx

Using a little less code with the ternary conditional operator

Here’s the code in the ForEach view that determines whether the checked or unchecked emoji is displayed for a checklist item:

if checklistItem.isChecked {
  Text("✅")
} else {
  Text("🔲")
}
Text(checklistItem.isChecked ? "✅" : "🔲")

A quick check before moving on

With Checklist now able to track the “checked” status of checklist items, you’re a little closer to a working checklist app.

Restoring the checklist

➤ Change the declaration for the ChecklistItems array back to the original:

@State var checklistItems = [
  ChecklistItem(name: "Walk the dog", isChecked: false),
  ChecklistItem(name: "Brush my teeth", isChecked: false),
  ChecklistItem(name: "Learn iOS development", isChecked: true),
  ChecklistItem(name: "Soccer practice", isChecked: false),
  ChecklistItem(name: "Eat ice cream", isChecked: false),
]

Reviewing the code

The code in ContentView.swift, minus the comments at the start, should look like this:

import SwiftUI

struct ChecklistItem: Identifiable {
  let id = UUID()
  var name: String
  var isChecked: Bool = false
}


struct ContentView: View {
  
  // Properties
  // ==========
  
  @State var checklistItems = [
    ChecklistItem(name: "Walk the dog", isChecked: false),
    ChecklistItem(name: "Brush my teeth", isChecked: false),
    ChecklistItem(name: "Learn iOS development", isChecked: true),
    ChecklistItem(name: "Soccer practice", isChecked: false),
    ChecklistItem(name: "Eat ice cream", isChecked: true),
  ]

  // User interface content and layout
  var body: some View {
    NavigationView {
      List {
        ForEach(checklistItems) { checklistItem in
          HStack {
            Text(checklistItem.name)
            Spacer()
            Text(checklistItem.isChecked ? "✅" : "🔲")
          }
          .onTapGesture {
            print("checklistitem name: \(checklistItem.name)")
          }
        }
        .onDelete(perform: deleteListItem)
        .onMove(perform: moveListItem)
      }
      .navigationBarItems(trailing: EditButton())
      .navigationBarTitle("Checklist")
      .onAppear() {
        self.printChecklistContents()
      }
    }
  }
  
  
  // Methods
  // =======
  
  func printChecklistContents() {
    for item in checklistItems {
      print(item)
    }
  }
  
  func deleteListItem(whichElement: IndexSet) {
    checklistItems.remove(atOffsets: whichElement)
    printChecklistContents()
  }
  
  func moveListItem(whichElement: IndexSet, destination: Int) {
    checklistItems.move(fromOffsets: whichElement, toOffset: destination)
    printChecklistContents()
  }
}


// Preview
// =======

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Checking the Canvas

If you haven’t been looking at the app in the Canvas lately, now’s a good time! SwiftUI does its best to interpret your code to give you a live preview of your work as you enter it. If you don’t see the Canvas, show it by selecting it in the menu in the upper right corner of the editor:

Showing the Canvas
Nmekadj jlu Coxpik

Looking at the code and the Canvas
Zuivevz ip lbe vamo ops nxa Xezqed

Toggling checklist items

Finding out when the user tapped a list item

The app now tracks each item’s “checked” status and can display it to the user. It’s time to give the user the ability to check and uncheck items by tapping on them!

ForEach(checklistItems) { checklistItem in
  HStack {
    Text(checklistItem.name)
    Spacer()
    Text(checklistItem.isChecked ? "✅" : "🔲")
  }
  .onTapGesture {
    print("checklistitem name: \(checklistItem.name)")
  }
}
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)
{ checklistItem in
  HStack {
    Text(checklistItem.name)
    Spacer()
    Text(checklistItem.isChecked ? "✅" : "🔲")
  }
  .onTapGesture {
    print("checklistitem name: \(checklistItem.name)")
  }
}
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)
.onTapGesture {
  // Code to perform when the user taps a list item goes here
}
var body: some View {
  NavigationView {
    List {
      ForEach(checklistItems) { checklistItem in
        HStack {
          Text(checklistItem.name)
          Spacer()
          Text(checklistItem.isChecked ? "✅" : "🔲")
        }
      }
      .onDelete(perform: deleteListItem)
      .onMove(perform: moveListItem)
      .onTapGesture {
        print("The user tapped a list item!")
      }
    }
    .navigationBarItems(trailing: EditButton())
    .navigationBarTitle("Checklist")
    .onAppear() {
      self.printChecklistContents()
    }
  }
}
.onTapGesture {
  print("The user tapped a list item!")
}
The user tapped a list item
Tci udor fecdop a doyl ufuw

Finding out which item the user tapped

It’s good to know that the user tapped a list item, but it’s even better to know which item.

.onTapGesture {
  print("The user tapped \(checklistItem.name).")
}
Xcode says that checklistItem is unresolved
Dwuyi govt lpow cjahxjetjIvug up evvihiqcon

checklistItem’s scope
fcohtbovcEkep’p ffoho

var body: some View {
  NavigationView {
    List {
      ForEach(checklistItems) { checklistItem in
        HStack {
          Text(checklistItem.name)
          Spacer()
          Text(checklistItem.isChecked ? "✅" : "🔲")
        }
        .onTapGesture {
          print("The user tapped \(checklistItem.name).")
        }
      }
      .onDelete(perform: deleteListItem)
      .onMove(perform: moveListItem)
    }
    .navigationBarItems(trailing: EditButton())
    .navigationBarTitle("Checklist")
    .onAppear() {
      self.printChecklistContents()
    }
  }
}
Seeing which item the user tapped
Douohc fguwj iwif nli otuc wobpor

Checking and unchecking a checklist item

Tapping an item in the list should change its “checked” status. If the item is unchecked, tapping it should change it to checked. Conversely, tapping a checked item should uncheck it.

.onTapGesture {
  if checklistItem.isChecked {
    checklistItem.isChecked = false
  } else {
    checklistItem.isChecked = true
  }
}
.onTapGesture {
  checklistItem.isChecked.toggle()
}
checklistItem is a let constant
lgakzqextObop as o qeq cujbhudl

.onTapGesture {
  self.checklistItems[0].isChecked.toggle()
}
Finding the matching item in checklistItems
Tisrigf mne qukffoyh upuv ox pdixcvefvAsofr

let myList = [
  "Alpha",
  "Bravo",
  "Charlie",
  "Delta",
]
let charlieIndex = myList.firstIndex(of: "Charlie")
let egbertIndex = myList.firstIndex(of: "Egbert")

Introducing nil and its friend, if let

nil is a special value, and it means “no value.” nil doesn’t mean 0, because 0 is a value. When firstIndex(of:) returns 0, it means that the first item that matches your search criteria is in the array’s first element. When firstIndex(of:) returns nil, it means that the array doesn’t have any items that match your search criteria is in the array’s first element.

let result = someOperation()
if result != nil {
  // Do something with result
}
if let result = someOperation() {
  // Do something with result
}

Finding a specific item in checklistItems

The firstIndex(of:) method is good for doing simple matches. Such as the one shown in the previous example, where we’re determining the location of “Charlie” in an array of names. We need a method that allows us to get the location of an object with a specific id value in an array of ChecklistItem instances. That method is the firstIndex(where:) method.

.onTapGesture {
  if let matchingIndex = self.checklistItems.firstIndex(where: { $0.id == checklistItem.id }) {
    self.checklistItems[matchingIndex].isChecked.toggle()
  }
  self.printChecklistContents()
}
A working checklist, as seen in the Simulator and debug console
O butwucr hmacqniwj, oh hiid uz zso Jojecajox oqq lozux dowwuha

if let matchingIndex = self.checklistItems.firstIndex(where: { $0.id == checklistItem.id }) {
{ $0.id == checklistItem.id }
self.checklistItems[matchingIndex].isChecked.toggle()

Fixing the “dead zone”

For each row in the list, the space between the item’s name and its checkbox is a “dead zone.” Tapping on it doesn’t check or uncheck the checkbox. That’s an annoying quirk. It might make your user think that your app is broken, that you’re a terrible programmer and perhaps even put a curse on you, the accursed developer and the seven generations to come after you. Let’s see what we can do about sparing you and your descendants from that horrible fate.

.background(Color.white) // This makes the entire row clickable
var body: some View {
  NavigationView {
    List {
      ForEach(checklistItems) { checklistItem in
        HStack {
          Text(checklistItem.name)
          Spacer()
          Text(checklistItem.isChecked ? "✅" : "🔲")
        }
        .background(Color.white) // This makes the entire row clickable
        .onTapGesture {
          if let matchingIndex =
            self.checklistItems.firstIndex(where: { $0.id == checklistItem.id }) {
            self.checklistItems[matchingIndex].isChecked.toggle()
          }
          self.printChecklistContents()
        }
      }
      .onDelete(perform: deleteListItem)
      .onMove(perform: moveListItem)
    }
    .navigationBarItems(trailing: EditButton())
    .navigationBarTitle("Checklist")
    .onAppear() {
      self.printChecklistContents()
    }
  }
}

Key points

In this chapter, you did the following:

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:

© 2021 Razeware LLC

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