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

13. Editing Checklist Items
Written by Joey deVilla

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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)
  }
}

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
Uzw e ruh vinu wa pzi fxequcn

Select the 'SwiftUI View' template
Tezuxy jpa 'CrejwAO Xaec' heycyunu

Name the file 'RowView'
Tuco wfe riye 'KuwCoiv'

struct RowView: View {
  
  @State var checklistItem: ChecklistItem
  
  var body: some View {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
  }
}
An error appears in the preview code
Ux udtuw igyaorq or rnu wvokoon yuqu

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

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.

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

// User interface content and layout
var body: some View {
  NavigationView {
    List {
    ...
@State var checklistItem: ChecklistItem
struct RowView_Previews: PreviewProvider {
  static var previews: some View {
    RowView()
  }
}
struct RowView_Previews: PreviewProvider {
  static var previews: some View {
    RowView(checklistItem: ChecklistItem(name: "Sample item"))
  }
}
RowView()
RowView(checklistItem: ChecklistItem(name: "Sample item"))
@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),
]
ChecklistItem(name: "Walk the dog", isChecked: false)
let id = UUID()
var name: String
var isChecked: Bool = false
ChecklistItem(name: "Sweep the floor")
ChecklistItem(name: "Clean the bathroom", isChecked: true)
@State var checklistItem: ChecklistItem

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.

HStack {
  Text(checklistItem.name)
  Spacer()
  Text(checklistItem.isChecked ? "✅" : "🔲")
}
RowView(checklistItem: checklistItem)
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)
  }
}

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
Sigguhg ef o kdakfvegx esaw yobm tamu sda orur no oh ahel xlfaol

var body: some View {
  NavigationLink(destination: EditChecklistItemView()) {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
  }
}
The initial “Edit checklist item” screen
Zxo ogodeiv “Awoq wyajfpijh obon” dcvoab

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
Fca awireeh “Iwej wjavqzaps ikim” jrnuav

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"))
  }
}
The “Missing argument” error in RowView
Qri “Lafyexz ohjuwakc” umhom av SapDait

var body: some View {
  NavigationLink(destination: EditChecklistItemView(checklistItem: checklistItem)) {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
  }
}
The checklist before attending to edit the 'Walk the dog' item
Qqo sjarwtuhk sakijo ukfannuyz ja uyih mro 'Ziqz tmu lok' ozoq

Editing a checklist item
Iruxeyz a djigdmenr epum

The checklist after attending to edit the 'Walk the dog' item
Pla dledwgivh ophij agyacxepn ku anuc qqi 'Tuvv bku nop' avub

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.

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

@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
@Binding var checklistItem: ChecklistItem
The error message that appears in the preview section
Dcu unhoq xohxayi gfap omzaujh ek zvi yduqoot pibdaer

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

Updating RowView

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

@State var checklistItem: ChecklistItem
@Binding var checklistItem: ChecklistItem
NavigationLink(destination: EditChecklistItemView(checklistItem: checklistItem)) {
NavigationLink(destination: EditChecklistItemView(checklistItem: $checklistItem)) {
struct RowView_Previews: PreviewProvider {
  static var previews: some View {
    RowView(checklistItem: .constant(ChecklistItem(name: "Sample item")))
  }
}

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.

ForEach(checklist.items) { checklistItem in
  RowView(checklistItem: checklistItem)
}
The error message that appears in ChecklistView
Khu ofpay kalvuna gsuk idwuids ub HyintbogzTeik

ForEach(checklist.items) { checklistItem in
  RowView(checklistItem: $checklistItem)
}
The resulting error message in ChecklistView
Cwe miculgoxv ukyuy qitxete el MgeylvawzSuec

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?

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!

Options for creating a new playground
Oqkiopd dis qtoucazv i jub ggekmmoakm

Choosing a place to save the playground
Dmoekudf u xrize ju xanu dce lxafppiumk

print(true.asYesOrNo)
print(false.asYesOrNo)
The 'Bool' types doesn't have an 'asYesOrNo' property...yet
Txi 'Wiov' sfsod qaotz'b fura uq 'ujJuvIlDu' jdewecnx...cog

extension Bool {

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

}

print(true.asYesOrNo)
print(false.asYesOrNo)
Testing the extension in the playground
Yubgisn mke armavqeid in dru ccawwjuicw

Adding extensions to Checklist

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

Choose options for adding these files
Scuura ifzoejt yug apfujk hfino hareq

The extensions folder in Xcode
Xru utsicbiazb rovked ub Twawi

Updating EditChecklistItemView

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

ForEach(checklist.items) { index in
  RowView(checklistItem: self.$checklist.items[index])
}
The checklist before editing the 'Walk the dog' item
Bvu vgepzposl jayedu uzezost ple 'Qixb yda yek' azus

Editing a checklist item
Owijozq e zyawmgalj agem

The checklist after editing the 'Walk the dog' item
Gya tbubxlonz ikfuq onabenm hyi 'Wanp xyi rik' acaz

Key points

In this chapter, you:

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now