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

12. Adding Items to the List
Written by Joey deVilla

Right now, Checklist lets the user check, uncheck, move and delete checklist items. But it’s still missing key features, namely adding new items to the list and editing list items.

Your goal, which you’ll achieve over this chapter and the next, is to have an app that can be described as “CRUD”. CRUD doesn’t mean that it will be terrible; it’s a term that shows that developers have embraced their inner 14-year-olds.

CRUD is shorthand for the tasks that most record-keeping apps perform. It’s made up of the first letters of those tasks:

  • Create a new record. For Checklist, this means creating a new checklist item. You’ll add this capability to the app in this chapter.
  • Report all records. Your app already does this by presenting the list of all items.
  • Update an existing record. In Checklist, this is the ability to edit an existing checklist item. The app can’t do this yet, but it will by the end of the chapter.
  • Delete a record. The app already has this capability.

Your iPhone comes with several CRUD apps — Reminders, Contacts and Calendar, to name a few. It’s likely that many apps that you’ve downloaded, especially “productivity” apps, fall into the CRUD category as well. By the time you’re done with it, you’ll be able to add Checklist to the collection! In this chapter, you’ll enable the “C” in CRUD: creating a new checklist item.

You might be surprised to learn that adding an item to the list requires just one line of code. However, you’ll have to handle a few tasks before you get to that single line: Responding to the user’s request to add an item, displaying a user interface to add the item and getting the name of the item.

Setting up the user interface

To add an item to the list, the user should be able to indicate that they want to add an item. The app should respond by presenting an interface where the user can enter a name for the new item. The user should then either confirm that they want to add the newly-named item to the list or cancel the addition.

The property that starts the process

If you think back to those long-ago days when you were coding Bullseye, you might remember that an alert pop-up appears when the user presses the Hit me! button:

The Bullseye app displays its alert pop-up
The Bullseye app displays its alert pop-up

alert(isPresented:) made this possible. It defines an alert pop-up complete with a title, a message and a button to dismiss the alert. It also makes use of a Boolean property that determines whether the pop-up is visible.

For Checklist, you’ll use the same technique to present the user with a pop-up where they can enter the name of the item that they want to add, then either confirm the addition or cancel it.

Now, you’ll create that Boolean property for the checklist view, name it newChecklistItemViewIsVisible and add it to ChecklistView.

➤ Open ChecklistView.swift and add the following line to ChecklistView’s Properties section, just after the checklist property and before the body property:

@State var newChecklistItemViewIsVisible = false

This property, when true, will cause the Add item pop-up to appear. You don’t want the pop-up to appear until the user presses the Add item button, which is why its initial value is false. When the user wants to add an item to the list, they’ll perform an action that changes the property’s value to true. Once they’ve added an item, something should happen to cause the value to revert to false.

Now that we have the property, it’s time to give the user a way to change it.

Adding the “Add item” button

What should the user do to create a new item in Checklist?

It’s a good idea to look at other people’s apps for inspiration, especially if those apps do something similar to the one you’re writing. You’ll often get good user interface ideas, learn from other developers’ design mistakes and get insight into the kinds of features and functionality that users expect from an app.

Look at how users add new items to lists in iOS’ built-in checklist app. Here’s a list from the iOS 13 version of Reminders:

The Reminders app on iOS 13
The Reminders app on iOS 13

To add a new item to a list in Reminders, the user presses the New Reminder button located at the bottom of the screen. You’ll use a similar button in Checklist.

On any given list screen in Reminders, the navigation bar is already fully occupied with controls on either side: the Back button on the left and a button for options on the right. That’s why the New Reminder button is at the bottom of the screen.

Now, look at Checklist’s user interface:

Where the navigation bar buttons go
Where the navigation bar buttons go

Only the right-hand side of the navigation bar contains a control: the Edit button. The left side is available, and that’s where you’ll put the Add item button. It will follow the same format as the New Reminder button in Reminders: a “plus” sign in a circle and some text that explains the button. Our text will say: “Add item.”

Before you add a new button to the navigation bar, check to see how you added the one that’s already there. You put it there with this call to one of List’s methods — a modifier — attached to end of the List in the Checklist view’s body property:

.navigationBarItems(trailing: EditButton())

