Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

10. A “Checkable” List
Written by Joey deVilla

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",
]

You may notice that checklistItems’s last element, “Eat ice cream,” has a comma , after it. Swift will ignore it, but that trailing comma makes it easier for you to add additional elements to the array in the future. It’s a good idea to use tricks like this that make your programs easy to update.

Arrays are ordered lists of objects, and checklistItems is a specific array instance that contains the five items that the checklist contains when the app is launched:

The checklistItems array with its current contents
The checklistItems array with its current contents

checklistItems is missing information. It stores the name of each item, but it doesn’t store each item’s “checked” status — that is, is the item checked or not? What we need is a checklistItems array where each element stores a checklist item’s name and “checked” status:

The checklistItems array with each element holding two values
The checklistItems array with each element holding two values

While an array can hold many items, its individual elements are like regular variables: They can hold only one item at a time.

What we need is a single package for each checklist item that we can use to hold its name, its “checked” status and any other data.

That package, being a single item, could be put into an array element or ordinary variable. Using these packages, the checklistItems array would look like this:

The checklistItems array with each element holding a single package containing all the value for a checklist item
The checklistItems array with each element holding a single package containing all the value for a checklist item

Here’s the good news: These things exist, and you’ve already been using them. They’re objects or, more accurately, instances of structs.

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?

An analogy that gets used all the time in object-oriented programming is the “blueprint and houses” analogy. A blueprint is a set of drawings that describes a house.

The blueprint is used to build one or more houses. The houses based on the same blueprint are identical in structure. They’re made with the same number of rooms with the same dimensions, but each house is a unique instance and will hold different people, furniture and other possessions.

You should think of a struct as the blueprint for objects. Just as you don’t live in a blueprint, your app doesn’t create structs to be components in your program. Instead, your program creates objects or instances based on the information in the struct.

For this app, you’ll code a struct named ChecklistItem that will be the blueprint for checklist item objects. When the app runs, instances of ChecklistItem will be created for each item in the list: “Walk the dog,” “Brush my teeth,” “Learn iOS development” and so on.

Each of these objects will hold two pieces of data: The checklist item’s name and its “checked” status.

A struct and its object instances
A struct and its object instances

Creating a struct for checklist items

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

  • The name of the checklist item, which we’ll call name. Since this will contain text data, this will be a String property. The user should be able to change the name of checklist items, so this should be a variable, which we specify with the var keyword.
  • The “checked” status of the checklist item. This is a true/false value, so it will be a Bool (Boolean) property. Since it’s a Boolean property, we’ll follow the convention of giving this property a name that begins with the word “is”: isChecked. The user should be able to check and uncheck checklist items, so this should also be a variable.

When someone adds an item to our checklist, we’ll assume the item is incomplete. After all, that’s why checklists exist in the first place. We should set up ChecklistItem so that unless otherwise indicated, its initial “checked” status should be “unchecked.”

With this criteria, there’s enough information to define the ChecklistItem struct.

Note that ChecklistItem starts with an uppercase “C.” In programming, things that act like blueprints typically have names that begin with uppercase letters. Meanwhile, objects based on those blueprints usually have names that start with lowercase letters. It’s a convention that programmers use to make their code easier to understand.

