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

13. Editing Checklist Items
Written by Joey deVilla

In the previous chapter, you added a key feature to Checklist: The ability to add items to the list. You’re no longer stuck with the five default items.

However, you still can’t fully edit an item. You can change its status from checked to unchecked, and vice versa, but you can’t change its name.

In this chapter, we’ll make checklist items fully editable, allowing the user to change both their names and checked status.

Changing how the user changes checklist items

Right now, when the user taps on a checklist item to toggle the item’s checked status. Tapping an unchecked item checks it, and tapping on a checked item unchecks it:

Tapping on a checklist item toggles its checked status
Tapping on a checklist item toggles its checked status

We’re going to give the user the ability to change either the name of a checklist item or its checked status. This will require making changes to how the app works.

Let’s look at the Reminders app that Apple includes on every iOS device as an example.

Here, tapping on an item’s name allows you to edit the name, while tapping on an item’s checkbox toggles its checked status:

Ideally, the user would tap on an item’s name to edit it, and tap on its checkbox to check or uncheck it
Ideally, the user would tap on an item’s name to edit it, and tap on its checkbox to check or uncheck it

Building this kind of user interface, as nice as it is, adds more complexity than an introductory tutorial should have. It would require changing the code in ChecklistView to support both showing the contents of the checklist and editing any given checklist item.

Instead, when the user taps a checklist item, we’ll take them to an edit screen that allows them to edit both its name and checked status:

Tapping on a checklist item will take the user to an edit screen
Tapping on a checklist item will take the user to an edit screen

The edit screen, which you’ll code in this chapter, will contain a Form view similar to the one you included in the Add new item screen. This Form will contain a view that allows the user to change the checklist item’s name and another view that allows the user to change its checked status.

With the changes that you’ll make, you’ll have a fully CRUD app by the end of this chapter. Checklist will be able to create, report, update and delete checklist items.

With that goal in mind, let’s get started!

Giving checklist rows their own view

First, we should look at the way that ChecklistView draws the list of checklist items onscreen. Here’s ChecklistView’s body property:

// User interface content and layout
var body: some View {
  NavigationView {
    List {
      ForEach(checklist.items) { checklistItem in
        HStack {
          Text(checklistItem.name)
          Spacer()
          Text(checklistItem.isChecked ? "✅" : "🔲")
        }
        .background(Color.white) // This makes the entire row clickable
        .onTapGesture {
          if let matchingIndex =
            self.checklist.items.firstIndex(where: { $0.id == checklistItem.id }) {
            self.checklist.items[matchingIndex].isChecked.toggle()
          }
          self.checklist.printChecklistContents()
        }
      }
      .onDelete(perform: checklist.deleteListItem)
      .onMove(perform: checklist.moveListItem)
    }
    .navigationBarItems(
      leading: Button(action: { self.newChecklistItemViewIsVisible = true
      }) {
        HStack {
          Image(systemName: "plus.circle.fill")
          Text("Add item")
        }
      },
      trailing: EditButton()
    )
    .navigationBarTitle("Checklist", displayMode: .inline)
    .onAppear() {
      self.checklist.printChecklistContents()
    }
  }
  .sheet(isPresented: $newChecklistItemViewIsVisible) {
    NewChecklistItemView(checklist: self.checklist)
  }
}

There’s a lot going on in this property. It:

  • Draws each checklist item, including its name and checked status.
  • Responds to presses on checklist items.
  • Responds to the user moving a checklist item.
  • Responds to the user deleting a checklist item.
  • Draws the navigation bar and its items, including the Add item button, the Edit button, and the title.
  • Responds to the user pressing the Add item button.

That’s already a lot of responsibilities in one place, and that means a lot of complexity.