This line of code adds an EditButton, a built-in user interface element, to the trailing side of the navigation bar. When the device’s language is set to a left-to-right language, such as English, the right side is the trailing side. In a right-to-left language like Hebrew, the left side is the trailing side.

The opposite of the trailing side is the leading side, which is on the left for devices set to a left-to-right language. We’ll put the Add item button there.

➤ Open ChecklistView.swift and update the navigationBarItems modifier to this:

.navigationBarItems(
  leading: Button(action: { self.newChecklistItemViewIsVisible = true }) {
    HStack {
      Image(systemName: "plus.circle.fill")
      Text("Add item")
    }
  },
  trailing: EditButton()
)

The modifier now has two parameters: leading:, for the button on the leading side of the navigation bar, and trailing:, for the button on the trailing side. The button on the trailing side remains the same.

This code defines the Add item button on the leading side:

Button(action: { self.newChecklistItemViewIsVisible = true }) {
  HStack {
    Image(systemName: "plus.circle.fill")
    Text("Add item")
}

This code creates a button based on two parameters:

  • The action: parameter, which contains code that defines what should happen when a user presses the button. This code sets the value contained in the newChecklistItemViewIsVisible property to true.
  • The parameter within the { and } characters, which contains View objects that define what the button looks like. These objects could be Text, Image or any other object that’s a kind of View.

In this case, you’ll use an HStack to create a button appearance that’s a combination of an Image view followed by Text, just like the New Reminder button in Reminders.

You probably noticed that you used a slightly different method, Image(systemName:), to create the icon for the Add item button. This method makes an image based on SF Symbols, a pre-defined set of over 1,500 symbols that you can use in any app running on iOS 13 or later. Image(systemName:) lets you call up any of the symbols’ images by name. The symbol named “plus.circle.fill” is a + sign drawn in a filled circle.

Note: You can browse the complete set of SF Symbols in the SF Symbols desktop app, which is available at Apple’s Developer site.

It’s time to see the button in action!

➤ Run the app. You should now see the Add item button on the left, or leading, side of the navigation bar. You can try pressing it, but nothing will happen… yet!

The app, now featuring the 'Add item' button
The app, now featuring the 'Add item' button

Displaying a pop-up

In Bullseye, when the user presses the Hit me! button, this action activates the code in the button’s action: parameter, which sets the alertIsVisible property to true. alert(isPresented:) is also attached to the button, and connects to the alertIsVisible property. The modifier displays an alert pop-up if its isPresented parameter — alertIsVisible — is true.

Here’s the code for the modifier:

.alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text(alertTitle()),
        message: Text(scoringMessage()),
        dismissButton: .default(Text("Awesome!")) {
          self.startNewRound()
        }
  )
}

You’re going to do something similar with the Add item button. You’ve already done some of the work: You’ve added a Boolean property that will control the appearance of a pop-up screen, and you’ve added a button that controls the value in the Boolean property. The next step is to create a pop-up window that the Boolean property controls.

An Alert pop-up would be a little too small:

The app, displaying an 'Alert' pop-up
The app, displaying an 'Alert' pop-up

Whatever kind of pop-up you use should provide lots of space — not just enough space for the user to enter a name for the new checklist item, but enough space for additional information that you might decide to include with an item in later editions of the app.

That kind of pop-up is called a sheet. It’s much larger than an alert; in fact, it takes up nearly the entire screen. Here’s an example of a sheet in action:

The app, displaying an 'Sheet' pop-up
The app, displaying an 'Sheet' pop-up

Next, you’ll write the code to display a basic sheet with the message, “New item screen coming soon!” when the newChecklistItemViewIsVisible property is true, which happens when the user presses the Add item button.

➤ Add the following code after NavigationView’s closing brace:

.sheet(isPresented: $newChecklistItemViewIsVisible) {
  Text("New item screen coming soon!")
}

The sheet(isPresented:) method takes two arguments:

  • isPresented: displays the sheet if its value is true.
  • The second argument, which you’ll find within the brackets, is a view defining the content of the sheet. The code above sets that content to a Text view displaying the text “New item screen coming soon!”.

➤ Run the app and press the Add item button. You’ll see this:

The app, displaying a sheet that says 'New item screen coming soon!'
The app, displaying a sheet that says 'New item screen coming soon!'

➤ Swipe down on the sheet. This will dismiss it, returning you to the checklist view.

Defining the sheet

