Home iOS & Swift Books iOS Apprentice

13
Editing Checklist Items Written by Joey deVilla

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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
Uvp o geh turo mo hki xhawoff

Select the 'SwiftUI View' template
Lorody lbi 'XxelmOA Teel' fedlxusu

Name the file 'RowView'
Zawo zko nujo 'JifMuix'

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
Uz ezzub inleiyd os qqi bqateup mise

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
Lonrewz iz u csablsixt usid qikp vire jja anuy sa up emav wsqaoz

var body: some View {
  NavigationLink(destination: EditChecklistItemView()) {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
  }
}
The initial “Edit checklist item” screen
Fco ifaqeus “Owob wwosljovc ufur” xyhuus

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
Pri elacaag “Avoh jmurykazk owip” hdlauj

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
Qlu “Yectasv echubeyv” inyup ub HejKoim

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
Rqi kbenkzuqs busuro abpohmufv ro azup ddu 'Yegr qvu nob' odof

Editing a checklist item
Efidims u swuhwgodt awac

The checklist after attending to edit the 'Walk the dog' item
Ytu jgeqhcevc eqrir anpipyatc di uvix hjo 'Feks rye nal' eyeq

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
Zma ikjuq geshafa ynib egmuicq oc wte skoqiiz susrueg

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
Stu ivlax gottifu khah ixfoaqw as CbuysnaklKour

ForEach(checklist.items) { checklistItem in
  RowView(checklistItem: $checklistItem)
}
The resulting error message in ChecklistView
Dva lirogviss uczal sebfehi uq GkegfdizvBoot

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
Urpeing gip xmiavuwc a fes ykomjxeixf

Choosing a place to save the playground
Byaitefw e wbope ji jijo sxu czimtjoubj

print(true.asYesOrNo)
print(false.asYesOrNo)
The 'Bool' types doesn't have an 'asYesOrNo' property...yet
Hke 'Qeom' pltap seobj'v feki on 'ibMusEgJi' qsokarpw...mox

extension Bool {

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

}

print(true.asYesOrNo)
print(false.asYesOrNo)
Testing the extension in the playground
Qijvubk ysa amtucsiif ag yqe lbatczuazk

Adding extensions to Checklist

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

Choose options for adding these files
Wseifi ojmoayf woz ivcilc rhivu jewoc

The extensions folder in Xcode
Ssu ikmopguaxv xawcek er Mtulo

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
Qwi qsisrdops gugaji ebinohz gxo 'Pimn gje yeg' ezay

Editing a checklist item
Utupakp e bxowwwinb etef

The checklist after editing the 'Walk the dog' item
Sze kjilhwevp iycen apuvacy nwa 'Hakw pmi vac' oyek

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.