➤ Add the following to ContentView, just after the import SwiftUI line and before the struct ContentView: View { line:

struct ChecklistItem {
  var name: String
  var isChecked: Bool = false
}

The declaration for the name property simply says that it’s a String property. The declaration for isChecked ends with = false, which means that isChecked has a default value of false.

Now that you’ve defined the ChecklistItem blueprint, you can create objects based on it. There are a couple of ways you can refer to this process: Creating ChecklistItem objects, creating ChecklistItem instances or instantiating ChecklistItem objects or instances. Any of these are correct. They all describe making a new thing based on a design for that thing.

To create an object or instance of a struct, use the struct’s name followed by parentheses containing the struct’s properties and the values for those properties. For example, to create a ChecklistItem object for “Learn iOS development” that is checked, you’d write this line of code:

ChecklistItem(name: "Learn iOS development", isChecked: true)

For a ChecklistItem object for “Walk the dog” that is unchecked, you can initialize it a couple of ways. First, there’s the complete way:

ChecklistItem(name: "Walk the dog", isChecked: false)

Since isChecked has a default value of false, you can simply just provide a value for the name parameter and skip providing a value for isChecked, which will cause it to default to false:

ChecklistItem(name: "Walk the dog")

When you’re creating a new instance of a struct, Xcode will try to help you by showing you the options. Here’s what it will show you when you’re making a ChecklistItem object:

Xcode will try to help you when you’re instantiating an object
Xcode will try to help you when you’re instantiating an object

Let’s update the checklistItems array by replacing the Strings that currently fill it with ChecklistItem instances. We want the same item names, and they should have these “checked” statuses:

  • Walk the dog — unchecked
  • Brush my teeth — unchecked
  • Learn iOS development — checked
  • Soccer practice — unchecked
  • Eat ice cream — checked

➤ Edit checklistItems so that it looks like this:

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

Here’s the part of ContentView’s body property that displayed the contents of checklistItems when it was an array of strings:

List {
  ForEach(checklistItems, id: \.self) { item in
    Text(item)
  }
  .onDelete(perform: deleteListItem)
  .onMove(perform: moveListItem)
}

We need to change the contents of the ForEach view so that it displays both the name and checked” status of each checklist item. The name should appear on the left side of the row, while the checkmark should appear on the right side. This sounds like a job for an HStack, a couple of Text views and a Spacer between them, arranged like this:

The HStack containing the items in a checklist row
The HStack containing the items in a checklist row

➤ Change the ForEach view in body to the following:

ForEach(checklistItems, id: \.self) { checklistItem in
  HStack {
    Text(checklistItem.name)
    Spacer()
    if checklistItem.isChecked {
      Text("✅")
    } else {
      Text("🔲")
    }
  }
}
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)

To get the ✅ emoji, type control++space to get the emoji selector and enter check into the Search text field. To get the 🔲 character, enter square into the emoji selector’s Search text field and scroll through the results to find it.

You’re almost ready to run the app and see the results of the changes you made. But first, there’s the matter of this error message:

What does this error message mean?
What does this error message mean?

Hooray for cryptic error messages! What Xcode is trying to tell you is that it’s running into trouble on this line:

ForEach(checklistItems, id: \.self) { checklistItem in

The part of the line where it’s running into trouble is id: \.self. The id parameter of ForEach tells SwiftUI how to identify each element in the data provided to it, which in this case is checklistItems. When checklistItems was an array of strings, we told ForEach to simply use the value of the string as a way of distinguishing one element from another, and it worked. Now that checklistItems is an array of ChecklistItem instances, \.self refers to a whole ChecklistItem instance, which is a blob of data.

We can fix this by changing the id parameter so that SwiftUI distinguishes between each ChecklistItem instance by its name property.

➤ Change the ForEach line to the following:

ForEach(checklistItems, id: \.self.name) { checklistItem in

This revised line of code says “loop through all the items in checklistItems, using each item’s name property to uniquely identify it, and within each loop through checklistItems, put the current item inside the checklistItem variable.”

You should notice that Xcode’s cryptic error message has disappeared. Will the app compile and run? There’s an easy way to find out:

➤ Run the app. Items in the list now have a checked and unchecked status:

The app now displays items’ “checked” status
The app now displays items’ “checked” status

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

As I said earlier, setting the id parameter to \.self.name tells ForEach to use each item’s name property as a way of uniquely identifying it. What happens if two or more items have the same name? Let’s find out.

➤ Change the declaration of checklistItems so that “Walk the dog” appears three times, with two of them checked:

@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),
]

Before you run the app, try to guess what this change will do.

➤ Run the app. You’ll see this:

The checklist, with multiple “Walk the dog” items, all unchecked
The checklist, with multiple “Walk the dog” items, all unchecked

The checklist has three “Walk the dog” items in the right places, but they’re all unchecked. That’s because the app is identifying items by name, and the first “Walk the dog” item it saw was the unchecked one. It thinks that the second and third instances, both of which are supposed to be checked, are the same instance as the first one, so it thinks they’re all unchecked.

If you’ve ever been in a situation with someone with the same name as you and someone called out your name, you know this sort of confusion.

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.

➤ Change the declaration of ChecklistItem so that it looks like this:

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