Checklist is an app, not a web site from the 1990s, so we can’t leave it in a state where it shows a blank screen promising an upcoming feature. Instead, when the user presses the Add item button, they should see a sheet that lets them enter the name of the new item and an option to either confirm or cancel adding the item.

You’ll set up the sheet so that it displays the following:

  • The title “Add new item”.
  • A text field where the user can enter the name of the new item.
  • A button that the user can press to confirm that they want to add the new item to the list.
  • A text prompt that tells the user to swipe down to cancel adding an item to the list.

You’ll define this screen in its own view, NewChecklistItemView, which will live in its own file, NewChecklistItemView.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. Click Next:

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

➤ Enter NewChecklistItemView 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 'NewChecklistItemView'
Name the file 'NewChecklistItemView'

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

➤ Open NewChecklistItemView.swift. Its body defines the default “Hello World!” view:

var body: some View {
  Text("Hello World!")
}

➤ Update NewChecklistItemView’s body to the following:

var body: some View {
  VStack {
    Text("Add new item")
    Text("Enter item name")
    Button(action: {
    }) {
      HStack {
        Image(systemName: "plus.circle.fill")
        Text("Add new item")
      }
    }
    Text("Swipe down to cancel.")
  }
}

To see your new screen, you need to change the line of code that defines the content of the sheet that appears when the user presses the Add item button on the checklist screen.

➤ Open ChecklistView.swift and change the code that opens the sheet to the following:

.sheet(isPresented: $newChecklistItemViewIsVisible) {
  NewChecklistItemView()
}

➤ Run the app and press the Add item button. You’ll see this:

The 'Add new item' sheet
The 'Add new item' sheet

That’s not quite the look you’re going for. Next, you’ll try to fix it.

Fixing the sheet’s layout

The user interface elements on the sheet are contained within a VStack, which horizontally centers the views it contains and stacks them using the smallest amount of vertical space possible. It then vertically centers itself. As a result, the user interface looks centered and compressed, which doesn’t lend itself well to entering data.

You could use Spacer views to fix this user interface, just as you did with Bullseye, but it’s worth looking at a couple of other approaches.

The List view organizes the views it contains into a vertical stack, just as a VStack does. Let’s put the Enter item name text and Add new item button into a List and see what happens.

➤ Update the body property to the following:

var body: some View {
  VStack {
    Text("Add new item")
    List {
      Text("Enter item name")
      Button(action: {
      }) {
        HStack {
          Image(systemName: "plus.circle.fill")
          Text("Add new item")
        }
      }
    }
    Text("Swipe down to cancel.")
  }
}

➤ Run the app and press Add item. You’ll see this:

The 'Add new item' sheet, using a List
The 'Add new item' sheet, using a List

A List aligns the views it contains to the leading side, which is the left side for left-to-right languages such as English. Unlike the VStack, which takes only the vertical space it needs, the List expands to take as much vertical space as possible, filling it with empty cells.

A List is a better container for the Enter item name text and Add new item button than a VStack, but it’s still not quite right. The empty cells at the bottom of the list suggest that the screen might present more information, but that’s not the case.

It’s time to introduce a new SwiftUI view: the Form. The official documentation describes it as, “A container for grouping controls used for data entry, such as in settings or inspectors.” Rather than comment on this description, change your List view into a Form and see what happens.

➤ Update body to the following:

var body: some View {
  VStack {
    Text("Add new item")
    Form {
      Text("Enter item name")
      Button(action: {
      }) {
        HStack {
          Image(systemName: "plus.circle.fill")
          Text("Add new item")
        }
      }
    }
    Text("Swipe down to cancel.")
  }
}

➤ Run the app and press Add item. You’ll see this:

The 'Add new item' sheet, using a Form
The 'Add new item' sheet, using a Form

Form was designed with data entry in mind. The way it separates itself from the views above and below it is a visual hint to the user that says, “You’re going to enter information here.” It aligns the views appropriately based on the user’s language settings, making it easier for them to enter information. It also provides clear divisions between the views it contains, clearly showing the user each piece of information they must provide, and subtly giving them an idea of how much information they’re expected to enter.

Some views, when put into a Form, adapt themselves for data entry. For example, the Button view expands its tappable area to take up the full width of the form:

Buttons expand their tappable areas when inside a Form
Buttons expand their tappable areas when inside a Form

