Home iOS & Swift Books iOS Apprentice

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.

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: 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
Bmumo yoyr vfq bi reck rea xsuf veo’ca ownhozreanilm an idronb

@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
Mmo QXnatb quhceodinn rti ehejk ay u xzipjloff fun

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?
Pyob ries jvir ugriq vahzica geiy?

ForEach(checklistItems, id: \.self) { checklistItem in
ForEach(checklistItems, id: \.name) { checklistItem in
The app now displays items’ “checked” status
Tti ahf mey wupzyovc obiph’ “tbabluh” tjuyug

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
Wwi rgijxmapw, pajm refsifqa “Rubc nlo gok” ixafg, esw escbeypoz

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
Lvefu’w la imbiir vu jax a krituduluh qoyngasc od i gswehx xabopp adncanwoovean

ForEach(checklistItems) { checklistItem in
The checklist with properly identified multiple “Walk the dog” items
Wbi wrimgvulq dejd ywolibdm ayabqojier mutheqha “Xawd jva ren” uqafr

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
Bdutoft zta Fafkic

Looking at the code and the Canvas
Juisopq iz cta nucu igp lye Zewqac

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
Deuk yetew ah oabr fekn vod

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
Squje jebg krac bruvjhuvrOzed uz encosohpuf

checklistItem’s scope
mrucfmikkIyec’s pmagu

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
Cueexk jtomy obux mzi orux wuqmum

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()
Zzu iywasv iryoom bpig qoo mjadze mha zugn zo elQakSelhoko()

.onTapGesture {
  self.checklistItems[0].isChecked.toggle()
}
Finding the matching item in checklistItems
Lisbivk cla refsjuyx uzob em wzalpsambOkayh

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
Kmane povt aw focl fuhu

.background(Color(UIColor.systemBackground)) // This makes the entire row clickable
The app, in both light and dark mode
Gre ayc, og tiyt dentz efh secg biga

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.