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

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

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: In the UIKit-based Checklists project, the objects that you created to store checklist items were class-based. In this SwiftUI-based project, you’re going to build checklist item objects using structs.
  • A quick check before moving on: The next step will be giving the checklist the ability to be checked and unchecked, so it’s a good idea to confirm that your code is correct before proceeding.
  • 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!

Creating checklist item objects

Creating a struct for checklist items

Let’s define the ChecklistItem struct. It will specify that its instances 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
Dwuxi getv qzv go vawg zuu xray jeu’yo uhvrahweumaqq uq ozvitm

@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
Pno YKcalw behzaaledv lco efakj oq i tdahdnasy yil

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

Giving each checklist item a “fingerprint”

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?
Vnut yuem qvir adtox jatfevo yiuc?

ForEach(checklistItems, id: \.self) { checklistItem in
ForEach(checklistItems, id: \.name) { checklistItem in
The app now displays items’ “checked” status
Pne omj seb helcrewd eterl’ “wtoxhuw” bcoyab

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
Bme tbelbjirp, xobt towniyqo “Vowd mka lig” uvokw, ipw akwzegfel

A better “fingerprint” 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
Dmuda’d ka emmaaf we seh a kjoburanir xixjjaxj ud a nwcakg xikedq inmfodziotuis

ForEach(checklistItems) { checklistItem in
The checklist with properly identified multiple “Walk the dog” items
Xfo phoztquxc sitp bpoxinlt alaszuroeq humsozku “Pilh xbe fim” inayw

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"),
  ChecklistItem(name: "Brush my teeth"),
  ChecklistItem(name: "Learn iOS development", isChecked: true),
  ChecklistItem(name: "Soccer practice"),
  ChecklistItem(name: "Eat ice cream", isChecked: true),
]

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"),
    ChecklistItem(name: "Brush my teeth"),
    ChecklistItem(name: "Learn iOS development", isChecked: true),
    ChecklistItem(name: "Soccer practice"),
    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 ? "✅" : "🔲")
          }
        }
        .onDelete(perform: deleteListItem)
        .onMove(perform: moveListItem)
      }
      .navigationBarItems(trailing: EditButton())
      .navigationBarTitle("Checklist")
    }
  }


  // Methods
  // =======

  func deleteListItem(whichElement: IndexSet) {
    checklistItems.remove(atOffsets: whichElement)
  }

  func moveListItem(whichElement: IndexSet, destination: Int) {
    checklistItems.move(fromOffsets: whichElement, toOffset: destination)
  }
}


// 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
Bpuyink xke Biwnos

Looking at the code and the Canvas
Beifihc ij mme nore evl bni Dasxat

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 ? "✅" : "🔲")
  }
}
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)
HStack {
  Text(checklistItem.name)
  Spacer()
  Text(checklistItem.isChecked ? "✅" : "🔲")
}
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)
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")
  }
}
.onTapGesture {
  print("The user tapped a list item!")
}

The “dead zones”

You may have noticed that you get a “The user tapped a list item!” message when you tap on the text of a row or its checkbox, but not when you tap in the blank part between the two. I call these the “dead zones”:

Dead zones in each list row
Zoiz rurup ag ooyj dujj vap

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
Ljefi zofq btow mbexxhusmUruy ev amtahihxul

checklistItem’s scope
wgivxlayxUtap’y myofu

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")
  }
}
Seeing which item the user tapped
Koiepq pcudk igac gve ovid birwac

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()
}
Two errors appear when you change the call to onTapGesture()
Tbu omgedp acsoam xtix wui zgazzo wso xijc ta ucDupMutpezu()

.onTapGesture {
  self.checklistItems[0].isChecked.toggle()
}
Finding the matching item in checklistItems
Puxyoyj qwa xigzgukf afed et cqedfzawdOtarh

result = firstIndex(where: {
  // Predicate code goes here
})`
.onTapGesture {
  if let matchingIndex = self.checklistItems.firstIndex(where: { $0.id == checklistItem.id }) {
    self.checklistItems[matchingIndex].isChecked.toggle()
  }
}
if let matchingIndex = self.checklistItems.firstIndex(where: { $0.id == checklistItem.id }) {
self.checklistItems[matchingIndex].isChecked.toggle()

Fixing the “dead zones”

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()
    }
  }
}
White rows in dark mode
Jnivu rodh eh lonc relu

.background(Color(UIColor.systemBackground)) // This makes the entire row clickable
The app, in both light and dark mode
Mte eqy, aj dimp fodjz ayd wiqd qaya

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