For these reasons, you’ll use Form as the view to contain the Enter item name text and the Add new item button.

Collecting user input

Now that you’ve settled on the layout of the Add new item sheet, you can make it functional.

➤ Run the app and press Add item. Try tapping on Enter item name to enter the name of an item to add to the list.

Nothing happens, because you tapped on a Text view. It’s a view that only outputs data. In order to collect data from the user, you’ll need to use a different view.

You’ve already used a view that allows the user to input data: the Slider view in Bullseye. You also created a property to store the slider’s current position and set up a two-way binding between the slider and the property. With the binding in effect, the property updated whenever the user moved the slider, and any changes made to the property updated the slider’s position.

You’ll do something similar to get the name of the item to add to the list. You’ll set a TextField view where the user can enter the name and a property to store the name. You’ll also bind the two together so that changes to the property will change the content of the text field and changes to the text field will change the content of the property.

Now, go ahead and create the property to store the name.

➤ Add the following to NewChecklistItemView, before body:

@State var newItemName = ""

This creates a new property, newItemName, which is initially set to an empty string — that’s what the two double-quote characters (") with nothing between them means.

Now, give the user a TextField where they can enter the new item name and connect it to newItemName.

➤ Find the following line in the body property:

Text("Enter item name")

And change it to this:

TextField("Enter new item name here", text: $newItemName)

This line initializes a new TextField. It takes two arguments:

  • Some hint text: Light-colored text that tells the user what the text field is for or what information to enter into it. This tutorial uses the text, “Enter new item name here” for the hint text, but feel free to customize it however you like.

  • A binding: A two-way connection to a property. This tutorial sets this value to $newItemName, which connects the text field to newItemName. Remember: newItemName refers to the value stored in the property, and $newItemName is a two-way connection to that value.

As the user changes text inside the text field, the value stored in newItemName will change to match. Conversely, if code changes the values stored in newItemName, the contents inside the text field will change to match.

With the changes you just made, the code for NewChecklistItemView should now look like this:

struct NewChecklistItemView: View {

  @State var newItemName = ""

  var body: some View {
    VStack {
      Text("Add new item")
      Form {
        TextField("Enter new item name here", text: $newItemName)
        Button(action: {
        }) {
          HStack {
            Image(systemName: "plus.circle.fill")
            Text("Add new item")
          }
        }
      }
      Text("Swipe down to cancel.")
    }
  }

}

Now it’s time to see these changes in action!

➤ Run the app and press Add item. You should see this:

The 'Add new item' screen with a text field
The 'Add new item' screen with a text field

As promised, the TextField displays the hint text. Now, you can try entering some text.

➤ Tap the text field and try typing in it. You can now type a name for a new checklist item:

Entering text into the text field
Entering text into the text field

If you also pressed the Add new item button, you saw that nothing happened. You’ll take care of that next.

Adding a new item to the list

When the user presses the Add new item button, the following should happen:

  1. The app should create a new checklist item. Its name should be whatever the user typed into the text field, and isChecked should be the default value, false.
  2. The newly-created checklist item should appear in the list.
  3. The Add new item sheet should disappear, returning the user to the checklist view.

Before you write the code that performs these tasks, check out where it will go.

Take a look at the code in body that defines the Add new item button:

Button(action: {
}) {
  HStack {
    Image(systemName: "plus.circle.fill")
    Text("Add new item")
  }
}

There’s code that defines what the button looks like:

HStack {
  Image(systemName: "plus.circle.fill")
  Text("Add new item")
}

And there’s a place where you’ll add code to define what the button does when pressed — the Button’s action: parameter:

Button(action: {
})

Start by adding the code to perform the first task: creating the new checklist item.

Creating a new checklist item

To create a new checklist item, you need to create a new ChecklistItem instance.

You may not realize it, but you’ve been creating new instances for some time now. You’ve been doing it by using the initializer of the struct or class that you wanted to instantiate, which is a special method that creates a new instance of a struct or class and can also set values for that instance’s properties.

When you define a new struct or class, Swift automatically defines at least one initializer for it. You can also define your own additional initializers to set up instances in very specific ways.

An initializer takes its name from struct or class that it initializes. Whenever you see a capitalized name followed by parentheses (the ( and ) characters), you’re probably looking at an initializer. If you look at the body of any view you’ve worked on so far, you’ll see that it’s full of calls to initializers: things like Text and Button.

Now, you’ll use ChecklistItem’s initializer to create a new checklist item.

➤ Add a line to the Button’s action: parameter so that the part of body that defines the button looks like this:

Button(action: {
  var newChecklistItem = ChecklistItem(name: self.newItemName)
}) {
  HStack {
    Image(systemName: "plus.circle.fill")
    Text("Add new item")
  }
}

As you typed in the new line to create a new ChecklistItem instance, Xcode suggested a couple of initializers:

Xcode suggests initializers for ChecklistItem
Xcode suggests initializers for ChecklistItem

That’s because ChecklistItem has not one, but two initializers:

  • ChecklistItem(name:): Creates a new ChecklistItem instance, given only a name for the new item.
  • ChecklistItem(name:isChecked:): Also creates a new ChecklistItem instance, but requires that you specify both a name for the new item and whether it’s checked or not.

ChecklistItem has two initializers because its isChecked property has a default value of false. Swift detected this default value and automatically created two initializers: One where you don’t have to provide a value for isChecked, and one where you do. Since you’re treating all new checklist items as unchecked, you’ll use the initializer that doesn’t require a value for isChecked.

The new line declares a new variable named newChecklistItem and assigns a new ChecklistItem instance to it. The item’s name is set to the contents of NewChecklistItemView’s newItemName. newItemName is bound to the text field, so its contents are the text field’s contents. With this single line, you’ve taken care of the first step of adding a new item to the list.

Now that you have a new checklist item, it’s time to add it to the list.

Getting access to the checklist

You can’t add the item to the list without accessing the list. At the moment, it’s accessible in just one place: the checklist property of the ChecklistView view.

To work with the list from within NewChecklistItemView, you’ll need to do a couple of things:

  • Set up a property within NewChecklistItemView that will hold the list.
  • Have ChecklistView give the list to NewChecklistItemView, which will store the list inside the property mentioned above.

Now, go ahead and set up the property.

➤ Add the following line of code just before the line where you declare newItemName.

var checklist: Checklist

This new line of code is pretty straightforward. It declares a property named checklist that can hold Checklist instances.

Moments after you add this new line of code, Xcode will show an error message in the code that generates the preview:

A new error message appears
A new error message appears

Adding the checklist property to NewChecklistItemView changed its initializer. It now has a parameter, checklist:, which you’ll use to pass the checklist from ChecklistView to NewChecklistItemView. The preview is making a call to the old initializer, NewChecklistItemView(), which no longer exists.

To fix the problem, you’ll change the call so that it passes the value of checklist to the preview.

➤ Change the preview code to the following:

struct NewChecklistItemView_Previews: PreviewProvider {
  static var previews: some View {
    NewChecklistItemView(checklist: Checklist())
  }
}

The code on the previous page tells the preview that its content is an instance of NewChecklistItemView and that it should use a new instance of Checklist for its checklist. Xcode then uses this information to generate the preview. [I changed this sentence from passive to active voice. Is Xcode the correct actor.]

Now that you’ve fixed the error in the preview, you can return to the task of passing the checklist from ChecklistView to NewChecklistItemView.

➤ Open ChecklistView.swift and change the code in ChecklistView’s body that displays the sheet to the following:

.sheet(isPresented: $newChecklistItemViewIsVisible) {
  NewChecklistItemView(checklist: self.checklist)
}

The complete body should now look like this:

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")
    .onAppear() {
      self.checklist.printChecklistContents()
    }
  }
  .sheet(isPresented: $newChecklistItemViewIsVisible) {
    NewChecklistItemView(checklist: self.checklist)
  }
}

