49.
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: 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:
- The name of the checklist item, which we’ll call
name. Since this will contain text data, this will be aStringproperty. The user should be able to change the name of checklist items, so this should be a variable, which we specify with thevarkeyword. - The “checked” status of the checklist item. This status is either true or false, so it will be a
Boolproperty. 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. To keep things simple, we’ll define ChecklistItem inside ContentView.swift for now.
➤ 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
}
Note that
ChecklistItemstarts with an uppercase “C.” You may have noticed that throughout this book, we’ve been giving things that act like blueprints for objects — classes and structs — names that begin with uppercase letters. Meanwhile, we’ve been giving instances of those classes and structs names that start with lowercase letters. This is a convention that programmers use to make their code easier to understand and maintain.
Remember that unlike a class, which requires an initializer if you don’t provide default values for all its properties, a struct will automatically provide a memberwise initializer. This initializer lets you set the struct instance’s parameters at the time of instantiation in the order in which the properties appear in the struct.
In the case of our ChecklistItem struct, the initializer takes on the form ChecklistItem(name:isChecked:).
To create a ChecklistItem object for “Learn iOS development” whose status is checked, you would call the initializer as follows:
ChecklistItem(name: "Learn iOS development", isChecked: true)
For a ChecklistItem object for “Walk the dog” whose status 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")
As you type in the code to instantiate ChecklistItem, Xcode will try to help you by showing you both initializer options:
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),
]
Once you make this change, Xcode will quickly present a couple of error messages. We’ll fix those shortly.
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:
➤ 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.
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:
Hooray for semi-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. The value being put into id: is \.self, which is a KeyPath that references the self property of the objects being passed to ForEach, which until recently were strings.
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 ChecklistItem instance.
The error message also provides a hint: “‘ForEach’ requires that ‘ChecklistItem’ conform to ‘Hashable’.” The phrase “conform to” should tell you that the items that you pass to ForEach should be objects that adopt or conform to a protocol called Hashable.
When a piece of data is hashable, it means that it can be turned into a hash, which is a number that acts as a “fingerprint” that uniquely identifies the data. This requires the use of a hash function, which takes the data as its input and outputs the hash. Two pieces of data are different if they produce different hash numbers when fed into the same hash function. If two pieces of data result in the same hash number when fed into the same hash function, it is incredibly unlikely that they are different (for many hash functions, the odds of this happening are smaller than one in four billion).
Swift strings conform to the Hashable protocol, which allows them to be uniquely identified. Unfortunately, our ChecklistItem struct doesn’t conform to Hashable. One way around this could be using the name property value of each ChecklistItem as the argument for ForEach’s id: parameter. By doing this, ForEach would distinguish between each ChecklistItem instance by its name property.
➤ Change the ForEach line to the following:
ForEach(checklistItems, id: \.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:
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 \.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 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 “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.
➤ 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 {
ChecklistItem now conforms to the Identifiable protocol, which defines specific properties and methods 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, which is what we want. UUID() creates a new instance of UUID, which creates a new universally unique identifier value.
Since id is a constant property, it isn’t a parameter in ChecklistItem’s memberwise initializer, as this Xcode screenshot shows:
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). ForEach no longer needs a value for its id: parameter because each ChecklistItem provides its own unique ID as a result of adopting the Identifiable protocol.
➤ 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:
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
trueorfalse. 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"),
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()
}
}
That’s not much code, especially considering what the app already does. Getting the app to this point would’ve taken a lot more code in UIKit!
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:
Press the Resume button, and you should see your app:
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 ? "✅" : "🔲")
}
}
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)
For every item in the collection passed to ForEach — in this case, that’s checkListItems, it creates a view that makes up a list row:
HStack {
Text(checklistItem.name)
Spacer()
Text(checklistItem.isChecked ? "✅" : "🔲")
}
Each list row has a couple of methods that respond to events. We added them in the previous chapter, and they’re for handling the cases when the user chooses to delete and move list items:
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)
Now it’s time to make list rows respond to taps. We’ve already seen this method in the previous chapter — onTapGesture(). Let’s start with something simple: in response to the user tapping a list item, we’ll print “The user tapped a list item!” to Xcode’s debug console.
➤ Update body by adding a call to onTapGesture() after the calls to onDelete(perform:) and onMove(perform:). In the end, body should look 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")
}
}
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 “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”:
For now, when you tap on a row, tap on its text or its checkbox. We’ll fix this problem near the end of the chapter.
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’”…
…and if you take a closer look at the code, you’ll see the reason behind the error. checklistItem’s scope is limited to the ForEach braces:
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 also a view — that is, it adopts the View protocol — 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")
}
}
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.
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 constant. It’s provided to us by ForEach as a value to read, but not to write to. Hence Xcode’s two error messages, “Type of expression is ambiguous without more context,” and “Result of call to function returning _ is unused. We’ll need to take a different approach to changing a checklist item’s status.
Let’s 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 so that we can change its isChecked property? There might be: Through the array of checklist items, checklistItems. It’s in scope for all of ContentView, and it’s a var property, which means it’s a variable, which allows us to modify its variable properties.
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. If you know the index of the item in checklistItems, you can change the item’s “checked” status. The problem is that we’re not told the index of the current checklistItem inside the ForEach view. All we have is the checklistItem itself.
Since we lack any other information, let’s look again at checklistItem’s properties. That first one, id, uniquely identifies it. There should be a way to use this unique identifer to get the index of the corresponding checklist item, as shown in the diagram below:
This looks like a good place to make use of the array method called firstIndex(where:). Given a predicate — a fancy term for “code that returns a result of either true or false” — this method returns the index of the first item in the array that satisfies the predicate, or nil if there’s no such element in the array.
The firstIndex(where:) method follows this format:
result = firstIndex(where: {
// Predicate code goes here
})`
The firstIndex(where:) method lets you create really specific searches of an array. 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’re not going to be that fancy. You’ll use firstIndex(where:) to find the first item in the list with a specific id value.
This is yet another one of those cases where showing something in action first is better than telling you how to use it. 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()
}
}
➤ Run the app. Tap on any of the item names or checkboxes to check and uncheck them.
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 }) {
You provide firstIndex(where:) with a closure whose result is either true or false and it goes through the array, applying that closure code to each element. The $0 closure variable takes on the value of 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.
In the case where 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 and stores it in the constant matchingIndex. matchingIndex is then used to access 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 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.
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. This can be done with the View method named background().
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, list rows are transparent by default. The white our list rows is actually the color of ContentView.
The standard for most user interfaces — not just iOS’ — is that transparent objects aren’t tappable or clickable. Giving the row a color means makes its pixels responsive to touches or clicks, and giving it the same color as the background view makes the whole under interface seamless.
➤ Run the app. You should now be able to tap anywhere on a row to check or uncheck its item.
The solution to the “dead zone” problem works — but only as long as you never put the device in dark mode. To see what I mean, run the app with your Simulator or device in dark mode:
Remember, to turn on dark mode in the Simulator, open Settings, select Developer and then turn on Dark Appearance. On a device running iOS 13, open Settings, select Display and Brightness and under Appearance, select the Dark option.
Not only do the white rows clash with the dark background, they also obscure the text, whose default foreground color is white when in dark mode.
In order to make the blank area of a row respond to touches, we need to give the row a background color that somehow adjusts to light mode and dark mode.
I did more online reasearch and found that these color values exist in properties built into iOS called UI element colors. These are properties of the UIColor object that contain the proper foreground and background color values for standard UI control views — things such as labels, text, buttons, links, and so on — for the current screen mode.
In case you’re curious, the full set of UI element colors is listed in Apple’s online documentation here: https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors
One of the UI element color values is systemBackground, which is the appropriate default color for the user interface background for the current mode. We’ll change the background color of the row to that value.
➤ Change the line in body that sets the row background color to the following:
.background(Color(UIColor.systemBackground)) // This makes the entire row clickable
Note that in order to get this color value property, we had to access it through its object, UIColor and then convert it into a Color object that View’s background() method will accept.
➤ Run the app and switch between light mode and dark mode. It should look like this:
As you gain more experience programming, you’ll find that your ability to have flashes of insight that overcome programming challenges will grow. Practice, to twist the expression slightly, makes programmer.
Key points
In this chapter, you did the following:
- You created your first proper
struct— not just some silly cat exercise! - You updated the user interface to show each checklist item’s name and “checked” status.
- You learned about the ternary operator.
- You used the
onTapGesturemethod thatViews have to detect when the user tapped on a row. - You learned the
firstIndex(where:)method for finding the first occurrence of an item in an array that meets 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 49 - A 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?