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

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 12 chapters
Show chapters Hide chapters

53. Persistence & Polish
Written by Joey deVilla

Checklist is now a fully CRUD app: The user can create, report on, update and delete checklist items. It needs only a little more work before it can be considered a fully-functional basic checklist app, namely:

  • It needs to be able to save and load the user’s checklist items.
  • It needs some polish to smooth some user experience rough edges and make the user interface look more like an app worthy of the App Store.

In this chapter, you’ll learn about:

  • Knowing your app’s life story: It pays to know when certain events in your app’s lifecycle happen.
  • Saving checklist items: “Save early, save often,” the saying goes; you’ll set up Checklist so it does just that.
  • Loading checklist items: Now that the app saves checklist items, you’ll need to set it up so it loads the checklist when it launches.
  • Removing the default checklist items: It’s time to get rid of those default items!
  • Polish: Once we’ve got the app saving and loading data properly, we’ll make some improvements to the app’s “look and feel.”
  • Next steps: You’re at the end. What’s next?

Knowing your app’s life story

We’re going to use the same approach to saving and loading that you used when building the UIKit-based Checklists app, namely:

  • Saving the checklist data when the app is paused or terminated.
  • Loading the checklist data when the app is launched.

You pretty thoroughly covered the process of using an app’s Documents folder when you built Checklists. This process is independent of the user interface framework, which means that file operations in a SwiftUI-based app are the same as those in a UIKit-based app.

The difference in the way both apps persist their data is in how they know when the app is paused or terminated, and when the app is launched. In UIKit, this involves making changes to the code in the SceneDelegate.swift. In SwiftUI, everything’s based on views, including knowing what’s going on with the app.

Detecting when a view has appeared or disappeared

➤ Open ChecklistView.swift. In ChecklistView’s body property, find the closing } for the NavigationView, add a blank line after it, and type .on into that line.

Xcode will recognize that you’re trying to add a method call to the NavigationView object and will start suggestion methods whose names begin with “on”:

Xcode suggests a number of methods that begin with 'on'
Xcode suggests a number of methods that begin with 'on'

These “on” methods specify that an action should take place when a specific event occurs. In an earlier version of Checklist, when tapping a list item toggled its status between checked and unchecked, you used the onTapGesture method to respond to those taps.

Let’s experiment with the onAppear() and onDisappear() methods, which are called when a view appears and disappears, respectively.

➤ Complete the “on” you were just typing so that it becomes this code:

.onAppear {
  print("ChecklistView has appeared!")
}
.onDisappear {
  print("ChecklistView has disappeared!")
}

As a result, the body property of ChecklistView should look like this:

var body: some View {
  NavigationView {
    List {
      ForEach(checklist.items) { index in
        RowView(checklistItem: self.$checklist.items[index])
      }
      .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)
  }
  .onAppear {
    print("ChecklistView has appeared!")
  }
  .onDisappear {
    print("ChecklistView has disappeared!")
  }
}

Let’s do something similar with the EditChecklistItemView screen.

➤ Open EditChecklistItemView.swift. In EditChecklistItemView’s body property, find the closing } for the Form, add a blank line after it, then add the following:

.onAppear {
  print("EditChecklistItemView has appeared!")
}
.onDisappear {
  print("EditChecklistItemView has disappeared!")
}

The body property of EditChecklistItemView should look like this:

var body: some View {
  Form {
    TextField("Name", text: $checklistItem.name)
    Toggle("Completed", isOn: $checklistItem.isChecked)
  }
  .onAppear {
    print("EditChecklistItemView has appeared!")
  }
  .onDisappear {
    print("EditChecklistItemView has disappeared!")
  }
}

And finally, do the same for the NewChecklistItemView screen.

➤ Open NewChecklistItemView.swift. In NewChecklistItemView’s body property, find the closing } for the VStack, add a blank line after it, then add the following:

.onAppear {
  print("NewChecklistItemView has appeared!")
}
.onDisappear {
  print("NewChecklistItemView has disappeared!")
}

