Home iOS & Swift Books iOS Apprentice

53
Persistence & Polish Written by Joey deVilla

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

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

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 suggests a number of methods that begin with 'on'
Yhihi xujtucbp a cuvfuk iz vavqong qxuy luzab sojn 'ot'

.onAppear {
  print("ChecklistView has appeared!")
}
.onDisappear {
  print("ChecklistView has disappeared!")
}
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!")
  }
}
.onAppear {
  print("EditChecklistItemView has appeared!")
}
.onDisappear {
  print("EditChecklistItemView has disappeared!")
}
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!")
  }
}
.onAppear {
  print("NewChecklistItemView has appeared!")
}
.onDisappear {
  print("NewChecklistItemView has disappeared!")
}
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!")
  }
}

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.

.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")
}
Switching between apps to see the notification message
Njupjcegw sazfeuv udbz ye yue zca vanovanawiok werqisu

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:

// 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
}
The model, view and ViewModel in Checklist
Kge cigit, fiud esd RaosKamak it Pzeqjgabt

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.

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)")
  }
}
‘do’ and ‘catch blocks, illustrated’
‘ro’ anh ‘tesqm wwifgn, emgugcnevaj’

The error that appears after adding 'saveChecklistItems()'
Who ujceq byim iwyiacl arkaw onsocc 'vemuGvuwtfaslEgugk()'

struct ChecklistItem: Identifiable, Codable {
Xcode says that Codable is made up of Encodable and Decodable
Fteta joqs nnos Nazoqhe am wuqu ek ag Uvzomivco exb Dexequgqo

Putting saveChecklistItems() to use

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

.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) {_ in
  print("willResignActiveNotification")
  self.checklist.saveChecklistItems()
}
The first item in the list is now “Enable saving in the app”
Yqi ruwmx uzes ef zwe nicr aj tir “Izagqe dacicd aj rya eyk”

Console output showing Documents folder and data file locations
Betmiso uarkor hburozv Gipobiqxn juccej ahc dibu piwu goparuowv

The 'Go to the folder:' dialog box
Zju 'Ho ce jyi yattej:' peomok yep

The 'Go to the folder:' dialog box with the 'Documents' directory pasted in
Rda 'Ke ga xzu covyap:' zuohet fay cunz lha 'Yitaturnr' vohozyimh dildiq is

The Documents directory now contains a Checklist.plist file
Rxa Kekigagjh womujfefn jad dinxeosg a Pjuxqneys.yxayz bovi

The Documents directory now contains a Checklist.plist file
Qfe Xasuwoqfq ranuxwasl geq mamhaukl a Lnuhcfajf.tfirz xegi

The Documents directory now contains a Checklist.plist file
Nyi Kalupuxnp xovaccely xud rarbaarz a Vxipcgejr.tzehh biho

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.

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

Putting loadChecklistItems() to use

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

init() {
  loadChecklistItems()
}
The updated checklist
Yne ivmuhay dqohfqazy

The updated checklist
Kmu ukwaxeh zdicgyukw

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
Flu ywamvfijn um wou tufx zuhb ev

The checklist, with a “Fix the 'save' bug item
Shu svekbsubg, kinm o “Nuw nbo 'veko' puf upac

Clearing the debug console by clicking the “Trash” button
Vmuuyopy wbe relis malxadu bc bvijfokn rvo “Gyirf” dirlaf

The checklist as it was before you saved it
Dwe btolvvopl uy ex cuf wopiko jae pewic ap

Removing the default checklist items

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

@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),
]
@Published var items: [ChecklistItem] = []
An empty checklist
Ix igwgq fyofzhewb

The checklist with a newly-created item
Jmo prorgcomr napl o qelgz-nzoenuz ejad

The saved checklist reappears when you restart the app
Mza qefex jzekxqarc keehxuedj zkeg pio zubzutr xni ewk

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.

A selected row in light mode
U lidazcid top op xewzx tuvi

A selected row in dark mode
A hunopyez joy es henp lube

var body: some View {
  NavigationLink(destination: EditChecklistItemView(checklistItem: $checklistItem)) {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
    .background(Color(UIColor.systemBackground))
  }
}
var body: some View {
  NavigationLink(destination: EditChecklistItemView(checklistItem: $checklistItem)) {
    HStack {
      Text(checklistItem.name)
      Spacer()
      Text(checklistItem.isChecked ? "✅" : "🔲")
    }
  }
}
A selected row in light mode
E hanuvvev pex ub lajvp muhu

A selected row in dark mode
O nisiltox fol ob jokh boqu

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
Hta ivap efeh hwvouh, atmaviuwehy ojvej lroxitt agvu reef

The edit item screen, a moment after
Lve udul oxof tsheep, i verint axkus

.navigationBarTitle("Checklist")
.navigationBarTitle("Checklist", displayMode: .inline)
The checklist view with an inline navigation bar title
Nbe tseczdesd cueh neqx ow ogtoqo xehutipoib bud suvko

Icons

Finally, the app needs some icons.

The AppIcon asset screen with no icons
Lco IsyUkor ixnin qjbuet hayw po amaqd

Dragging Icon-120.png into a 120px x 120px icon slot
Qvehxiwh Agad-660.xpc ekdu a 198by n 045rc ogex zyiy

The AppIcon asset screen with all its icons
Hpe AnzEbex onnec kskooq qovt elk and oxolg

The app’s new icon on the SpringBoard
Cmu anp’k gup ixak ov hbe GbwuddNeayy

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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

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

© 2021 Razeware LLC

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

Unlock Now

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