You just made two changes to ChecklistItem. The first is in the first line:

struct ChecklistItem: Identifiable {

The line still defines a struct named ChecklistItem. The addition to the end of the line says that ChecklistItem is also a kind of Identifiable. Remember, in most cases in Swift, the “:” character means “is a kind of.”

You’re probably wondering what Identifiable is. It’s a protocol, which in case you’ve forgotten, is an agreement for an object blueprint to provide some kind of feature or service by including specific properties and methods. The Identifiable protocol is an agreement that an object blueprint adopts or conforms to to guarantee that all its instances can be uniquely identified — hence the name “Identifiable.”

Identifiable is a simple protocol. For an object blueprint to adopt it, it needs to do only one thing: Include an id property whose value is guaranteed to be different for every object. Luckily, Apple operating systems have a built-in struct called UUID, which generates a universally unique value (a UUID, short for “universally unique identifier”) every time it’s called. And I’m not kidding. By universally unique, I mean that if you took billions of UUID generators and had them generate billions of UUIDs a day for billions of years, the odds of any two of them generating the same UUID would still be practically zero.

This brings us to the second change to ChecklistItem, which is the addition of this line:

let id = UUID()

This adds a property named id to ChecklistItem. The let makes it a constant, which means its value can be set only once when the object is created. UUID() creates a new instance of UUID, which creates a new universally unique identifier value. This value is put into id.

id is a constant, and its value is set when the ChecklistItem instance is created. This means that you don’t have to set it when creating a new ChecklistItem instance.

You won’t even get the opportunity to set its value. As this Xcode screenshot shows, there won’t be an option to set id’s value during instantiation:

There’s no option to set a predefined constant of a struct during instantiation
There’s no option to set a predefined constant of a struct during instantiation

With these changes, we’ve upgraded ChecklistItem so it now comes with a “fingerprint” in the form of the id property that uniquely identifies every instance. With this change comes a bonus: You no longer have to tell the ForEach view how to uniquely identify instances of ChecklistItem anymore, because they now conform to the Identifiable protocol.

➤ Change the ForEach line to the following:

ForEach(checklistItems) { checklistItem in

Note the change: It’s now ForEach(checklistItems) instead of ForEach(checklistItems, id: \.self.name) because each ChecklistItem now has the ability to uniquely identify itself to ForEach.

➤ Run the app. Now that each ChecklistItem instance has its own unique identifier, the app can properly distinguish between items, even if they have the same name:

The checklist with properly identified multiple “Walk the dog” items
The checklist with properly identified multiple “Walk the dog” items

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("🔲")
}

The pattern — “if this condition is met, use this value; otherwise use this other value” — is one you’ll often use in programming. In fact, it’s used so often that Swift and many other programming languages use a special shorthand that condenses this sort of decision down to a single line. Using this shorthand, replace the code above with the following:

Text(checklistItem.isChecked ? "✅" : "🔲")

This shorthand is called the ternary conditional operator, or ternary operator. “Ternary” refers to the fact that it has three parts:

  • A condition that is evaluated as either true or false. This is the part that comes before the ?.
  • The “true” outcome. This is the output if the condition evaluates to true, and it appears between the ? and the :.
  • The “false” outcome. This is the output if the condition evaluates to false, and it appears after :.

With the ternary operator, what once took five lines of code now takes just one. When you look at others’ code, which is a great way to learn, you’ll find that many programmers prefer to use the ternary operator whenever possible.

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.

Before moving to the next step — giving the user the ability to check and uncheck items — let’s restore the checklist and review the code. Remember, it has some duplicate items right now.

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()
    }
}

If you’re new to programming in general, this may look like a lot of code. You might be surprised to discover that a checklist app that can do as much as this one, even in its incomplete state, would require considerably more code if you did it using UIKit (the previous way of writing iOS apps), on Android or various web application frameworks.

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
Showing the Canvas

Press the Resume button, and you should see your app:

Looking at the code and the Canvas
Looking at the code and the Canvas

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!

Let’s look at the ForEach view inside body:

ForEach(checklistItems) { checklistItem in
  HStack {
    Text(checklistItem.name)
    Spacer()
    Text(checklistItem.isChecked ? "✅" : "🔲")
  }
  .onTapGesture {
    print("checklistitem name: \(checklistItem.name)")
  }
}
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)