Consider a term I used a couple of times in Section 1 of this book: Functional decomposition. It’s a fancy academic term that means “breaking down a big complex task into a set of smaller, simpler tasks.” We’re going to apply this principle to ChecklistView to simplify it. We’ll do this by splitting ChecklistView’s set of responsibilities into two groups:

  1. Responsibilities that involve individual checklist item rows, namely:
    • Drawing each checklist item, including its name and checked status.
    • Responding to presses on checklist items.
  2. Responsibilities that involve the checklist as a whole, namely:
    • Responding to the user moving a checklist item.
    • Responding to the user deleting a checklist item.
    • Drawing the navigation bar and its items, including the Add item button, the Edit button, and the title.
    • Responding to the user pressing the Add item button.

We’ll do this by defining a new view that will be responsible for drawing individual checklist rows. We’ll then call on this view from ChecklistView.

Defining the new row view

We’ll call this new view RowView, and we’ll put it in its own file, RowView.swift.

➤ Add a new file to the project by right-clicking or control-clicking on the Checklist folder in Xcode’s Project Explorer. Select New File… from the menu that appears:

Add a new file to the project
Add a new file to the project

➤ In the window that appears, make sure that you’ve selected iOS, then select SwiftUI View and click Next:

Select the 'SwiftUI View' template
Select the 'SwiftUI View' template

➤ Enter RowView into the Save As: field. Make sure that you’ve selected ChecklistItem in the Group menu and in the Targets menu, then click Create:

Name the file 'RowView'
Name the file 'RowView'

The project now has a new file named RowView.swift.

➤ Open RowView.swift and change the current definition of RowView to the following:

struct RowView: View {
  
  @State var checklistItem: ChecklistItem
  
  var body: some View {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
  }
}

As soon as you make this change, Xcode will report an error: Missing argument for parameter ‘checklistItem’ in call

An error appears in the preview code
An error appears in the preview code

Let’s fix the error first, then look at why it came up.

➤ In RowView.swift, change the preview section of the code to the following:

struct RowView_Previews: PreviewProvider {
  static var previews: some View {
    RowView(checklistItem: ChecklistItem(name: "Sample item"))
  }
}

With this change, the error message will disappear. Let’s find out why.

Initializing structs

If you look through the structs that make up the app, you’ll see that most of them have pre-defined properties. Let’s look at the first struct that you defined for this app.

➤ Open ChecklistView.swift and look at its properties: checklist, newChecklistItemViewIsVisible and body. You’ll see this. For brevity’s sake, only the first few lines of body are shown:

@ObservedObject var checklist = Checklist()
@State var newChecklistItemViewIsVisible = false

