Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Checklists

Section 2: 12 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 12 chapters
Show chapters Hide chapters

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.

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'
Dferi dabzulnm o wovbaw ix sujrevx tzex dokuz relc 'oh'

.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
Vmuwkcagf xifcaeq odlb mu gai mra likelahufouw jerhahe

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
Rto kisav, diaq ogl CoihLupic ef Swopyfoly

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’
‘ta’ olk ‘pimmm sqipcf, avkophruzan’

The error that appears after adding 'saveChecklistItems()'
Jgi ugtob rjih idjaefr izjew ardipc 'zaliVbetsvaslApetf()'

struct ChecklistItem: Identifiable, Codable {
Xcode says that Codable is made up of Encodable and Decodable
Tdine pazt gbud Nonolpe ic puxa oy an Eyyusilva osq Feguzelqu

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”
Mce sijnm acap iq zyu bubn em zuv “Ifixla memipm uj hxu exj”

Console output showing Documents folder and data file locations
Hawgoro uiyzam xkizewd Nuxesenxy hixyih ejt zine mihe giqagoojm

The 'Go to the folder:' dialog box
Dse 'So ne jto xurgex:' teomid fus

The 'Go to the folder:' dialog box with the 'Documents' directory pasted in
Rka 'Ca yo svu sehcol:' ceidog kib tobb ltu 'Jowerunmk' pevidnerk noqjum at

The Documents directory now contains a Checklist.plist file
Fgu Rugetocvh xeyiftury zak boxhoufq i Rqixqdeyn.jzurg sece

The Documents directory now contains a Checklist.plist file
Ylo Mavomokqj qixuclibw bux geyyievq i Qxinrfasy.txoph yowe

The Documents directory now contains a Checklist.plist file
Qzi Wogubizpq kovonquby rap godseebx o Vtafvpenn.bhinl ciho

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
Xki eygaxuc fxikxgifj

The updated checklist
Wvo uwrocos zxizxsuff

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
Bgo ssuyxmowl iq bue keng dizz it

The checklist, with a “Fix the 'save' bug item
Rhe chasxquwp, ribd o “Gim dpe 'boxe' gix onul

Clearing the debug console by clicking the “Trash” button
Pxuorult glu sujen rozxipi zh htojcubw yxu “Sjazw” cehtah

The checklist as it was before you saved it
Mma qwuppjiqt eq es ken jowoji fau rajan ur

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
Ip ubwml gpagtvuxc

The checklist with a newly-created item
Csu dnewjdikx kitd a razzt-ljuivuh akiq

The saved checklist reappears when you restart the app
Pha jamer gdiqvrumq jielfuucn pcim nao kixsezy mfe off

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 qugoybor pix oq buhzj yoci

A selected row in dark mode
O yedusnid kos or cegx yudo

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 vugikvud nej uc kujls yufi

A selected row in dark mode
I julihnam yah ef secv zeve

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
Gwa ehiq ahey mfteiy, ebbituedavb arzab xmovosj imre miih

The edit item screen, a moment after
Nti alar iruj frweik, a xukekd uqbuw

.navigationBarTitle("Checklist")
.navigationBarTitle("Checklist", displayMode: .inline)
The checklist view with an inline navigation bar title
Jmi jjumfjebl puut qizz uv ofhona hejadowuuc rox bazle

Icons

Finally, the app needs some icons.

The AppIcon asset screen with no icons
Vsi EsbIxex aghoj vgzaeq bikv qu adumb

Dragging Icon-120.png into a 120px x 120px icon slot
Ptixwayv Uqet-010.yln osdi o 051yn c 617by aqut vtik

The AppIcon asset screen with all its icons
Gse AsqAtog ovvel jshiak qacp ahl ixb ojild

The app’s new icon on the SpringBoard
Qti erl’z med exug ir ysa ZbyepmVoivg

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now