For every item in the bundle of data that you pass to ForEach, which in this case is checkListItems, it creates a view as defined by the stuff in the braces that follow the ForEach keyword, which is this code:

{ checklistItem in
  HStack {
    Text(checklistItem.name)
    Spacer()
    Text(checklistItem.isChecked ? "✅" : "🔲")
  }
  .onTapGesture {
    print("checklistitem name: \(checklistItem.name)")
  }
}

The checklistItem at the start of this block of code contains the current checklist item. The first time through the loop, checklistItem contains the “Walk the dog” checklist item.

The second time, it contains the “Brush my teeth” item, and so on. As a result, each item in the checklist gets its own view: An HStack containing two Text views that show the item’s name and “checked” status, and a Spacer between them.

There are also calls to methods at the end of the ForEach view:

.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)

These attach instructions to each item in the List for when the user opts to delete and move list items, and we wrote those instructions in the previous chapter.

If there are methods for responding to events where the user deletes or moves a list item, there must be one for responding to the user tapping a list item.

There is, and it’s called onTapGesture(), and it’s a method that all Views have. Here’s how we’ll use it:

.onTapGesture {
  // Code to perform when the user taps a list item goes here
}

Let’s start with something simple: We’ll print “The user tapped a list item!” to Xcode’s debug console whenever the user taps an item.

➤ Update body so that it looks like this:

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()
    }
  }
}

The change you made was adding a call to onTapGesture() immediately after the call to onMove():

.onTapGesture {
  print("The user tapped a list item!")
}

➤ Run the app and tap some list items. Look at the debug console in Xcode. You should see “The user tapped a list item!” for every time you tapped a list item:

The user tapped a list item
The user tapped a list item

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.

The checklistItem variable inside the ForEach view contains the current list item, so we should be able to use it to identify the tapped item.

➤ Change onTapGesture() so that its print function displays the name of the current item:

.onTapGesture {
  print("The user tapped \(checklistItem.name).")
}

Xcode will complain, showing you an error message that says “Use of unresolved identifier ‘checklistItem’”:

Xcode says that checklistItem is unresolved
Xcode says that checklistItem is unresolved

This is Xcode’s terribly technical way of saying, “I have no idea what you mean by checklistItem.” If your response is “But it’s right there!,” I feel your pain.

If you take a closer look at the code, you’ll see the reason behind Xcode’s confusion. A variable, constant, or method that is defined within a set of braces is “visible” — or as programmers say, in scope — only to code within the same set of braces. checklistItem’s visibility or scope is limited to the ForEach braces:

checklistItem’s scope
checklistItem’s scope

The onTapGesture() method call lives outside the braces where checklistItem is in scope. If we want to know which item the user tapped, we’ll need to use onTapGesture() somewhere inside those braces.

The HStack that makes up a list row is a View, which means that it has an onTapGesture() method. Better still, it’s inside the braces where checklistItem is in scope. Let’s move the call to onTapGesture() there!

➤ Update body so that it looks like this:

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()
    }
  }
}

Note the change: The call to onTapGesture() is now attached to the HStack view instead of the ForEach view. onTapGesture is called whenever the user taps one of the HStacks in the list. Don’t take my word for it, though — try it out for yourself!

➤ Run the app, tap some list items, and look at Xcode’s debug console, which shows you which item the user tapped.

Seeing which item the user tapped
Seeing which item the user tapped

Now that you know which item the user tapped, it’s time to check or uncheck it.

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.

The isChecked property determines a checklist item’s “checked” status. Setting it to true checks the item, and setting it to false unchecks it. With that in mind, we could code onTapGesture() like so:

.onTapGesture {
  if checklistItem.isChecked {
    checklistItem.isChecked = false
  } else {
    checklistItem.isChecked = true
  }
}

However, there’s a better, shorter way. Boolean (Bool) values have a method called toggle() which does the same thing, toggling the value from true to false and vice versa. This reduces all those lines of the if statement above to a single line. Lets use that instead.

➤ Change the call to onTapGesture() so that it uses toggle() to change the item’s isChecked property:

.onTapGesture {
  checklistItem.isChecked.toggle()
}