// User interface content and layout
var body: some View {
  NavigationView {
    List {
    ...

All three of ChecklistView’s properties have initial values assigned to them:

  • checklist is assigned the value Checklist(), which returns a new instance of Checklist.
  • newChecklistItemViewIsVisible is assigned the value false.
  • body is assigned a NavigationView that defines the user interface of the checklist screen.

Now, let’s look at the app’s newest struct: RowView.

➤ Open RowView.swift and look at its property: checklistItem:

@State var checklistItem: ChecklistItem

In this case, the property doesn’t have a value assigned to it. checklistItem is declared as a variable that holds instances of ChecklistItem, but it doesn’t have an initial value. It sits there, waiting for one.

With that observation, let’s now take a look at the change to the preview section of RowView.swift that made the error disappear. It was a change from this:

struct RowView_Previews: PreviewProvider {
  static var previews: some View {
    RowView()
  }
}

To this:

struct RowView_Previews: PreviewProvider {
  static var previews: some View {
    RowView(checklistItem: ChecklistItem(name: "Sample item"))
  }
}

More precisely, you changed this line of code, which simply says, “Create a new instance of RowView”:

RowView()

To this:

RowView(checklistItem: ChecklistItem(name: "Sample item"))

This line also creates a new instance of RowView. It also specifies a value to be assigned the new RowView instance’s checklistItem property: A new instance of ChecklistItem, with name property set to “Sample item.”

Since RowView doesn’t assign an initial value to its checklistItem property, you have to provide an initial value whenever you create a new instance. That’s why RowView() results in an error, but RowView(checklistItem: ChecklistItem(name: "Sample item")) doesn’t.

This isn’t the first time you’ve assigned values to struct instances while creating them. You also did it when you created the initial set of items for the checklist.

➤ Open Checklist.swift and look at its items property:

@Published var items = [
  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),
]

To create each of the checklist items in items, use ChecklistItem(name:isChecked:) to create a new instance of ChecklistItem and specify both its name and isChecked properties. For example, the line:

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

Says, “Create a new ChecklistItem whose name property is ‘Walk the dog’ and whose isChecked property is false.”

Since we’re instantiating ChecklistItem instances, let’s take a look at its properties.

➤ Open ChecklistItem.swift and look at its properties:

let id = UUID()
var name: String
var isChecked: Bool = false

Note the following:

  • The first property, id, is assigned an initial value using the let keyword instead of a var. This means that it is a constant, and its value can’t be changed. It’s what’s called a read-only property; its value can be read, but not rewritten. You can’t assign a value to this property.

  • The second property, name, isn’t given an initial value, which means you must provide one when creating an instance of this struct.

  • The third property, isChecked, is assigned an initial value of false using the var keyword. This means that the value of isChecked can be changed — either when creating the instance, or at a later time.

How ChecklistItem’s properties are defined means that you have a couple of options when creating ChecklistItem instances. You can provide a value for the name property:

ChecklistItem(name: "Sweep the floor")

This creates a new ChecklistItem instance whose name value is “Sweep the floor” and whose isChecked value is the default value, false.

You can also provide values for both the name and isChecked properties:

ChecklistItem(name: "Clean the bathroom", isChecked: true)

This creates a new ChecklistItem instance whose name value is “Clean the bathroom” and whose isChecked value is true.

Going back to RowView and its property, remember that this line defines it:

@State var checklistItem: ChecklistItem

The property allows another object to specify which checklist item the row should represent. By not giving the property an initial value, the checklist item has to be specified when the row generates. You’ll see this in action in the next part, where we finally make use of this new view.

Updating ChecklistView to use RowView

Our goal was to make each checklist row responsible to drawing itself. Now that we’ve defined the view that lets rows do just that, let’s update ChecklistView.

➤ Open ChecklistView.swift and in the body property of ChecklistView, change the lines that define each row in the list from this:

HStack {
  Text(checklistItem.name)
  Spacer()
  Text(checklistItem.isChecked ? "✅" : "🔲")
}

To this:

RowView(checklistItem: checklistItem)

➤ Run the app. It will appear to run as before, which means that we’ve successfully moved the responsibility of drawing individual rows from ChecklistItem to RowView.

Let’s see what happens if you tap on a row.

➤ Tap on any item in the list. You’ll see that it no longer checks or unchecks items.

Don’t worry; we’ll give individual rows the ability to respond to taps shortly.

In the meantime, since the code in ChecklistView that responds to taps on the list no longer works, let’s remove it.

➤ Update the body property in ChecklistView by removing the code for handling taps on the list. The result should look like this:

var body: some View {
  NavigationView {
    List {
      ForEach(checklist.items) { checklistItem in
        RowView(checklistItem: checklistItem)
      }
      .onDelete(perform: checklist.deleteListItem)
      .onMove(perform: checklist.moveListItem)
    }
    .navigationBarItems(
      leading: Button(action: { self.newChecklistItemViewIsVisible = true }) {
        Image(systemName: "plus")
      },
      trailing: EditButton()
    )
    .navigationBarTitle("Checklist")
    .onAppear() {
      self.checklist.printChecklistContents()
    }
  }
  .sheet(isPresented: $newChecklistItemViewIsVisible) {
    NewChecklistItemView(checklist: self.checklist)
  }
}

➤ Run the app to confirm that the changes you made didn’t create any errors.

Just as we made each row responsible for drawing itself by moving the row-drawing code to RowView, we’ll also make each row responsible for responding to user taps by moving the tap-response code to the same place.

Making rows respond to taps

Instead of checking or unchecking the corresponding item, tapping a row should take the user to a screen where they can edit both the item’s name and checked status:

Tapping on a checklist item will take the user to an edit screen
Tapping on a checklist item will take the user to an edit screen

You already have experience navigating between screens in a SwiftUI app. You used the NavigationLink view to provide the user a link that, when tapped, takes them to another screen. We’ll use the same kind of view to take the user to an “Edit item” screen when they tap a checklist item.

➤ Open RowView.swift and update the body property of RowView to the following:

var body: some View {
  NavigationLink(destination: EditChecklistItemView()) {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
  }
}

You just took the HStack that defined a checklist row and put it inside a NavigationLink. This makes the entire row respond to taps from the user, and it will respond by taking the user to the view specified in the destination: parameter: A new instance of the EditChecklistItemView view.

Reminder: You created EditChecklistItemView and its file, EditChecklistItemView.swift, back in Chapter 11. Right now, EditChecklistItemView defines a mostly empty screen that says, “Hello World.”

➤ Run the app. Tap on any item in the list. You’ll see the following:

The initial “Edit checklist item” screen
The initial “Edit checklist item” screen

Now that tapping on a checklist item takes you to EditChecklistItemView, it’s time to define that screen.

Defining EditChecklistItemView

Remember, when the user taps on a checklist item, we want them to see an “Edit” screen that looks like this:

The initial “Edit checklist item” screen
The initial “Edit checklist item” screen

This screen should have the following:

  • A TextField view containing the name of the selected checklist item. The user should be able to change the name of the checklist item by changing the text in this view. You used this control when creating the Add new item sheet in the previous chapter.
  • A control that displays the current checked status of the checklist item. The user should be able to change the checked status of the item by toggling this control. We’ll use a Toggle view to create this control.

Just as we did with NewChecklistItemView, we’ll put these into a Form view that will organize and display them in a way that is most suitable for gathering user input.

➤ Open EditChecklistItemView.swift and change all the code below the ‘Import SwiftUI’ with the following:

struct EditChecklistItemView: View {

  @State var checklistItem: ChecklistItem

  var body: some View {
    Form {
      TextField("Name", text: $checklistItem.name)
      Toggle("Completed", isOn: $checklistItem.isChecked)
    }
  }

}

struct EditChecklistItemView_Previews: PreviewProvider {
  static var previews: some View {
      EditChecklistItemView(checklistItem: ChecklistItem(name: "Sample item"))
  }
}

You might be tempted to run the app right now to see how the EditChecklistItemView screen looks, but you won’t be able to just yet. This new code has caused an error to pop up in RowView.

➤ Open RowView.swift. Look at RowView’s body property, and you’ll see a familiar error: Missing argument for parameter ‘checklistItem’ in call

The “Missing argument” error in RowView
The “Missing argument” error in RowView

Before you read on, ask yourself: How did you fix this error the last time you saw it?

The reason that the NavigationLink line now has an error is because of a key change you made in EditChecklistItem. You gave it a property that doesn’t have an initial value: checklistItem. It’s there so that the NavigationLink can do more than just bring up the “Edit item” screen. It can also tell the “Edit item” screen which item it’s editing.

Since EditChecklistItem’s checklistItem property doesn’t have an initial value, we need to provide that value when creating the EditChecklistItemView view. Let’s do that.

➤ In RowView.swift, update the body property of RowView to the following:

var body: some View {
  NavigationLink(destination: EditChecklistItemView(checklistItem: checklistItem)) {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
  }
}

With this change, the error should vanish. Let’s see the “Edit item” screen in action now!

➤ Run the app:

The checklist before attending to edit the 'Walk the dog' item
The checklist before attending to edit the 'Walk the dog' item

Let’s try editing the Walk the dog item.

➤ Tap the Walk the dog row. The “Edit item” screen will appear, containing the item’s current name and checked status.

➤ Change “Walk the dog” to “Walk the cat” and moved the Completed toggle from the “off” to the “on” position:

Editing a checklist item
Editing a checklist item

➤ Now that you’ve made those edits, tap the < Checklist button in the Navigation Bar to return to the checklist. Here’s what you’ll see:

The checklist after attending to edit the 'Walk the dog' item
The checklist after attending to edit the 'Walk the dog' item

Your changes vanished! The first checklist item’s name is still “Walk the dog” instead of “Walk the cat,” and it remains unchecked instead of checked.

What happened?

Retracing our steps so far

As you progress as a developer, you’re going to have more of these experiences where you’re coding away, and everything seems fine when suddenly, you run into an unexpected problem. Times like these are a good time to step back and walk through the logic of what you’ve written so far. Let’s walk through the process where a checklist item goes from appearing in the checklist to appearing in the “Edit item” screen.

➤ Open ChecklistView.swift and look at its body property. Here’s the part of body that draws all the items in the checklist:

ForEach(checklist.items) { checklistItem in
  RowView(checklistItem: checklistItem)
}

The ForEach view goes through checklist.items, the array containing all the items in the checklist. For each item in that array, it creates a new RowView instance and, in doing so, sets that RowView instance’s checklistItem property to the current checklist item.

Each checklist item is an instance of ChecklistItem, which is a struct. That means that when you set a RowView instance’s checklistItem property, you’re giving the RowView instance its own copy of the checklist item.

➤ Open RowView.swift and look at its body property. Here’s the line in body that determines what happens when the user taps the row:

NavigationLink(destination: EditChecklistItemView(checklistItem: checklistItem)) {

The NavigationLink, when tapped, takes the user to the view specified in its destination: parameter. In this case, the destination is a new EditChecklistItemView view. In creating the new EditChecklistItemView, we set its checklistItem property to the checklist item used by RowView.

Once again, the checklist item that we’re passing to EditChecklistItemView is a struct, which means that we’re giving the EditChecklistItemView instance its own copy of the checklist item, which in turn is a copy of the checklist item from ChecklistView.

Here’s what you should take from all this retracing: When you’re editing a checklist item in EditChecklistItemView, you’re editing a copy of a copy of an item in the checklist. That’s why your changes to the “Walk the dog” item don’t appear in the checklist after you dismiss the “Edit item” window.

What we need is a way to pass a connection to the actual checklist item from ChecklistView to RowView to EditChecklistItemView instead of a mere copy. That way, any changes made in EditChecklistItemView will be made in the checklist.

@Binding properties

Luckily for us, there is a way to pass a connection to a checklist item rather than a copy. Let’s make use of it by starting with EditChecklistItemView.

Updating EditChecklistItemView

➤ Open EditChecklistItemView.swift. Change the line that defines the checklistItem property from this:

@State var checklistItem: ChecklistItem

To this:

@Binding var checklistItem: ChecklistItem

You’ve just changed checklistItem from a @State property to a @Binding property. As a @State property, checklistItem was a property that belonged to EditChecklistItemView. When a RowView instance passes a checklist item to an EditChecklistItemView instance via the checklistItem item property, it makes a copy of RowView’s checklist item. Any changes made to the checklist item in EditChecklistItemView aren’t reflected in the matching checklist item in RowView, which is what we want.

As a @Binding property, checklistItem is a connection to another object’s property. Now, when a RowView instance passes a checklist item to an EditChecklistItemView via the checklistItem item property, any changes made to the checklist item in EditChecklistItemView will be reflected in the matching checklist item in RowView.

This change will cause an error in the preview code, whose code is trying to pass put a checklist item into a property that now expects a binding to a checklist item:

The error message that appears in the preview section
The error message that appears in the preview section

➤ Update the preview code in EditChecklistItemView to the following:

struct EditChecklistItemView_Previews: PreviewProvider {
  static var previews: some View {
    EditChecklistItemView(checklistItem: .constant(ChecklistItem(name: "Sample item")))
  }
}

Wrapping ChecklistItem(name: "Sample item") inside the .constant function creates a binding to a checklist item, which is the kind of value that the checklistItem property expects.

This completes all the changes we need to make to EditChecklistItemView. It’s time to edit the blueprint for objects that pass checklist items to EditChecklistItemView: RowView.

Updating RowView

➤ Open RowView.swift. Change the line that defines the checklistItem property from:

@State var checklistItem: ChecklistItem

To:

@Binding var checklistItem: ChecklistItem

This should give you a sense of déjà vu, and with good reason. You made the exact same changes in EditChecklistItemView! The connection to a checklist item that EditChecklistItemView receives from RowView is, in fact, a connection that RowView will receive from ChecklistItemView.

Since RowView will not be passing a checklist item to EditChecklistItemView, but a binding to a checklist item, we need to specify that.

➤ Change the NavigationLink line in RowView’s body property from:

NavigationLink(destination: EditChecklistItemView(checklistItem: checklistItem)) {

To:

NavigationLink(destination: EditChecklistItemView(checklistItem: $checklistItem)) {

The change is so subtle that you might have missed it. Instead of setting EditChecklistItemView’s checklistItem property to checklistItem, you’re now setting it to $checklistItem. The $ makes the difference: checklistItem is a checklist item, and $checklistItem is a binding to a checklist item.

Just as with EditChecklistItemView, changing RowView’s checklistItem property into a @Binding created an error in the preview code. Once again, it’s a matter of changing its code so that it passes a binding to a checklist item and not just a checklist item to RowView.

➤ Update the preview code in RowView to the following:

struct RowView_Previews: PreviewProvider {
  static var previews: some View {
    RowView(checklistItem: .constant(ChecklistItem(name: "Sample item")))
  }
}

We’re done making the necessary changes to RowView. But, there’s one more object blueprint to edit: ChecklistView.

Updating ChecklistView

Just as RowView passes a binding to its checklist item to EditChecklistItemView, we want ChecklistView to pass bindings to checklist items to RowView. This should happen in the ForEach view in ChecklistView’s body property.

➤ Open ChecklistView.swift and look at the ForEach view in the body property:

ForEach(checklist.items) { checklistItem in
  RowView(checklistItem: checklistItem)
}

Since the checklistItem property of RowView now holds bindings to checklist items instead of checklist items, the current code causes Xcode to display an error message:

The error message that appears in ChecklistView
The error message that appears in ChecklistView

This should easily be fixed by changing the value we put into RowView’s checklistItem property from a checklist item into a binding to a checklist item by prefacing it with a $ character.

➤ Change the ForEach view in the body property to the following:

ForEach(checklist.items) { checklistItem in
  RowView(checklistItem: $checklistItem)
}

That won’t work either:

The resulting error message in ChecklistView
The resulting error message in ChecklistView

The error message, Use of unresolved identifier ‘$checklistItem’, is Xcode’s way of saying: “I have no idea what you mean by “$checklistItem.” The problem is that you can only create a binding to a @State or @Binding variable, and the checklistItem inside ForEach’s braces is neither.

The perils of new platforms, again

IIn the previous chapter, we worked around a bug that caused strange behavior in the navigation bar buttons. You’ve just run into another rough edge that comes with working with a brand new platform like SwiftUI. There is a workaround, but it requires learning about another Swift feature.

Introducing extensions

Sometimes a struct or class gives you almost all the functionality you need. If it’s one that you wrote or have the source code for, you can add that missing functionality by writing more properties and methods. But what do you do when you didn’t write the struct or class, and you don’t have the source code?

That’s when you use extensions. They’re a way for you to say: “Here’s some extra code that I’d like to add to the struct or class.”

Making a simple extension

The best way to understand extensions is to see them in action, and the simplest way to do that is to start another Xcode playground session!

➤ In Xcode’s File menu, select New ➤ and then Playground…. The Choose a template for you new playground window will appear. Select macOS and Blank, then click Next.

Options for creating a new playground
Options for creating a new playground

➤ The Save as: window will appear. Enter a name for the playground. I used Extensions. In the Add to: menu, select Don’t add to any project or workspace. Once you’ve done that, click the Create button:

Choosing a place to save the playground
Choosing a place to save the playground

➤ Replace the code in the playground with the following:

print(true.asYesOrNo)
print(false.asYesOrNo)

Soon after you enter the code, you’ll see the following error messages:

The 'Bool' types doesn't have an 'asYesOrNo' property...yet
The 'Bool' types doesn't have an 'asYesOrNo' property...yet

That’s because true and false are both instances of the Bool type, which doesn’t have a property called asYesOrNo. Bool is a struct, which means that we can add a property to it using an extension.

The property we’ll add will be called asYesOrNo, and it will return the string “Yes” if the Bool’s value is true and the string “No” if the Bool’s value is false.

➤ Change the contents of the playground to the following:

extension Bool {

  var asYesOrNo: String {
    if self {
      return "Yes"
    } else {
      return "No"
    }
  }

}

print(true.asYesOrNo)
print(false.asYesOrNo)

Let’s test the extension.

➤ Move the cursor over the number for the last line of code in the playground and click the “Play” button that appears in the margin:

Testing the extension in the playground
Testing the extension in the playground

The debug console will show the output of both the print statements: “Yes” for true.asYesOrNo and “No” for false.asYesOrNo.

That’s the power of extensions — they let you add functionality to objects, even if you don’t have access to their source code.

Adding extensions to Checklist

Let’s get back to the issue that we currently have with Checklist.

We need a way for ChecklistView to go through each item in the checklist and give RowView a binding to each item. SwiftUI doesn’t have a built-in way to do this, but we’ve written some extensions that make up for this shortcoming.

➤ Open the Resources folder that comes with this book, and then open the Checklist subfolder. Inside that folder, you’ll find a folder named Extensions. Drag this folder onto the yellow Checklist folder in the Xcode project.

➤ When the Choose options for adding these files: window appears, make sure that the Copy items if needed checkbox is checked, the Create groups option is selected and that the Checklist item in the Add to targets menu is checked:

Choose options for adding these files
Choose options for adding these files

The project should look similar to this in Xcode’s Project Navigator:

The extensions folder in Xcode
The extensions folder in Xcode

Updating EditChecklistItemView

Now that the project has the necessary extensions, let’s make use of them!

➤ Open ChecklistView.swift. Change the ForEach view in the body property to:

ForEach(checklist.items) { index in
  RowView(checklistItem: self.$checklist.items[index])
}

With the help of the extensions, this code goes through checklist.items and passes a binding to each item to RowView.

➤ Run the app. It should display the default list of items:

The checklist before editing the 'Walk the dog' item
The checklist before editing the 'Walk the dog' item

➤ Select a checklist item to edit by tapping on one of them. In this example, I tapped on the first item, “Walk the dog” and edited it by changing its name to “Walk the cat” and changing its status to completed:

Editing a checklist item
Editing a checklist item

➤ Tap on the < Checklist button in the upper left-hand corner of the screen to return to the checklist. You’ll see that this time, your edits remain!

The checklist after editing the 'Walk the dog' item
The checklist after editing the 'Walk the dog' item

Congratulations — Checklist is now CRUD!

Key points

In this chapter, you:

  • Created a new view, allowing rows to draw themselves and respond to taps independently.

  • Learned more about initializing structs and their properties.

  • Updated Checklist’s user interface to support both checking items and editing their names.

  • Defined the “Edit item” screen.

  • Learned about how @Bindings can be used to shared properties among screens.

  • Learned about extensions and how to use them to extend the functionality of objects.

  • Used extensions to get around a rough edge in SwiftUI.

  • Brought the app to the point where it can list checklist items, create a new checklist item, edit an existing checklist item and delete checklist items. You have a full CRUD app now!

In the next chapter, we’ll add a much-needed capability to checklist: The ability to remember list items between sessions.

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.