The body property of NewChecklistItemView should look like this:

var body: some View {
  VStack {
    Text("Add new item")
    Form {
      TextField("Enter new item name here", text: $newItemName)
      Button(action: {
        let 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.")
  }
  .onAppear {
    print("NewChecklistItemView has appeared!")
  }
  .onDisappear {
    print("NewChecklistItemView has disappeared!")
  }
}

➤ Run the app and watch Xcode’s debug console. ChecklistView has appeared! gets printed to the console.

➤ Tap any list item to edit it, then dismiss the edit item screen by tapping the Checklist button. You’ll see EditChecklistItemView has appeared! in the console when you enter the edit item screen and EditChecklistItemView has disappeared! when the edit item screen disappears and you return to the checklist.

➤ Finally, tap on Add item, then dismiss the add item screen either by adding a new item or swiping down. You’ll see NewChecklistItemView has appeared! in the console when the add item screen appears and NewChecklistItemView has disappeared! when the add item screen disappears and you return to the checklist.

➤ Play with the app. Note that no matter what you do, the message ChecklistView has disappeared! never appears in the console. That’s because it never actually disappears. It’s still there at the bottom of the navigation stack, covered by either the add item or edit item screen.

On the other hand, the add item and edit item screens do disappear when dismissed, which is why their “disappear” messages get printed on the console.

SwiftUI’s onAppear() and onDisappear() methods are similar to UIKit’s viewDidAppear() and viewDidDisappear() methods, and the closest things to viewDidLoad() and viewDidUnload(). They’re more flexible, as you can attach them to any view — try adding onAppear() or onDisappear() to any SwiftUI user interface control, such as a button or text view. You can use them to perform set-up or take-down tasks when a screen or any on-screen item is shown or taken away.

Detecting when the app has gone into the background, returned to the foreground or been terminated

In the UIKit Checklists app, you detected when the app was sent to the background or had been terminated by the user by making use of the sceneWillResignActive() and sceneDidDisconnect() methods in SceneDelegate.swift. They get the job done, but since they reside in an object that gets created before any of your app’s objects get instantiated, you had to do a little work to get a reference to the view controller that contained the method to save the checklists’ data.

The SceneDelegate.swift file also exists in SwiftUI projects, which means we could use these methods in the SwiftUI-based Checklist. We won’t do that — instead, we’ll one of the “on” methods that SwiftUI provides for its view: onReceive().

The onReceive() method connects a view to another object that sends messages. By establishing this connection, the view becomes the subscriber in this relationship and the object to which it is connected becomes the publisher. With the connection established, the subscriber continuously “listens” to the publisher for a specific message. If the publisher sends the message that the subscriber is listening for, the onReceive() method executes the code provided to it.

We’ll use onReceive() to listen to messages from iOS’ Notification Center, the internal messaging system that lets different parts of an app send messages to each other and lets the iOS notify your app when certain system events happen. Since ChecklistView is always active when the app is running, it makes sense to use its onReceive() method to listen for and respond to Notification Center messages.

➤ In ChecklistView.swift, add the following to the end of ChecklistView’s body property, after the calls to the onAppear() and onDisappear() methods:

.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) {_ in
  print("willResignActiveNotification")
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) {_ in
  print("didEnterBackgroundNotification")
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) {_ in
  print("willEnterForegroundNotification")
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) {_ in
  print("didBecomeActiveNotification")
}

These four calls to onReceive() are each listening for these Notification Center messages:

  1. willResignActiveNotification — The app is about to go into the background and become inactive. This is often followed by didEnterBackgroundNotification.
  2. didEnterBackgroundNotification - The app is now in the background.
  3. willEnterForegroundNotification - The app is about to enter the foreground and become the active app. This is often followed by didBecomeActiveNotification
  4. didBecomeActiveNotification - The app is now in the foreground.

➤ Run the app. While keeping an eye on Xcode’s debug console, look at the messages that appear. When the app starts, you’ll see didBecomeActiveNotification appear in the debug console. After launch, the app is in the foreground and the active app.

➤ Start the process of switching to another app, so that the screen looks something like this:

Switching between apps to see the notification message
Switching between apps to see the notification message

Notice that the newest line in the debug console is willResignActiveNotification.

➤ Don’t switch to another app. Instead, tap on the Checklist screen to bring it back to the foreground. Another didBecomeActiveNotification will appear in the debug console.

➤ Go to the Home screen. You’ll see these two message in the debug console, one after the other: first willResignActiveNotification, then didEnterBackgroundNotification.

➤ Switch back to Checklist. You’ll see these two message in the debug console, one after the other: first willEnterForegroundNotification, then didBecomeActiveNotification.

➤ Finally, terminate Checklist. You’ll see the same messages you saw when the app went into the background: first willResignActiveNotification, then didEnterBackgroundNotification.

Now that you know your app’s life story, you can set up Checklist to use the same strategy for preserving checklists as the one you used for the UIKit-based Checklists: Saving the checklist data when the app goes into the background.

Saving checklist items

You’ll save and load the checklist data to the app’s Documents directory, which is the designated place for storing data within the “sandboxed” file system that only your app can access. Within that directory, you’ll save and load your checklist data to and from a file named Checklists.plist.

Finding the right place in the file system

In order to read data from and write data to that file, you’ll use a couple of methods that you wrote back when you were working on Checklists:

  • A method to get a reference to the Documents directory, and
  • a method to create a path to the Checklists.plist within that directory.

➤ In Checklist.swift, add the following methods to Checklist after all its other methods:

// MARK: File management

func documentsDirectory() -> URL {
  let paths = FileManager.default.urls(for: .documentDirectory,
                                       in: .userDomainMask)
  let directory = paths[0]
  print("Documents directory is: \(directory)")
  return directory
}

func dataFilePath() -> URL {
  let filePath = documentsDirectory().appendingPathComponent("Checklist.plist")
  print("Data file path is: \(filePath)")
  return filePath
}

In Checklists, you placed these methods in the ChecklistViewController class. This class had a lot of responsibilities: Not only did it draw the checklist, it also managed adding new checklist items and editing existing ones, as well as saving and loading checklist data.

In UIKit projects, it’s too easy and tempting to put all the code inside the view controllers. Doing so lets you get an app up and running quickly, which is why a lot of tutorials take this approach. The problem with this approach is that over time, your view controllers grow in size to cover hundreds (and even thousands) of lines, which makes them hard to debug and maintain.

We averted this problem by structuring Checklist around the MVVM (Model-View-ViewModel) pattern a couple of chapters ago:

The model, view and ViewModel in Checklist
The model, view and ViewModel in Checklist

  • ChecklistView is the app’s View, and its code is all about presenting an interface to the user and responding to messages that the user interface receives.
  • Checklist is the ViewModel, which contains the properties and methods that the view needs to show data to the user and to respond to the user’s actions. It also has access to the model objects.
  • ChecklistItem instances make up the Model.

Since the checklist is stored in the ViewModel, Checklist, as its items property, Checklist is a good place to put methods for saving and loading the app’s data.

Saving the file

Now that you have methods that determine where the app will write Checklist.plist, it’s time to write a method to save that file.

➤ Add the following method to Checklist.swift:

func saveChecklistItems() {
  // 1
  print("Saving checklist items")
  // 2
  let encoder = PropertyListEncoder()
  // 3
  do {
    // 4
    let data = try encoder.encode(items)
    // 5
    try data.write(to: dataFilePath(),
                   options: Data.WritingOptions.atomic)
    // 6
    print("Checklist items saved")
    // 7
  } catch {
    print("Error encoding item array: \(error.localizedDescription)")
  }
}

If this method gives you a sense of déja vu, it’s because you’ve seen it before. You wrote almost the exact same code to save items in Checklists — this one just has a couple of additional print() functions. While UIKit and SwiftUI require you to take different approaches to coding the user interface, the code for the logic behind the user interface is the same.

It’s been a while since you’ve seen this code, so let’s review what it does.

This method takes the contents of the items array, converts it to a block of binary data and then writes this data to the Checklist.plist file in the app’s Documents directory.

In order to understand this code, go through the commented lines step-by-step:

  1. This print() function is here for debugging purposes while working on the app. It lets you know that the method has been called.

  2. This creates an instance of PropertyListEncoder, an object that takes the data stored in an object and encodes that data into a property list.

  3. We’re calling methods that can result in failure or errors, which means that we need to put them in a do - catch block. Here’s a reminder of what these blocks do:

‘do’ and ‘catch blocks, illustrated’
‘do’ and ‘catch blocks, illustrated’

The do block contains code that might fail or result in a error — or, as you say in programming, throw an error. It lets you mark lines of code that might fail with the try keyword, and if any of those lines throw an error, the code in the catch block takes over.

  1. Here, you call the encoder’s encode() method to encode the items array. The method could fail. It throws an error if it’s unable to encode the data for some reason: Perhaps it’s not in the expected format, or it’s corrupted, or the device’s flash drive is unavailable.

The try keyword indicates that the call to encode can fail and if that happens, that it will throw an error. The try keyword is mandatory when calling methods that throw errors; in fact, if you remove the try keyword that comes before encoder.encode(items), Xcode will display an error message.

If the call to encode() fails, execution will immediately jump to the catch block instead of proceeding to the next line.

  1. If the call to encode() succeeds, data now contains the contents of the items array in encoded form. This line attempts to write this encoded data to a file using the file path returned by a call to dataFilePath(). The write() method, like many file operations, can fail for many reasons and throw an error. Once again, you have to make use of a try statement, so the catch block can handle the case where write() fails.

  2. This is another print() function for debugging purposes. Its message will appear in Xcode’s debug console only if the save was successful.

  3. This is the start of the catch block, which contains the code to execute if any line of code in the do block threw an error. The code in this block executes only if the code in the do block throws an error. If you were planning to sell this app in the App Store, you might do all kinds of things with this code to deal with cases where encoding the data or writing it to the device’s file system fails. In this case, you’ll simply print out an error message to Xcode’s console.

You might notice that the print() statement in the catch block references an error variable. Where did that come from?

When you create a pair of docatch code blocks, you can explicitly check for specific types of errors. Swift will automatically create a local variable named error that contains information about the error thrown by the code within the do block. You can refer to that error variable within the catch block, which is handy for printing out a descriptive error message that should give you a hint about what went wrong.

Immediately after entering saveChecklistItems(), Xcode will show the error message “Referencing instance method ‘encode’ on ‘Array’ requires that ‘ChecklistItem’ conform to ‘Encodable’”:

The error that appears after adding 'saveChecklistItems()'
The error that appears after adding 'saveChecklistItems()'

Like the method you just added, you’ve seen this error before. It tells you that any object encoded by a PropertyListEncoder must conform to the Encodable protocol, which provides an object any additional properties and methods it needs to encode itself.

You could simply make ChecklistItem conform to Encodable, which would make it possible to encode the checklist data, which in turn could be written to the file. However, as you saw with Checklists, that saved data will eventually be read from the file, and that data will have to be decoded. You could make ChecklistItem also conform to the Decodableprotocol; remember, a class or struct can conform to any number of protocols.

Instead, you’ll make ChecklistItem conform to the Codable protocol, a single protocol that gives an object the powers granted by both Encodable and Decodable.

➤ Switch to ChecklistItem.swift and modify the struct line as follows:

struct ChecklistItem: Identifiable, Codable {

As you type Codable, Xcode’s suggestion pop-up will appear and tell you that Codable is made up of Encodable and Decodable:

Xcode says that Codable is made up of Encodable and Decodable
Xcode says that Codable is made up of Encodable and Decodable

With this change, the error will disappear.

Putting saveChecklistItems() to use

You have to call the new saveChecklistItems() method when the app is put into the background.

Challenge: Before you go forward and look at the code, think about everything you’ve covered in this chapter so far. Where in the source code would you call saveChecklistItems()?

Answer: In the method that gets called whenever the app is about to be put into the background. That’s the onReceive() call attached in ChecklistView that’s listening for the willResignActiveNotification message.

➤ In ChecklistItemView.swift, add a call to Checklist’s new saveChecklistItems() method to the onReceive() call listening for the willResignActiveNotification message. The call should end up looking like this:

.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) {_ in
  print("willResignActiveNotification")
  self.checklist.saveChecklistItems()
}

For the following steps, make sure that you’re running the app in the Simulator. This will allow you to use the file system on your Mac to examine the Documents directory on your simulated device.

➤ Run the app in the Simulator. Change the name of the first item in the checklist to “Enable saving in the app”:

The first item in the list is now “Enable saving in the app”
The first item in the list is now “Enable saving in the app”

➤ Go to the Home screen. You can do this by clicking on the Home button in the Simulator, pressing +Shift+H on your keyboard or selecting Home from the Simulator’s Hardware menu.

The app will move to the background, which should cause the contents of the checklist to be saved. You’ll confirm this by looking at the saved file. You’ll need the location of that saved file; luckily, the app printed it in Xcode’s debug console.

➤ In the text that was most recently added to Xcode’s debug console, look for the phrase “Documents directory is:”.

The file path that follows “Documents directory is:” will differ from device to device. Here’s what I saw in Xcode’s debug console when I ran the app the Simulator on my computer:

Console output showing Documents folder and data file locations
Console output showing Documents folder and data file locations

➤ Copy the file path for the Documents directory, but don’t include the file:// bit (there are three / characters that follow file:; don’t include the first two). The path that you copy should starts with /Users/, complete with the opening /.

➤ Open a new Finder window by clicking on the Desktop and typing Command+N or by clicking the Finder icon in your dock, if you have one. Then press Command+Shift+G or select Go ▸ Go to Folder… from the menu.

You’ll see a dialog box that says Go to the folder:

The 'Go to the folder:' dialog box
The 'Go to the folder:' dialog box

➤ Paste the full path of the Documents folder into the text field in the dialog box and click the Go button:

The 'Go to the folder:' dialog box with the 'Documents' directory pasted in
The 'Go to the folder:' dialog box with the 'Documents' directory pasted in

The Finder window shows you the contents of that folder:

The Documents directory now contains a Checklist.plist file
The Documents directory now contains a Checklist.plist file

Double-click Checklists.plist. Since Xcode is installed on your Mac, it’s probably what will open the file. Xcode will display the file contents using the plist editor:

The Documents directory now contains a Checklist.plist file
The Documents directory now contains a Checklist.plist file

The Root item represents the entire file. There should be five Item entries, number 0 through 4, each one representing an item in the checklist. The app had five items in the list, so it appears to have saved the correct number of items.

➤ Open the items to confirm that it saved them properly. You should see something like this:

The Documents directory now contains a Checklist.plist file
The Documents directory now contains a Checklist.plist file

Your id values will be different, but the name and isChecked values on your system should be the same as these:

  • Item 0:
    • name: Enable saving in the app
    • isChecked: NO
  • Item 1:
    • name: Brush my teeth
    • isChecked: NO
  • Item 2:
    • name: Learn iOS development
    • isChecked: YES
  • Item 3:
    • name: Soccer practice
    • isChecked: NO
  • Item 4:
    • name: Eat ice cream
    • isChecked: YES

If you see the values shown above, the checklist data is saving properly. It’s now time to take care of loading the data.

Loading checklist items

Loading the file

As you have already guessed, the next method you’ll write is loadChecklistItems(), and it’s pretty much the same method as the one that you wrote for Checklists (once again, it just has some extra print() functions). It’s like the encoding and saving process — but in reverse.

➤ Open Checklist.swift and add the following new method, just after saveChecklistItems():

func loadChecklistItems() {
  // 1
  print("Loading checklist items")
  // 2
  let path = dataFilePath()
  // 3
  if let data = try? Data(contentsOf: path) {
    // 4
    let decoder = PropertyListDecoder()
    do {
      // 5
      items = try decoder.decode([ChecklistItem].self,
                                 from: data)
      // 6
      print("Checklist items loaded")
      // 7
    } catch {
      print("Error decoding item array: \(error.localizedDescription)")
    }
  }
}

As we did with saveChecklistItems(), let’s go through the commented lines in loadChecklistItems() step-by-step:

  1. This print() function is here for debugging purposes while working on the app. It lets you know that the method has been called.

  2. First, you store the results of dataFilePath() — the path to the Checklist.plist file — in a temporary constant named path.

  3. The method tries to load the contents of Checklist.plist into a new Data object. The try? command attempts to create the Data object, but returns nil — Swift’s way of saying “no result” — if it fails. That’s why you put it in an if let statement.

Why would it fail? If there is no Checklist.plist file, then there are obviously no ChecklistItem objects to load. This happens when the app starts up for the very first time. In that case, you’ll skip the rest of this method.

Notice that this is another way to use the try statement. Instead of enclosing the try statement within a do block, as you did previously, you have a try? statement that indicates that the try could fail. If it does, it will return nil. Whether you use the do block approach or this one is completely up to you.

  1. When the app does find a Checklist.plist file, the method creates an instance of PropertyListDecoder.

  2. The method loads the saved data back into items using the decoder’s decode method. The only item of interest here is the first parameter passed to decode . The decoder needs to know what type of data the result of the decode operation will be. You let it know that it will be an array of ChecklistItem objects.

This populates the array with exact copies of the ChecklistItem objects that you froze into the Checklist.plist file.

  1. This is another print() function for debugging purposes. Its message will appear in Xcode’s debug console only if the load was successful.

  2. This is the start of the catch block, which contains the code that executes if any line of code in the do block throws an error.

As with saveChecklistItems(), if this were an app that would go into the App Store, this code might do all sorts of things to deal with cases where decoding the data or reading it from the device’s file system fails. Once again, you’ll simply print out an error message to Xcode’s console.

Putting loadChecklistItems() to use

You now have the loadChecklistItems() method, which restores the app’s data from Checklist.plist.

Challenge: Before continuing, ask yourself: Where in the source code would you call the loadChecklistItems() method?

There’s only one time when you need to load the saved checklist data: when the app launches, or more specifically, at the moment when the Checklist instance is created. This is where Checklist’s initializer — its init() method comes in handy. It’s called at that very moment, making it the perfect place to put the call to loadChecklistItems().

If you go to Checklist.swift and look for an initializer, you’ll see that it doesn’t appear to have one. That’s because classes whose properties are defined at the start have an implicit initializer — an invisible init() method that has no parameters and no body that gets automatically called when you create an instance.

Now that you need something to happen when Checklist is instantiated, you need to create an initializer.

➤ Open Checklist.swift and add an init() method to the start of Checklist’s “Methods” section. init() should look like this:

init() {
  loadChecklistItems()
}

Note that there’s no call to super.init() in this initializer. That’s because this class doesn’t have a superclass.

It’s time to test loadChecklistItems() by seeing if the app “remembers” changes to the checklist after the user closes and reopens it.

➤ If the app is still running in the Simulator, stop it first.

➤ Run the app in the Simulator, then makes some changes to the checklist.

In my case, I made these changes:

  • Edited the name of the “Walk the dog” item, changing it to “Walk the cat.”
  • Deleted the “Brush my teeth’ item.
  • Checked the “Soccer practice” item.
  • Rearranged the items so that “Eat ice cream” comes before “Soccer practice.”

After these changes, my checklist looked like this:

The updated checklist
The updated checklist

➤ Close the app — but do it by terminating the app in the Simulator (use the App Switcher, which you can access in the Simulator’s Hardware menu). Don’t click the Stop button in Xcode this time.

There’s a reason why I’m asking you not to stop the app in Xcode. I’ll explain in the next section, which is appropriately titled Fixing the “save” bug.

➤ Restart the app. Instead of the five default checklist items, you should see the checklist as it was when you quit the app.

In my case, the newly-launched app showed this checklist:

The updated checklist
The updated checklist

You now have an app that loads and saves data!

The “save” bug in action

➤ Run the app in the Simulator. You should see the checklist as you last left it:

The checklist as you last left it
The checklist as you last left it

➤ Go to the Home screen. Among the messages that Xcode debug console should display are Saving checklist items and Checklist items saved.

➤ Return to the app. The Xcode console should display the messages willEnterForegroundNotification and didBecomeActiveNotification.

➤ Change any item’s name to “Fix the ‘save’ bug”.

The checklist, with a “Fix the 'save' bug item
The checklist, with a “Fix the 'save' bug item

You’ll also see the EditChecklistItemView has appeared! and EditChecklistItemView has disappeared! messages in the process.

➤ Clear the debug console by clicking on the “Trash” button located near its lower right:

Clearing the debug console by clicking the “Trash” button
Clearing the debug console by clicking the “Trash” button

➤ Use Xcode to stop the app. Note the complete lack of messages, including anything about saving the checklist items.

➤ Run the app in the Simulator. The change you made was never saved:

The checklist as it was before you saved it
The checklist as it was before you saved it

When you stopped the app in Xcode, the Saving checklist items and Checklist items saved messages never appeared. This means that:

  • the saveChecklistItems() method in Checklist never got called, which means
  • the onReceive() method in ChecklistView never got triggered, which means
  • the willResignActiveNotification message never got sent by the system.

From time to time, a developer will write code that causes the app to hang. For this reason, Xcode’s “Stop” button probably skips some of the niceties involved in stopping an app, which appears to include sending out the usual system messages when an app terminates.

This is a problem that only developers will face, since they won’t be running the app from Xcode. Just keep this issue in mind when building apps.

Removing the default checklist items

Since Checklist now remembers its checklist items between sessions, it no longer needs its default checklist items.

You want the app to behave this way:

  • If the app has been launched before, it should contain the same checklist items from the previous session when it launches again.
  • If the user has just installed the app and has never used it, the app should display an empty checklist at launch.

This change in behavior is easy to accomplish.

➤ Switch to Checklist.swift and change the definition of the items property from this:

@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 this:

@Published var items: [ChecklistItem] = []

Now, test the effect of this change. You’ll need to run the app as if it were freshly installed and never used, which requires getting rid of the existing Checklist.plist file.

The easiest way to do this is to delete the app from the Simulator or device. Deleting an app deletes its file system and any data within.

➤ If the app is on the Simulator or device, delete it.

➤ Run the app from Xcode, which will install it in the process. You should now see an empty checklist, ready for you to fill it:

An empty checklist
An empty checklist

Next, make sure that everything in the app works properly.

➤ Tap the Add item button, enter a name for the new item, and tap the Add new item button. You’ll return to the checklist, which will display the newly-created item:

The checklist with a newly-created item
The checklist with a newly-created item

➤ Close the app, remembering NOT to do it by stopping it in Xcode.

➤ Run the app again. You’ll see that the checklist is the same as when you closed the app:

The saved checklist reappears when you restart the app
The saved checklist reappears when you restart the app

With this final change, Checklist is fully functional! It’s now time to give the app some polish.

Polish

Fixing the way rows are highlighted

There’s something a little odd about the way a row is highlighted when it’s selected. To see what I mean, do the following.

➤ Run the app. In the checklist view, press and hold a row. If your device or Simulator is in light mode, the row will look like this:

A selected row in light mode
A selected row in light mode

In dark mode, the row will look like this:

A selected row in dark mode
A selected row in dark mode

In both modes, the background behind the text view, spacer, and checkbox have kept their background color while the rest of the row has the highlight color.

This is a side effect of using the background() method to color the row to eliminate the unresponsive “dead zone” between the text and checkbox. This was necessary back when the app used the onTapGesture() method to respond to taps on rows.

➤ Open RowView.swift and look at RowView’s body property. You’ll see this:

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

Notice that the object returned by the body property is no longer the HStack view that defined the row, but a NavigationLink view that contains the HStack. A NavigationLink makes everything it contains tappable. Perhaps the background() method isn’t necessary anymore.

➤ Remove the call to background() so that RowView’s body property looks like this:

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

➤ Run the app. In the checklist view, press and hold a row. If your device or Simulator is in light mode, the row will now look like this:

A selected row in light mode
A selected row in light mode

In dark mode, the row will look like this:

A selected row in dark mode
A selected row in dark mode

With that one change, a major user interface quirk in the app is eliminated. Let’s handle another UI oddity.

Fixing the way the “Edit item” screen slides into view

Another strange bit of user interface behavior comes up when the edit item view slides into place. The elements of the Form view slide in a little lower than they should at first…

The edit item screen, immediately after sliding into view
The edit item screen, immediately after sliding into view

…and then after a split second, those items pop into place:

The edit item screen, a moment after
The edit item screen, a moment after

This is a SwiftUI quirk, and one of those annoyances that comes with working with a new framework. Unlike UIKit, which has had the benefit of over a dozen years to be refined and have its bugs fixed, SwiftUI is very new and still has a number of “rough edges.” Unfortunately, you’ve just run into one of them.

The fix is a non-intuitive one, and it came from Adam Rush, this book’s editor. It involves changing the style of the navigation bar title in the checklist view.

➤ Open ChecklistView.swift and change the call to navigationBarTitle() in ChecklistView’s body property from this…

.navigationBarTitle("Checklist")

…to this:

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

This changes the way the title in the navigation bar is displayed from a large headline below the navigation bar to a smaller headline positioned inline, as shown in this screen shot:

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

➤ Run the app. Tap on a row to bring the edit item screen into view. The “popping” effect is gone.

Icons

Finally, the app needs some icons.

➤ Open the asset catalog (Assets.xcassets) and select AppIcon:

The AppIcon asset screen with no icons
The AppIcon asset screen with no icons

You’ll see empty slots for the app’s icon in all its size. Each empty slot has the expected dimensions of its icon specified in the middle of the slot. For example, the 2x icon for iPhone Notification expects a 40-pixel by 40-pixel icon.

➤ In Finder, open the Resources folder for this section and then open the Icon folder. You’ll see 13 files named Icon-20.png through Icon-1024.png The number in each filename is its square dimensions in pixels; for example, Icon-20.png is a 20-pixel by 20-pixel icon.

➤ For each icon slot in the asset catalog, drag the appropriate size icon into that slot:

Dragging Icon-120.png into a 120px x 120px icon slot
Dragging Icon-120.png into a 120px x 120px icon slot

In the end, the AppIcon assets screen should look like this:

The AppIcon asset screen with all its icons
The AppIcon asset screen with all its icons

➤ Run the app, then put it in the background. You’ll see its new icon on the SpringBoard:

The app’s new icon on the SpringBoard
The app’s new icon on the SpringBoard

Next steps

You’ve just finished writing your second SwiftUI app. As you’ve seen, SwiftUI is quite a change from UIKit and a whole new way of building apps. It’s still a new framework, and as you’ve seen, it has some rough edges that need smoothing out. It may also be some time before it’s the way most iOS apps are written.

The state-based approach to building apps that SwiftUI uses is similar to the way it’s done on other platforms, such as React and Flutter. It appears to be the future direction of UI building. Learning SwiftUI isn’t just learning the way that iOS apps will be written in the future, but possibly learning the way that software in general will be written.

SwiftUI is a rapidly-evolving platform, with new features and bug fixes being added to it all the time. There aren’t any SwiftUI experts with years of experience, even at Apple, and we’re all learning it as we go along. This is an area where a willingness to experiment and explore will serve you well.

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.