Once again, Xcode has an issue with what you just did. Lets have a look at what the issue is this time.

checklistItem is a let constant
checklistItem is a let constant

Xcode’s error message, “Cannot use mutating member on immutable value: ‘checklistItem’ is a ‘let’ constant,” is telling you that checklistItem is a constant, which means you can’t change any of its properties, including isChecked. You can only see what its properties contain.

You’ll often run into situations like this when programming. You’ll come up with a solution that you think should work, but when you code it, the compiler throws this kind of seemingly intractable obstacle in your way. This is the time to step back, look at the code, and see if it provides you with another solution.

First, think about what we can do with checklistItem. We can read its properties, which are:

  • id: The automatically generated universally unique identifier for the item.
  • name: The name of the item. We used this earlier to print the name of the tapped item in the Xcode console.
  • isChecked: The “checked” status of the item.

Then you should ask yourself: Is there another way to access a given checklist item? There might be, by way of the array of checklist items, checklistItems. The entire ContentView struct is its scope.

It’s declared as a var property, which means it’s a variable, and so are its contents. Accessing a given element of checklistItems is done using array notation: checklistItems[0] is the first element of the array, checklistItems[1] is the second, checklistItems[2] is the third, and so on.

Let’s test this idea by changing onTapGesture() so that when the user taps a list item, the first item in the list — checklistItems[0] — is toggled.

➤ Change the call to onTapGesture() to the following:

.onTapGesture {
  self.checklistItems[0].isChecked.toggle()
}

➤ Run the app and tap any list item. The first item in the list, “Walk the dog,” should toggle between checked and unchecked.

Now we’re getting somewhere. We now know that if you know the index (the element number, which starts at 0) of the item in checklistItems, you can change the item’s “checked” status. The problem is that there’s no such information inside the single checklistItem we get inside the ForEach view. All we have are checklistItems properties.

Look again at checklistItem’s properties. That first one, id, uniquely identifies it. Couldn’t we use it as a “search term” to scan through the items in the checklistItems array to find the matching item, and then toggle that item’s isChecked property?

Finding the matching item in checklistItems
Finding the matching item in checklistItems

Fortunately, the answer is “yes,” and there’s a way to do it in a couple of lines of code.

Swift arrays have many built-in methods for all sorts of purposes, including inspecting their contents, accessing one or more of their elements, adding and removing elements and finding specific elements. This final method is the most important for our immediate needs. One of these methods is firstIndex(of:), which gives you the index of the first element in the array that matches the given criteria. Remember, an array index is the number that specifies an element in an array.

Suppose we had an array named myList that was defined this way:

let myList = [
  "Alpha",
  "Bravo",
  "Charlie",
  "Delta",
]

If you wanted to find the index of “Charlie” in myList, you’d use this code:

let charlieIndex = myList.firstIndex(of: "Charlie")

“Charlie” is the third item in myList, which means its index is 2, so the value of charlieIndex is set to 2. As a reminder, array elements begin at 0.

What happens if you search for an item that isn’t in the array, say “Egbert?”

let egbertIndex = myList.firstIndex(of: "Egbert")

Since “Egbert” isn’t in myList, it doesn’t have an index. When this happens, firstIndex(of:) returns a value called nil, and that’s the value put into egbertIndex.

In case you were wondering how I knew about firstIndex(of:), I didn’t spring forth from the womb as a fully-formed Swift programmer. Instead, I looked at the Arrays section of Apple’s Swift documentation, located at developer.apple.com/documentation/swift/array. If you ever have a question about a Swift language feature, the developer docs are an excellent place to start.

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.

You’ll learn more about nil and optional types later on.

You’ll often find yourself writing code that follows this pattern: “If an operation produced a result with a value, do something with that result.” In many programming languages, you’d have to write this sort of code this way:

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

In case you were wondering, != means “is not equal to.”

In the code above, we’re calling someOperation(), which returns a result. If that result is not nil, the code inside the braces with the “Do something with result” comment is executed. If the result is nil, the code inside the braces is skipped entirely.

Swift has the if let construct, which makes this sort of code a little more concise. Here’s how you’d rewrite the code above using if let:

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

