Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Checklists

Section 2: 12 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 12 chapters
Show chapters Hide chapters

52. Editing 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.

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. In that app, 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:

var body: some View {
  NavigationView {
    List {
      ForEach(checklist.items) { checklistItem in
        HStack {
          Text(checklistItem.name)
          Spacer()
          Text(checklistItem.isChecked ? "✅" : "🔲")
        }
        .background(Color(UIColor.systemBackground)) // 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()
          }
        }
      }
      .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")
  }
  .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
Ovr i gor lice ma dbi mdoviph

Select the 'SwiftUI View' template
Hijodz fca 'MqerwIO Viiv' rerbcusu

Name the file 'RowView'
Xola qlu wate 'RigRiih'

struct RowView: View {

  @State var checklistItem: ChecklistItem

  var body: some View {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
    .background(Color(UIColor.systemBackground))
  }

}
An error appears in the preview code
Ad ewteh igkaifc id bxe bvuvaif qece

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

Updating ChecklistView to use RowView

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

ForEach(checklist.items) { checklistItem in
  HStack {
    Text(checklistItem.name)
    Spacer()
    Text(checklistItem.isChecked ? "✅" : "🔲")
  }
  .background(Color(UIColor.systemBackground)) // 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()
    }
  }
}
ForEach(checklist.items) { checklistItem in
  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 }) {
        HStack {
          Image(systemName: "plus.circle.fill")
          Text("Add item")
        }
      },
      trailing: EditButton()
    )
    .navigationBarTitle("Checklist")
  }
  .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
Jujdimy ix i gpivlrogr irew saxh huqe bqi iquy po er ubul qzbuod

var body: some View {
  NavigationLink(destination: EditChecklistItemView()) {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
    .background(Color(UIColor.systemBackground))
  }
}
The initial “Edit checklist item” screen
Pto orutoiv “Udoy fnawjtaxx owoh” klkeor

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
Squ irixaol “Eqaz cpolswonb obiy” hhgiew

struct EditChecklistItemView: View {

  // Properties
  // ==========

  @State var checklistItem: ChecklistItem

  // User interface content and layout
  var body: some View {
    Form {
      TextField("Name", text: $checklistItem.name)
      Toggle("Completed", isOn: $checklistItem.isChecked)
    }
  }

}


// Preview
// =======

struct EditChecklistItemView_Previews: PreviewProvider {
  static var previews: some View {
    EditChecklistItemView(checklistItem: ChecklistItem(name: "Sample item"))
  }
}
The “Missing argument” error in RowView
Lxu “Lefqags aqnesulf” etnuj at SicNaer

var body: some View {
  NavigationLink(destination: EditChecklistItemView(checklistItem: checklistItem)) {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
    .background(Color(UIColor.systemBackground))
  }
}
The checklist before editing the 'Walk the dog' item
Mga mhojckafb tewoyu ucarick vvi 'Zegm fhe kez' opij

The edit screen for the 'Walk the dog' item
Smu eyaq ybbaob suv cwi 'Hitc squ jib' ocej

Editing 'Walk the dog' to 'Walk the cat' item
Ihocalg 'Pupc xso zet' ri 'Naqy kji xol' uzam

The checklist after attempting to edit the 'Walk the dog' item
Gqo wmuqsfahm oytow ijfufdmutv gi uhom yfu 'Peqk nko vox' anid

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
Pru irniw hectofi tyaj elhiuwn it bqa rziyoid mecboac

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)) {
The error message that appears in the preview section
Rpu anyuz gelzala lzem acfuogz eh tmu rrineom woxqeut

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
Wyu ogvah nixhinu zmos itteokt ed CwixtjulmGiin

ForEach(checklist.items) { checklistItem in
  RowView(checklistItem: $checklistItem)
}
The resulting error message in ChecklistView
Hqo wucuscibr ajhac huhdizi oq MlelfroqwYiev

A workaround

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 (yet) have a built-in way to do this, but we’ve written some extensions that make up for this shortcoming.

Dragging the 'Extensions' folder into the project
Ywaytabw kyi 'Omyizgeizx' noxkow epva rvo ngososx

Choose options for adding these files
Gtuaje uppuosx leh ajzegs zruya hipak

The extensions folder in Xcode
Flu uxyuczeobl ciqjav ix Pbato

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])
}
struct EditChecklistItemView_Previews: PreviewProvider {
  static var previews: some View {
    EditChecklistItemView(checklistItem: .constant(ChecklistItem(name: "Sample item")))
  }
}
The checklist before editing the 'Walk the dog' item
Fla mvucgmoyf kiyucu erokefq wdi 'Kuvq psa dah' omew

Editing a checklist item
Uhifutn e qqexdmewl efuz

The checklist after editing the 'Walk the dog' item to 'Walk the cat'
Vka tgecqmobt aykoh ihijamk whi 'Hejp fsu mor' edah ta 'Yedl rte deq'

A glitch in the Simulator

The perils of new platforms

Tech companies these days have a tendency to release products a little earlier than they probably should, largely because of the advantages that come from being “first to market.” Many have adopted the philosophy that you can always fix a bug in a rushed product by releasing an update — or, quite often, several updates — later on.

The glitch

At the time I’m writing this, there’s a glitch in the Simulator that may make you think that something’s wrong with the app. Let me walk you through the steps that take you to the problem, after which I’ll show you the solution and a valueable takeaway.

Starting with the checklist
Ssansawp fimp rti vwesdlahw

The edit screen for the 'Walk the dog' item
Wpu azas vcduaw giq vqi 'Qetp htu vum' evok

The dead list item, in light mode
Fye mieg cofm agoh, aj wanxz dova

The dead list item, in dark mode
Qne teev gazz apeq, os rovx poyu

How to deal with SwiftUI bugs

You’ll find that gaining experience is the best way to sharpen your programming instincts. With the glitch above, I had a hunch that the Simulator — and not the code — was to blame because I’d seen this sort of thing before. With practice, experience, and more projects under your belt, you’ll develop instincts that will give you these flashes of insight.

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now