➤ Run the app and press the Add item button. It appears to work as before, but under the hood, NewChecklistItemView has information that it didn’t have before: It now has access to the checklist data.

A quick reminder: Checklist is a class

It’s time for a quick reminder about Checklist, which uses a slightly different kind of object blueprint.

➤ Open Checklist.swift. Near the top of the file you’ll see this line:

class Checklist: ObservableObject {

Unlike most of the other object blueprints in the project, Checklist is a class and not a struct. I covered this in the previous chapter, but rather than make you look back, I’ll provide a quick refresher.

A struct is a value type, where each instance keeps its own copy. Think of value types as being like a spreadsheet file that you email to your co-workers:

A value type is like a spreadsheet file
A value type is like a spreadsheet file

With this arrangement, each co-worker has their own copy of the spreadsheet. Any change that a co-worker makes to their spreadsheet will appear only in that co-worker’s spreadsheet and nowhere else.

On the other hand, a class is a reference type, where all instances refer to the same copy. Think of reference types as being like a Google Docs spreadsheet that you share with your co-workers:

A reference type is like a Google doc
A reference type is like a Google doc

In this scenario, every co-worker is working on the same copy of the doc. Any change that a co-worker makes to the doc will be seen by every other co-worker, since they’re all viewing the same thing.

By declaring Checklist as a class, you made it possible for different views to access the same checklist. When ChecklistView passes the checklist to NewChecklistItemView, it’s giving NewChecklistItemView access to the same checklist that it uses. That’s what makes it possible for NewChecklistItemView to add an item to the checklist.

Adding the new item to the list

Now that you’ve gone through all that setup, plus a quick review of value and reference types, it’s time to add the newly-created checklist item to the list!

➤ In NewChecklistItemView.swift, add a couple of lines to the Button’s action: parameter so that the part of body that defines the button looks like this:

Button(action: {
  var newChecklistItem = ChecklistItem(name: self.newItemName)
  self.checklist.items.append(newChecklistItem)
  self.checklist.printChecklistContents()
}) {
  HStack {
    Image(systemName: "plus.circle.fill")
    Text("Add new item")
  }
}

As I mentioned near the start of the chapter, it takes a single line of code to add a new item to the checklist. It’s this line:

self.checklist.items.append(newChecklistItem)

Remember that items is a property of the checklist and that it’s an array that holds all its items. append() is an array method that takes a given object, adds an element to the array and puts the object in the new element. append(newChecklistItem) adds newChecklistItem to the checklist.

To confirm that the app is really adding newly-created objects to the checklist, you included this line of code:

self.checklist.printChecklistContents()

This uses the printChecklistContents() method in Checklist to display what’s in the checklist.

Now, try adding items to the list!

➤ Run the app. Press the Add item button, which will display the Add new item sheet.

➤ Type something into the text field and press the Add new item button:

Entering the first new checklist item
Entering the first new checklist item

Look at Xcode’s debug console; you should see that the item has been added to the list:

The new checklist item in the Xcode console
The new checklist item in the Xcode console

➤ Swipe down on the Add new item sheet. It will disappear, revealing the checklist screen, which will contain the newly-added item:

The checklist with the newly-added checklist item
The checklist with the newly-added checklist item

It works! The user can now add items to the checklist, bringing you one step closer to having a fully-CRUD app. (Don’t forget, in this case, CRUD is a good thing. :] )

