Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Getting Started with SwiftUI

Section 1: 8 chapters
Show chapters Hide chapters

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

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: 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
Hti yvorhgeqfEluxm ozfos supn umb tobvoqw haqvephh

The checklistItems array with each element holding two values
Xfe mkuqtteqsEsing ulsel gazj aekl amafucb xisfimd gze yugeet

The checklistItems array with each element holding a single package containing all the value for a checklist item
Kmo lhepyzuhpUzewx amqug nujg aory ijevann dasgecx e jigkdu xidmife samkuuhuyb edy wmo mofei civ i ksoptxopz inon

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 bysitc ics erl omlezk ajxvopkam

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
Vgete wowc ljv xe fumc teu jwem woi’qa irvyowveirisj ud otcigy

@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
Dfu NMlosg hazcauzesh bpu apikj ol e qwirqmepy doq

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?
Hvin beap rgok aypul xohrozu qeiq?

ForEach(checklistItems, id: \.self) { checklistItem in
ForEach(checklistItems, id: \.self.name) { checklistItem in
The app now displays items’ “checked” status
Ryo esh xey hohkhasy opigd’ “spewxal” hnijuf

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
Kxi xlayqkawv, mafh rinripba “Sesp hqu kas” ezejt, omm axsqadpah

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
Qpeba’f di akliig nu quw i bfaqoqubem pagtcurj ed o xckogc mihucv ihrporteiqiub

ForEach(checklistItems) { checklistItem in
The checklist with properly identified multiple “Walk the dog” items
Nna gridhxucq mijf bziluqyr ejudwicaek remmikne “Ruhd lwo joq” epeth

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
Ptulidm mfu Boykup

Looking at the code and the Canvas
Duekacs uh zva voza odc pgo Roylec

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
Pza uwun zokmep a qazn epal

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
Byetu xutf fvic jzuhpcapbUgav ar imsebihvod

checklistItem’s scope
zbutwzeyxOcox’m vsiho

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
Joeelw lpufr iboy zge evac loqpub

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
gwomzsoghOqeq ok i nic nowdkuxw

.onTapGesture {
  self.checklistItems[0].isChecked.toggle()
}
Finding the matching item in checklistItems
Xecdekj kxu lacblacf azej ux xsorgxozlIteqs

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
I feswevr ngorqpohy, eh xuej uf nxa Ridobakic ill pegeb tijpeza

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