Just like the code before it, this code calls someOperation() and puts its result in the variable result. If result’s value is not nil, the code inside the braces with the “Do something with result” comment is executed. Otherwise, the code inside the braces is skipped.

We’re going to use if let to toggle the “checked” status of a checklist item in checklistItems if one whose id matches the id of the tapped item is found by a close cousin of the firstIndex(of:) method.

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.

The firstIndex(where:) method follows this format:

result = firstIndex(where: { Code for search criteria that produces a true or false result goes here } )

Where firstIndex(of:) is useful for finding the first exact match in an array, firstIndex(where:) lets you get really specific. In a really fancy checklist app, you could use it to search for the first checklist item in the list that is checked, entered on a Tuesday, marked as high priority and features a cat picture. In this checklist app, you’ll use it to find the first item in the list with a specific id value.

Using firstIndex(where:) is another one of those cases where showing it in action first is better than telling you how to use it. So, that’s just what I’ll do:

➤ Change the call to onTapGesture() to this:

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

➤ Run the app. Tap on any of the item names or checkboxes to check and uncheck them. You might notice that the blank space between the name and checkbox doesn’t respond to taps — we’ll fix that shortly.

You can confirm that the items’ isChecked properties are being updated by looking at Xcode’s debug console:

A working checklist, as seen in the Simulator and debug console
A working checklist, as seen in the Simulator and debug console

Now that it’s possible for the user to check and uncheck items, let’s look at the code that made it possible. Here’s the first line of the new code:

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

Let’s take a closer look at funny-looking part of that line, namely:

{ $0.id == checklistItem.id }

You provide firstIndex(where:) with the code in the braces — in case you’ve forgotten, it’s called a closure — and it goes through the array, applying that code to each element. The $0 is shorthand for “the first parameter passed to the closure,” which is the current array element.

The first time that firstIndex(where:) applies the code to the array, $0 represents the “Walk the dog” checklist item, and its id property compared to the id property of the tapped item. The second time, $0 represents the “Brush my teeth” checklist item, and the id property comparison is made. The third time, $0 represents the “Learn iOS development” item, and once again, id properties are compared. This cycle continues until the code in the closure results in a true value or the code has been applied to every element in the array.

If firstIndex(where:) finds a checklist item in checklistItems whose id property matches the id property of the tapped checklist item (checklistItem), it returns the index of matching item in the checklistItems array. This value is stored in the constant matchingIndex.

Now that we have the index of the matching item in checklistItems, we can toggle its isChecked property:

self.checklistItems[matchingIndex].isChecked.toggle()

We now have a checklist that the user can actually check! It’s always nice when an app lives up to its name. There’s just one little user experience issue that we should fix.

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.

The solution was the result of some experimenting and guessing. Rather than drag you through my experimentation and guesswork, let me simply give you the summary.

Do you know how you can make the whole row tappable, rather than just the visible parts? Give it a background color.

I decided to set the row’s background color to white, which I did by adding this method call to the HStack that defines each row:

.background(Color.white) // This makes the entire row clickable

With this change, the body property should look like this:

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()
    }
  }
}

Why does this work? It’s because, in its default state, list rows are transparent. The white color of a default list row is actually the white color of the view that contains the whole user interface.

The standard for most user interfaces — not just iOS’ — is that transparent objects aren’t tappable or clickable. Giving the row a color means that its pixels are clickable, and giving it the same color as the background view makes the whole under interface seamless.

As you gain more experience programming, you’ll find that your ability to come up with these flashes of insight will improve. Practice, to twist the expression slightly, makes programmer.

Key points

In this chapter, you did the following:

  • You created your first struct, and in the process, learned the difference between structs and objects or instances.
  • You updated the user interface to show each checklist item’s name and “checked” status.
  • You learned about the ternary operator.
  • You used the onTapGesture method that Views have to detect when the user tapped on a row.
  • You learned about methods for finding the first occurrence of an item in an array that met specific criteria.
  • You got a look into the sort of problem-solving that goes hand in hand with writing programs. As you do more programming, you’ll get better at it!

As always, you can find the project files for the app at this stage under 10 - Checkable List in the Source Code folder.

In the next chapter, we’ll handle the next big piece of missing functionality: Adding and editing checklist items. Checklist is beginning to look like a real app, isn’t it?

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.