Dismissing the Add new item sheet automatically

The user should only swipe down on the Add new item sheet to cancel adding an item. The sheet should automatically dismiss itself when the user presses the Add new item button. With just two lines of code, you can make this happen.

The first line of code will create a property that allows you to control the way the current view presents itself.

➤ Add the following just below the line where you declared the newItemName property:

@Environment(\.presentationMode) var presentationMode

While you may not know exactly what this line of code does, you’ve probably seen enough Swift syntax to know that it creates a variable property named presentationMode, which is a kind of @Environment(\.presentationMode), whatever that is.

In Swift, anything that begins with the @ character is a hint to the operating system that it’s something special that it needs to pay particular attention to.

You’ve already seen something that begins with the @ character: @State. In Checklist, you used it in both ChecklistView and NewChecklistItemView to mark certain properties as state properties. In ChecklistView, you marked the checklist property as a @State property so that changes to checklist would be immediately and automatically reflected in the List view. In NewChecklistItemView, you marked the newItemName property so that its contents would be reflected in the “Add new item” text field and vice versa.

@Environment marks a property as one that can access a specific system setting related to the operating system environment, hence the name. It lets you find useful settings, such as whether the user’s language is left-to-right or right-to-left, which calendar system is appropriate for the user’s locale settings, the current color scheme and other system-wide settings.

The content of the parentheses (the ( and ) characters) that follow @Environment specifies the kind of setting to access. In this case, you’re accessing a setting called presentationMode. It’s an object that has a method that dismisses the current view.

