Chapters

Hide chapters

SwiftUI Apprentice

Second Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section I: Your First App: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your Second App: Cards

Section 2: 9 chapters
Show chapters Hide chapters

8. Saving History Data
Written by Caroline Begbie

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

@AppStorage is excellent for storing lightweight data such as settings and other app initialization. You can store other app data in property list files, in a database such as SQLite or Realm, or in Core Data. Since you’ve learned so much about property list files already, you’ll save the history data to one in this chapter.

The saving and loading code itself is quite brief, but when dealing with data, you should always be aware that errors might occur. As you would expect, Swift has comprehensive error handling so that, if anything goes wrong, your app can recover gracefully.

In this chapter, you’ll learn about error checking techniques as well as saving and loading from a property list file. Specifically, you’ll learn about:

  • Optionals: nil values are not allowed in Swift unless you define the property type as Optional.
  • Debugging: You’ll fix a bug by stepping through the code using breakpoints.
  • Error Handling: You’ll throw and catch some errors, which is just as much fun as it sounds. You’ll also alert the user when there is a problem.
  • Closures: These are blocks of code that you can pass as parameters or use as completion handlers.
  • Serialization: Last but not least, you’ll translate your history data into a format that can be stored.

Adding the Completed Exercise to History

➤ Continue with your project from the previous chapter, or open the project in this chapter’s starter folder.

➤ Open HistoryStore.swift and examine addDoneExercise(_:). This is where you save the exercise to exerciseDays when your user taps Done.

Currently, on initializing HistoryStore, you create a fake exerciseDays array. This was useful for testing, but now that you’re going to save real history, you no longer need to load the data.

➤ In init(), comment out createDevData().

➤ Build and run your app. Start an exercise and tap Done to save the history. Your app performs addDoneExercise(_:) and crashes with Fatal error: Index out of range.

Xcode highlights the offending line in your code:

if today.isSameDay(as: exerciseDays[0].date) {

This line assumes that exerciseDays is never empty. If it’s empty, then trying to access an array element at index zero is out of range. When users start the app for the first time, their history will always be empty. A better way is to use optional checking.

Using Optionals

Skills you’ll learn in this section: optionals; unwrapping; forced unwrapping; filtering the debug console

Swift Dive: Optionals

In the previous chapter, to remove a key from Preview’s UserDefaults, you needed to assign nil to ratings.

@AppStorage("ratings") static var ratings: String?
if exerciseDays.first != nil {
  if today.isSameDay(as: exerciseDays[0].date) {
    ...
  }
}
var myArray: [ExerciseDay?] = []
if let newProperty = optionalProperty {
  // code executes if optionalProperty is non-nil
}
if let firstDate = exerciseDays.first?.date {

Swift Dive: Forced Unwrapping

If you’re really sure your data is non-nil, then you can use an exclamation mark ! on an optional. This is called forced unwrapping, and it allows you to assign an optional type to a non-optional type. When you use a force-unwrapped optional that contains nil, your app will crash. For example:

let optionalDay: ExerciseDay? = exerciseDays.first
let forceUnwrappedDay: ExerciseDay = exerciseDays.first!
let errorDay: ExerciseDay = exerciseDays.first

Multiple Conditionals

When checking whether you should add or insert the exercise into exerciseDays, you also need a second conditional to check whether today is the same day as the first date in the array.

if let firstDate = exerciseDays.first?.date,
  today.isSameDay(as: firstDate) {
print("History: ", exerciseDays)
Completed exercise log
Bevwzenac ugebqiga quk

Debugging HistoryStore

Skills you’ll learn in this section: breakpoints

Error Reproduction
Ocloq Vupvegirheid

An Introduction to Breakpoints

When you place breakpoints in your app, Xcode pauses execution and allows you to examine the state of variables and, then, step through code.

Breakpoint
Kbiorbiacm

Execution paused
Eyaweboam joimog

Icons to control execution
Umimh zu sawspog iqukasuik

po today
po exerciseDays
Printing out contents of variables
Mhuygufv oej duhbumsq oz pakaazpah

print("Initializing HistoryStore")
Initializing HistoryStore
Enefeoxewebk HedmihwFquxa

@StateObject

@State, being so transient, is incompatible with reference objects and, as HistoryStore is a class, @StateObject is the right choice here. @StateObject is a read-only property wrapper. You get one chance to initialize it, and you can’t change the property once you set it.

@StateObject private var historyStore = HistoryStore()
.environmentObject(historyStore)
Successful history store
Noxqulvxaj torsujq jxazi

Swift Error Checking

Skills you’ll learn in this section: throwing and catching errors

enum FileError: Error {
  case loadFailure
  case saveFailure
}
func load() throws {
  throw FileError.loadFailure
}

Swift Dive: do…catch

To handle an error from a throwing method, you use the expression:

 do {
   try methodThatThrows()
} catch {
  // take action on error
}
do {
  try load()
} catch FileError.loadFailure {
  // load failed
} catch {
  // any other error
}
do {
  try load()
} catch {
  print("Error:", error)
}

Alerts

Skills you’ll learn in this section: Alert view

An alert
Ih opadj

@Published var loadingError = false
loadingError = true
.alert(isPresented: $historyStore.loadingError) {
  Alert(
    title: Text("History"),
    message: Text(
      """
      Unfortunately we can't load your past history.
      Email support:
        support@xyz.com
      """))
}
History Alert
Mopkucq Elemb

Saving History

Skills you’ll learn in this section: closures; map(_:); transforming arrays

var dataURL: URL {
  URL.documentsDirectory
    .appendingPathComponent("history.plist")
}
func save() throws {
  var plistData: [[Any]] = []
  for exerciseDay in exerciseDays {
    plistData.append(([
      exerciseDay.id.uuidString,
      exerciseDay.date,
      exerciseDay.exercises
    ]))
  }
}
An array of type [[Any]]
Ik agjor aj bjye [[Iyn]]

Swift Dive: Closures

map(_:) takes a closure as a parameter so, before continuing, you’ll learn how to use closures. You’ve already used them many times, as SwiftUI uses them extensively.

A closure
I vkaqihe

Closure result
Cnijeme zujujh

Closure signature
Fyokaye gepvilale

let aClosure: () -> String = { "Hello world" }
let result: (ExerciseDay) -> [Any] = { exerciseDay in
  [
    exerciseDay.id.uuidString,
    exerciseDay.date,
    exerciseDay.exercises
  ]
}

Using map(_:) to Transform Data

Similar to a for loop, map(_:) goes through each element individually, transforms the data to a new element and then combines them all into a single array.

let plistData: [[Any]] = exerciseDays.map(result)
let plistData = exerciseDays.map { exerciseDay in
  [
    exerciseDay.id.uuidString,
    exerciseDay.date,
    exerciseDay.exercises
  ]
}
func map<T>(
  _ transform: (Self.Element) throws -> T) rethrows -> [T]
Deconstructing map(_:)
Welamhnnoggajp mix(_:)

Type of plistData
Msha ez jfowlLiqe

An Alternative Construct

When you have a simple transformation, and you don’t need to spell out all the parameters in full, you can use $0, $1, $2, $... as replacements for multiple parameter names.

let plistData = exerciseDays.map {
  [$0.id.uuidString, $0.date, $0.exercises]
}
Type of plistData
Kkku ez xhiplTanu

Property List Serialization

Skills you’ll learn in this section: property list serialization

Writing Data to a Property List File

You now have your history data in an array with only simple data types that a property list can recognize. The next stage is to convert this array to a byte buffer that you can write to a file.

do {
  // 1
  let data = try PropertyListSerialization.data(
    fromPropertyList: plistData,
    format: .binary,
    options: .zero)
  // 2
  try data.write(to: dataURL, options: .atomic)
} catch {
  // 3
  throw FileError.saveFailure
}
do {
  try save()
} catch {
  fatalError(error.localizedDescription)
}
Saved history property list file
Xoquw daqvubd dlapoyfy pitl kucu

Reading Data From a Property List File

You’re successfully writing some history, so you can now load it back in each time the app starts.

func load() throws {
  do {
    // 1
    let data = try Data(contentsOf: dataURL)
    // 2
    let plistData = try PropertyListSerialization.propertyList(
      from: data,
      options: [],
      format: nil)
    // 3
    let convertedPlistData = plistData as? [[Any]] ?? []
    // 4
    exerciseDays = convertedPlistData.map {
      ExerciseDay(
        date: $0[1] as? Date ?? Date(),
        exercises: $0[2] as? [String] ?? [])
    }
  } catch {
    throw FileError.loadFailure
  }
}
Saved history
Reziv piqmulk

Ignoring the Loading Error

➤ Delete history.plist in Finder, and build and run your app.

Load error
Xeoy iwpan

let data = try Data(contentsOf: dataURL)
guard let data = try? Data(contentsOf: dataURL) else {
  return
}
Final result
Wiciq cezert

Key Points

  • Optionals are properties that can contain nil. Optionals make your code more secure, as the compiler won’t allow you to assign nil to non-optional properties. You can use guard let to unwrap an optional or exit the current method if the optional contains nil.
  • Don’t force-unwrap optionals by marking them with an !. It is tempting to use an ! when assigning optionals to a new property because you think the property will never contain nil. Instead, try and keep your code safe by assigning a fall-back value with the nil coalescing operator ??. For example: let atLeastOne = oldValue ?? 1.
  • Use breakpoints to halt execution and step through code to confirm that it’s working correctly and that variables contain the values you expect.
  • Use throw to throw errors in methods marked by throws.
  • If you need to handle errors, call methods marked by throws with do { try ... } catch { ... }. catch will only be performed if the try fails. If you don’t need to handle errors, you can call the method with let result = try? method(). result will contain nil if there is an error.
  • Use @StateObject to hold your data store. Your app will only initialize a state object once.
  • Closures are chunks of code that you can pass around just as you would any other object. You can assign them to variables or provide them as parameters to methods. Array has a number of methods requiring closures to transform its elements into a new array.
  • PropertyListSerialization is just one way of saving data to disk. You could also use JSON, or Core Data, which manages objects and their persistence.
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