Now that you have the presentationMode property, you can call on one of the methods nestled deep within it to dismiss the sheet when the user presses the Add new item button.

➤ In NewChecklistItemView.swift, add a new line of code to the Button’s action: parameter so that the part of the body property that defines the button looks like this:

Button(action: {
  var newChecklistItem = ChecklistItem(name: self.newItemName)
  self.checklist.items.append(newChecklistItem)
  self.checklist.printChecklistContents()
  self.presentationMode.wrappedValue.dismiss()
}) {
  HStack {
    Image(systemName: "plus.circle.fill")
    Text("Add new item")
  }
}

➤ Run the app. Press the Add item button, which will display the Add new item sheet. Enter a name for a new item and press the Add new item button.

The sheet will dismiss itself and you’ll return to the checklist, which is the way the app should work.

Dealing with a SwiftUI bug

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.

This “release early, release often” approach is doubly true for developer tools and platforms. Unlike consumer products, which usually have a small set of use cases, developers use their tools in many different ways, and there’s no way to predict how they’ll use any given feature. The vendors who make developer tools often find it more practical to treat their users as “gamma testers” — in the Greek alphabet, gamma is the next letter after beta — and rely on their feedback to find out where the bugs are.

SwiftUI is a brand-new platform, and as one of the earliest developers to use it, you should expect to encounter some bugs. If you’ve been playing with the app, you may have already encountered the one that you’re about to deal with next.

Fixing the navigation bar button bug

➤ Run the app. Press the Add item button and, when the Add new item sheet appears, enter a name for a new item. Press the Add new item button, which will return you to the checklist. So far, so good:

The checklist with a newly-added checklist item
The checklist with a newly-added checklist item

The bug becomes apparent when you try to add another item.

➤ On the checklist screen, press the Add item button again. This time, it doesn’t respond to your touch, nor does it take you to the Add new item sheet. If you press the Edit button, you’ll find that it also doesn’t work.

I struggled with this bug for a few minutes and discovered that checking or unchecking any item in the list un-sticks the buttons in the navigation bar:

➤ Tap on any item in the checklist to check or un-check it. Then try pressing the Add item or Edit button. You’ll see that they work now.

I looked at the code that defined the buttons in the navigation bar and the code that dismissed the Add new item sheet after the user added a new item and couldn’t see anything that would cause this strange behavior.

With the help of Adam Rush, the technical editor for this book, I found a strange workaround. For some reason, changing the style of the title in the navigation bar fixes the bug:

➤ Open ChecklistView.swift. In the body of ChecklistView, change the line that defines the navigation bar title from this:

.navigationBarTitle("Checklist")

To this:

.navigationBarTitle("Checklist", displayMode: .inline)

Want to see what this change does? Run the app and find out.

➤ Run the app. The checklist screen will look like this:

The checklist with an inline navigation bar title
The checklist with an inline navigation bar title

This alteration to the code changes the style of the navigation bar title from the default style…

Navigation bar with the default style title
Navigation bar with the default style title

…to the inline style, where the navigation bar’s title is on the same line as its buttons:

Navigation bar with the inline style title
Navigation bar with the inline style title

While I prefer the default style, I prefer a properly-working app even more.

Apple will eventually address this bug with an iOS update, and when that happens, this fix will no longer be necessary.

How to deal with SwiftUI bugs on your own

Just as developer tool vendors often rely on the developer community to find bugs, you can also rely on the developer community to help you find ways around them. Programming has a strong tradition of sharing information, and you’ll find that iOS programmers are generally an energetic and friendly bunch. They’re quick to discover bugs in iOS, figure out solutions or workarounds and publish their findings. If you frequent their online hangouts, you’re quite likely to find answers to your questions.

Whenever you find yourself stuck while working on a project in The iOS Apprentice, the first place you should go for help is the online forum for this book. It’s the perfect place to ask questions if anything we’ve written has you confused or isn’t working for you. A team of moderators keeps an eye on the forums, ensuring when you ask a question, you’re not just screaming into the void. When they see a new question, they alert the authors — Yours Truly included — and we’ll gladly help out.

Note: We’ll update this edition of The iOS Apprentice when Apple updates iOS 13 and makes fixes, and those updates are included in the cost of the book! Make sure that you check this book’s page on the raywenderlich.com site for new versions.

There are other places online that you may find useful — not just for dealing with SwiftUI bugs, but also for getting answers for your iOS programming questions and learning about other aspects of iOS programming. Here are some good starting points:

  • raywenderlich.com’s forums: These cover not just iOS topics, but Android, Unity and Flutter development as well.
  • Stack Overflow: This is the best-known of all the developer forum sites out there. If you’re a programmer, you’ll eventually end up here looking for (or possibly dispensing) answers. You’ll probably peruse the questions tagged iOS, Swift, and SwiftUI often.
  • Reddit’s iOSProgamming subreddit: The biggest collection of forums online has one dedicated to iOS programming. It’s so active that collectively, its readers have filed over 60,000 “radars” — that’s Apple’s term for bug reports and requests for features or enhancements.

Improving the user interface

Before closing out this chapter, make one more improvement to Checklist’s user interface that will make it more usable.

Disabling the “Add new item” button

Here’s a question: What happens if you create a new checklist item without providing a name? It’s time to be empirical and try it out.

➤ Run the app. Press the Add item button, which will display the Add new item sheet. Without entering a name for a new item, press the Add new item button.

Here’s what you’ll see when the Add new item sheet dismisses itself:

The checklist with an unnamed item
The checklist with an unnamed item

As you can see, the app currently lets the user add items without names to the list. The app shouldn’t allow this; each item should have at least one character in its name.

You can fix this easily by using one of Button’s methods: disabled, which accepts a single Boolean parameter. Setting this parameter to true disables the button so pressing it has no effect. Use it now to disable the button when the text field in the Add new item sheet is empty.

➤ Open NewChecklistItemView.swift and update NewChecklistItemView’s body to this:

var body: some View {
  VStack {
    Text("Add new item")
    Form {
      TextField("Enter new item name here", text: $newItemName)
      Button(action: {
        var newChecklistItem = ChecklistItem(name: self.newItemName)
        self.checklist.items.append(newChecklistItem)
        self.checklist.printChecklistContents()
        self.presentationMode.wrappedValue.dismiss()
      }) {
        HStack {
          Image(systemName: "plus.circle.fill")
          Text("Add new item")
        }
      }
      .disabled(newItemName.count == 0)
    }
    Text("Swipe down to cancel.")
  }
}

In making the change, you added one line to the end of the section that defines the button:

.disabled(newItemName.count == 0)

This line disables the button if newItemName’s count property is equal to zero. newItemName is a string property, and for properties that contain strings, the count property contains the number of characters in that string. An empty string — that is, a string that doesn’t contain any characters — has a count of zero.

Note: To determine if one value is equal to another, use the “double-equals” operator, ==. Don’t use the “single-equals”, =, which is for assigning values to constants and variables.

➤ Run the app. Press the Add item button, which will display the Add new item sheet. Note that the Add new item button is disabled:

The 'Add new item' sheet, with an empty text field and a disabled button
The 'Add new item' sheet, with an empty text field and a disabled button

➤ Enter some text — any text — into the text field. The Add new item button will enable:

The 'Add new item' sheet, with a non-empty text field and an enabled button
The 'Add new item' sheet, with a non-empty text field and an enabled button

If you delete all the text from the text field, the button will be disabled again. This has the effect you want: The user won’t be able to add an item to the list unless it has at least one character in its name.

With Checklists now able to add new items to the list, you’re one step closer to a fully-CRUD app!

Key points

  • You learned about CRUD apps, and what CRUD stands for: Create, Report, Update and Delete.
  • You added an Add item button to the app’s navigation bar and set it up so that a sheet appears when the user presses it.
  • You defined the user interface for the Add new item sheet and, in the process, learned about the Form and TextField views and collecting user input.
  • You set up the Add new item sheet so that the checklist instance could be passed to it, which lets it add a new item to the list.
  • You had a quick review of value and reference types.
  • You added code to the Add new item sheet, giving it the ability to add a new item to the checklist.
  • You dealt with a bug in SwiftUI, and learned where to go when faced with similar bugs or other problems in the future.
  • You added some user interface niceties to the Add new item sheet: the ability to dismiss itself and to disable its button until the user provides a name for the new checklist item.

You’ll find the project files for the app at this stage under 12 – Adding Items in the Source Code folder.

In the next chapter, you’ll make Checklists fully-CRUD and give it the ability to edit checklist items.

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.