9.
Saving History Data
Written by Caroline Begbie
@AppStorage
is excellent for storing lightweight data such as settings and other app initialization. You can store other app data in property list files, a database such as SQLite or Realm, or Core Data. Since you’ve learned so much about property list files already, in this chapter, you’ll save the history data to one.
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 for 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
. So you defined ratings
as an optional String
type by adding ?
to the type:
@AppStorage("ratings") static var ratings: String?
ratings
here can either hold a string value or nil
.
You may have learned that Booleans can be either true or false. But an optional Boolean can hold nil
, giving you a third alternative.
Swift Tip:
Optional
is actually an enumeration with two cases:some(Wrapped)
andnone
, wheresome
has a generic value of typeWrapped
andnone
has no value.
Checking for nil
can be useful to prevent errors. At compile time, Xcode prevents Swift properties from containing nil
unless you’ve defined them as optional. At run time, you can check that exerciseDays
is not empty by checking the value of the optional first
:
if exerciseDays.first != nil {
if today.isSameDay(as: exerciseDays[0].date) {
...
}
}
When first
is nil
, the array is empty, but if first
is not nil
, then it’s safe to access index 0 in the array. This is true, because exerciseDays
doesn’t accept nil
values. You can have arrays with nil
values by declaring them like this:
var myArray: [ExerciseDay?] = []
The more common way of checking for nil
is to use:
if let newProperty = optionalProperty {
// code executes if optionalProperty is non-nil
}
This places a non-optional unwrapped result into newProperty
. Unwrapped here means that newProperty
is assigned the contents of optionalProperty
as long as optionalProperty
is not nil
.
➤ Change if today.isSameDay(as: exerciseDays[0].date) {
to:
if let firstDate = exerciseDays.first?.date {
if let
tells the compiler that whatever follows could result in nil
. The property first?
with the added ?
means that first
is an optional and can contain nil
.
If exerciseDays
is empty, then first?
will be nil
and your app won’t perform the conditional block, otherwise firstDate
will contain the unwrapped first element in exerciseDays
.
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
-
optionalDay
is of typeExerciseDay?
and allowsnil
whenexerciseDays
is empty. -
forceUnwrappedDay
is not optional and could cause a runtime error ifexerciseDays
is empty and you force-unwrapfirst
. -
errorDay
causes a compile error because you are trying to put an optional which could containnil
into a property that can’t containnil
.
Unless you’re really certain that the value will never contain nil
, don’t use exclamation marks to force-unwrap it!
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.
➤ Change if let firstDate = exerciseDays.first?.date {
to:
if let firstDate = exerciseDays.first?.date,
today.isSameDay(as: firstDate) {
You can stack up conditionals, separating them with a comma. Your second conditional evaluates the Boolean condition. If firstDate
is not nil
, and today
is the same day as firstDate
, then the code block executes.
➤ At the end of addDoneExercise(_:)
, add:
print("History: ", exerciseDays)
This will print the contents of exerciseDays
to the debug console after adding or inserting history.
➤ Build and run, complete an exercise and tap Done.
Your app doesn’t crash, and your completed exercise prints out in the console.
Filtering the debug console
You may find that your debug console is filled with Apple errors, and it can be quite difficult to see your own print
statements there.
➤ Enter part of the print output that you expect to see, in this case History, into Filter at the bottom right of the console:
Your log will show up on its own:
To hide the left panel in the debug console, you can choose the icon at the bottom right. This allows you to read your debug messages more easily.
If you have a number of print statements that you wish to see, you can prefix them with particular characters, such as >>>.
print(">>>", today)
print(">>> Inserting \(exerciseName)")
You can then enter >>> into Filter and your logs will show up on their own. Remember to clear your filter when you’re through. It can be frustrating when you forget to add >>>, and you filter out your own debugging logs.
Debugging HistoryStore
Skills you’ll learn in this section: breakpoints
Even though the contents of exerciseDays
appears correct at the end of addDoneExercise(_:)
, if you tap History, your history data is blank. This is a real-life frustrating situation where you’re pretty sure you’ve done everything correctly, but the history data refuses to stay put.
Time to put your debugging hat on.
The first and often most difficult debugging step is to find where the bug occurs and be able to reproduce it consistently. Start from the beginning and proceed patiently. Document what should happen and what actually happens.
➤ Build and run, complete an exercise and tap Done. The contents of exerciseDays
print out correctly in the debug console. Tap History and the view is empty, when it should show the contents of exerciseDays
. This error happens every time, so you can be confident at being able to reproduce it.
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.
➤ Still running the app, with the first exercise done, in Xcode tap in the gutter to the left of let today = Date()
in addDoneExercise(_:)
and click. This adds a breakpoint at that line.
➤ Without stopping your app, complete a second exercise and tap Done.
When execution reaches addDoneExercise(_:)
, it finds the breakpoint and pauses. The Debug navigator shows the state of the CPU, memory and current thread operations. The debug console shows a prompt — (lldb)
— allowing you to interactively debug.
Above the debug console, you have icons to control execution:
- Deactivate breakpoints: Turns on and off all your breakpoints.
- Continue program execution: Continues executing your app until it reaches another active breakpoint.
- Step over: If the next line to execute includes a method call, stop again after that method completes.
- Step into/out: If your code calls a method, you can step into the method and continue stepping through it. If you step over a method, it will still be executed, but execution won’t be paused after every instruction.
➤ Click Step over to step over to the next instruction. today
is now instantiated and contains a value.
➤ In the debug console, remove any filters, and at the (lldb)
prompt, enter:
po today
po exerciseDays
po
prints out in the debug console the contents of today
and exerciseDays
:
In this way, you can examine the contents of any variable in the current scope.
Even though exerciseDays
should have data from the previous exercise, it now contains zero elements. Somewhere between tapping Done on two exercises, exerciseDays
is getting reset.
➤ Step over each instruction and examine the variables to make sure they make sense to you. When you’ve finished, drag the breakpoint out of the gutter to remove it.
The next step in your debugging operation is to find the source of truth for exerciseDays
and when that source of truth gets initialized. You don’t have to look very far in this case, as exerciseDays
is owned by HistoryStore
.
➤ At the end of init()
add:
print("Initializing HistoryStore")
➤ Build and run, and reproduce your error by performing an exercise and tapping Done. In the debug console, filter on History.
Your console should look like this:
Now you can see why exerciseDays
is empty after performing an exercise. Something is reinitializing HistoryStore
!
➤ Open ContentView.swift. This is where you initialize HistoryStore
in an environment object modifier on TabView
.
You may remember from the end of the previous chapter that @SceneStorage
reinitializes TabView
when it stores selectedTab
. The redraw re-executes environmentObject(HistoryStore())
and incorrectly initializes HistoryStore
with all its data.
You’ve now successfully debugged why your history data is empty. All you have to do now is decide what to do about it.
This first step to fix this is to move the initialization of HistoryStore
up a level in the view hierarchy. Later in the chapter, you’ll set up HistoryStore
so that you’re sure that the store will initialize only once.
➤ Cut environmentObject(HistoryStore())
from ContentView
’s body
.
➤ Open HIITFitApp.swift and paste the modifier to ContentView
:
WindowGroup {
ContentView()
.environmentObject(HistoryStore())
...
}
➤ Build and run, perform all four exercises, tapping Done after each, and check your history:
Congratulations! You fixed your first bug! You can now remove all your print statements from HistoryStore
with pride and a sense of achievement.
Now you can continue on and save your history so that it doesn’t reset every time you restart your app.
Swift error checking
Skills you’ll learn in this section: throwing and catching errors
Saving and loading data is serious business, and if any errors occur you’ll need to know about them. There isn’t a lot you can do about file system errors, but you can let your users know that there has been an error, and they need to take some action.
➤ Open HistoryStore.swift and add a new enumeration to HistoryStore
:
enum FileError: Error {
case loadFailure
case saveFailure
case urlFailure
}
This is a list of possible errors that you’ll check for.
To create a method that raises an error, you mark it with throws
and add a throw
statement.
➤ Add this new method to HistoryStore
:
func load() throws {
throw FileError.loadFailure
}
Here, you’ll read the history data from a file on disk. Currently, this method will always raise an error, but you’ll come back to it later when you add the loading code. When you throw an error, the method returns immediately and doesn’t execute any following code. It’s the caller that should handle the error, not the throwing method.
try…catch
When calling a method that throws, you use try
. If you don’t need to handle any errors specifically, you can call the method with try?
as, for example, try? load()
. This will convert an error result to nil
and execution continues. To handle an error from a throwing method, you use the expression do { try ... } catch { }
.
➤ Add this to the end of init()
:
do {
try load()
} catch {
print("Error:", error)
}
You call the throwing method and, if there’s an error, the catch
block executes.
➤ Build and run and, in the debug console, you’ll see your printed error: Error: loadFailure. (Remember to clear your debug console filter if you have one.)
Throwing initializers
You can also throw errors when initializing an object. If your loading of the history data fails, you could either report a catastrophic error and crash the app or, preferably, you could report an error but continue with no history and an empty exerciseDays
.
➤ Change the signature of init()
to:
init(withChecking: Bool) throws {
You’ll try to create a HistoryStore
using this initializer, but fall back to the default initializer if necessary.
➤ Create a new default initializer:
init() {}
This is your fall-back initializer, which won’t call any loading code.
➤ In init(withChecking:)
, change print("Error:", error)
to:
throw error
This will pass back the error to the object that initializes HistoryStore
.
➤ Open HIITFitApp.swift. This is where you initialize HistoryStore
and place it into the environment.
➤ Add a new property to HIITFitApp
:
@StateObject private var historyStore: HistoryStore
So far you’ve used @State
for mutable values. You should only use @State
properties for temporary items, as they will disappear when the view is deleted. @StateObject
will create an observable object which won’t disappear when the view does.
@State
, being so transient, is incompatible with reference objects and, as HistoryStore
is a class, @StateObject
is the right choice here.
Note: In case you’re confused about all the property wrappers you’ve used so far, you will review them in Chapter 11, “Understanding Property Wrappers”.
Earlier in the chapter, you moved the initialization of HistoryStore
from ContentView
to HIITFitApp
, but noted that the store must initialize only once. @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.
As you want to instantiate historyStore
using a throwing method, you’ll use an initializer.
➤ In HIITFitApp
, create a new initializer:
init() {
let historyStore: HistoryStore
do {
historyStore = try HistoryStore(withChecking: true)
} catch {
print("Could not load history data")
historyStore = HistoryStore()
}
}
When ContentView
first initializes, you try loading the history. If there is no error, then historyStore
will contain the loaded history data. If the try
fails, then you print out an error message and use HistoryStore
’s default initializer. HistoryStore.init()
can’t possibly fail, but will load with empty history data.
You still have to assign the local historyStore
to the state object.
➤ Add this to the end of init()
:
_historyStore = StateObject(wrappedValue: historyStore)
As the name suggests, a property wrapper wraps an underlying value or object. You use the StateObject(wrappedValue:)
initializer to set the wrapped value of the state object and use an underscore prefix to assign the initialized state object to historyStore
.
➤ Change .environmentObject(HistoryStore())
to:
.environmentObject(historyStore)
Here you use the state object instead of creating HistoryStore
, when setting up the environment object.
➤ Build and run and, because load()
still throws an error, you’ll see your error in the debug console: Could not load history data.
Alerts
Skills you’ll learn in this section:
Alert
view
When you release your app, your users won’t be able to see print
statements, so you’ll have to provide them with more visible communication. When you want to give the user a choice of actions, you can use an ActionSheet
but, for simple notifications, an Alert
is perfect. An Alert
pops up with a title and a message and pauses app execution until the user taps OK.
➤ Open HIITFitApp.swift and add a new property to HIITFitApp
:
@State private var showAlert = false
This toggle will control whether you show the alert.
➤ In the catch
block in init()
, update showAlert
:
showAlert = true
➤ Add a new modifier to ContentView
:
.alert(isPresented: $showAlert) {
Alert(
title: Text("History"),
message: Text(
"""
Unfortunately we can’t load your past history.
Email support:
support@xyz.com
"""))
}
When showAlert
is true, you show an Alert
view with the supplied Text
title and message. Surround the string with three """
to format your string on multiple lines.
➤ Build and run. Because HistoryStore
’s initializer fails, you set showAlert
to true, which causes your Alert
to show.
➤ Tap OK. Alert
resets showAlert
and your app continues with empty history data.
Now that your testing of error checking is complete, open HistoryStore.swift and remove throw FileError.loadFailure
from load()
.
Note: You can find out more about error handling in our Swift Apprentice book, which has an entire chapter on the subject. You can find Swift Apprentice at: https://bit.ly/2MuhHu0.
Saving history
Skills you’ll learn in this section:
FileManager
You’ll first save your history data to disk and then, come back to filling out load()
using the saved data.
➤ Add a new method to HistoryStore
to create the URL where you will save the data:
func getURL() -> URL? {
// 1
guard let documentsURL = FileManager.default.urls(
for: .documentDirectory, in: .userDomainMask).first else {
// 2
return nil
}
// 3
return documentsURL.appendingPathComponent("history.plist")
}
This method returns an optional URL. The calling method can then decide what to do if the result of this method is nil
.
Going through the code:
- Using
guard
, you can jump out of a method if a condition is not met.guard let
is similar toif let
in that you assign an optional to a non-optional variable and check it isn’tnil
. Here you check thatFileManager.default.urls(for:in:).first
is notnil
and, if it isn’tnil
, assign it todocumentsURL
. - You always provide an
else
branch withguard
where you specify how to leave the method when theguard
conditional test fails. Generally you return from the method, but you could also usefatalError(_:file:line:)
to crash the app. - You add the file name to the documents path. This gives you the full URL of the file to which you’ll write the history data.
➤ Add a new throwing method to HistoryStore
:
func save() throws {
guard let dataURL = getURL() else {
throw FileError.urlFailure
}
}
You set up your URL. If getURL()
returns nil
, you throw an error and save()
stops execution.
You’ll save the history data to a property list (plist) file. As mentioned in the previous chapter, the root of a property list file can be a dictionary or an array. Dictionaries are useful when you have a number of discrete values that you can reference by key. But in the case of history, you have an array of ExerciseDay
to store, so your root will be an array.
Property list files can only store a few standard types, and ExerciseDay
, being a custom type, is not one of them. In Chapter 19, “Saving Files”, you’ll learn about Codable
and how to save custom types to files but, for now, the easy way is to separate out each ExerciseDay
element into an array of Any
and append this to the array that you will save to disk.
➤ Add this to save()
after the previous code:
var plistData: [[Any]] = []
for exerciseDay in exerciseDays {
plistData.append(([
exerciseDay.id.uuidString,
exerciseDay.date,
exerciseDay.exercises
]))
}
For each element in the loop, you construct an array with a String
, a Date
and a [String]
. You can’t store multiple types in an Array
, so you create an array of type [Any]
and append this element to plistData
.
plistData
is a type [[Any]]
. This is a two dimensional array, which is an array that contains an array. After saving two elements, plistData
will look like this:
The for
loop maps exerciseDays
to plistData
. In other words, the loop transforms one set of data to another set of data. As this happens so often in code, Swift provides map(_:)
, an optimized method on Array
, for this transforming of data.
Closures
Skills you’ll learn in this section: closures;
map(_:)
; transforming arrays
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 is simply a block of code between two curly braces. Closures can look complicated, but if you recognize how to put a closure together, you’ll find that you use them often, just as SwiftUI does. Notice a closure’s similarity to a function: Functions are closures — blocks of code — with names.
The closure is the part between the two curly braces {...}
. In the example above, you assign the closure to a variable addition
.
The signature of addition
is (Int, Int) -> Int
and declares that you will pass in two integers and return one integer.
It’s important to recognize that when you assign a closure to a variable, the closure code doesn’t execute. The variable addition
contains the code return a + b
, not the actual result.
To perform the closure code, you execute it with its parameters:
You pass in 1
and 2
as the two integer parameters and receive back an integer:
Another example:
let aClosure: () -> String = { "Hello world" }
This closure takes in no parameters and returns a string.
Your current task is to convert each ExerciseDay
element to an element of type [Any]
.
This is the closure that would perform this conversion for a single ExerciseDay
element:
let result: (ExerciseDay) -> [Any] = { exerciseDay in
[
exerciseDay.id.uuidString,
exerciseDay.date,
exerciseDay.exercises
]
}
result
is of type (ExerciseDay) -> [Any]
. The closure takes in a parameter exerciseDay
and combines the ExerciseDay
properties into an array of type [Any]
.
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.
You could send result
to map
which returns an array of the results:
let plistData: [[Any]] = exerciseDays.map(result)
map(_:)
takes the closure result
, executes it for every element in exerciseDays
and returns an array of the results.
Rather than separating out into a closure variable, it’s more common to declare the map
operation together with the closure.
➤ Replace the previous code from var plistData: [[Any]] = []
to the end of save()
with:
let plistData = exerciseDays.map { exerciseDay in
[
exerciseDay.id.uuidString,
exerciseDay.date,
exerciseDay.exercises
]
}
The full declaration of Array.map(_:)
is:
func map<T>(
_ transform: (Self.Element) throws -> T) rethrows -> [T]
- If
map(_:)
finds any errors, it will throw. -
T
is a generic type. You’ll discover more about generics in Section 2, but hereT
is equivalent to[Any]
. -
transform
’s signature is(Self.element) -> T
. You’ll recognize this as the signature of a closure to which you pass a single element ofExerciseDay
and return an array of type[Any]
.
This is how your code matches map(_:)
:
This code gives exactly the same result as the previous for
loop. Option click plistData
, and you’ll see that its type is [[Any]]
, just as before.
One advantage of using map(_:)
rather than dynamically appending to an array in a for
loop, is that you declare plistData
as a constant with let
. This is some extra safety, so that you know that you won’t accidentally change plistData
further down the line.
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.
➤ Replace the previous code with:
let plistData = exerciseDays.map {
[$0.id.uuidString, $0.date, $0.exercises]
}
Here you have one input parameter, which you can replace with $0
. When using $0
, you don’t specify the parameter name after the first curly brace {
.
Again, this code gives exactly the same result. Option click plistData
, and you’ll see that its type is still [[Any]]
.
Swift Dive: filter(_:)
and reduce(_:_:)
There are other common methods that convert one array to another.
With filter(_:)
you can filter one array to another array, as for example:
let oneToThree = [4, 2, 0, 9, 3, 1].filter {
Range(1...3).contains($0) // same as $0 >= 1 && $0 <= 3
}
The closure takes each element of the array and returns a value of true
if the integer is between one and three. When the closure result is true
, the element is added to the new array. After completing this code, oneToThree
contains [2, 3, 1]
.
reduce(_:)
combines all the elements in an array into one value. For example:
let result = [4, 2, 0].reduce(0) { runningTotal, value in
runningTotal + value
}
You call reduce(_:_:)
with a starting value. Although you can substitute $0
and $1
for the parameters here, the code reads better with explicitly named parameters. The first parameter is the running total, and you add the second parameter to the first, resulting in a single value. After this code result
will contain 6.
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.
➤ Add this code to the end of save()
:
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
}
Going through the code:
- You convert your history data to a serialized property list format. The result is a
Data
type, which is a buffer of bytes. - You write to disk using the URL you formatted earlier.
- The conversion and writing may throw an error, which you catch by throwing an error.
➤ Call save()
from the end of addDoneExercise(_:)
:
do {
try save()
} catch {
fatalError(error.localizedDescription)
}
If there’s an error in saving, you crash the app, printing out the string description of your error. This isn’t a great way to ship your app, and you may want to change it later.
➤ Build and run and do an exercise. Tap Done and your history file will save.
➤ In Finder, go to your app’s Documents directory, and you’ll see history.plist. Double click the file to open this file in Xcode.
See how the property list file matches with your data:
-
Root: The property list array you saved in
plistData
. This is an array of type[[Any]]
. -
Item 0: The first element in
exerciseDays
. This is an array of type[Any]
. -
Item 0: The id converted to
String
format. - Item 1: The date of the exercise
- Item 2: The array of exercises that you have performed and tapped Done to save. In this example, the user has exercised on one day with two exercises: Sun Salute and Burpee.
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.
➤ In HistoryStore.swift, add this code to load()
:
// 1
guard let dataURL = getURL() else {
throw FileError.urlFailure
}
do {
// 2
let data = try Data(contentsOf: dataURL)
// 3
let plistData = try PropertyListSerialization.propertyList(
from: data,
options: [],
format: nil)
// 4
let convertedPlistData = plistData as? [[Any]] ?? []
// 5
exerciseDays = convertedPlistData.map {
ExerciseDay(
date: $0[1] as? Date ?? Date(),
exercises: $0[2] as? [String] ?? [])
}
} catch {
throw FileError.loadFailure
}
Loading is very similar to saving, but with some type checking to ensure that your data conforms to the types you are expecting. Going through the code:
-
First set up the URL just as you did with saving the file.
-
Read the data file into a byte buffer. This buffer is in the property list format. If history.plist doesn’t exist on disk,
Data(contentsOf:)
will throw an error. Throwing an error is not correct in this case, as there will be no history when your user first launches your app. You’ll fix this error as your challenge for this chapter. -
Convert the property list format into a format that your app can read.
-
When you serialize from a property list, the result is always of type
Any
. To cast to another type, you use the type cast operatoras?
. This will returnnil
if the type cast fails. Because you wrote history.plist yourself, you can be pretty sure about the contents, and you can castplistData
from typeAny
to the[[Any]]
type that you serialized out to file. If for some reason history.plist isn’t of type[[Any]]
, you provide a fall-back of an empty array using the nil coalescing operator??
. -
With
convertedPlistData
cast to the expected type of[[Any]]
, you usemap(_:)
to convert each element of[Any]
back toExerciseDay
. You also ensure that the data is of the expected type and provide fall-backs if necessary.
➤ Build and run, and tap History. The history you saved out to your property list file will load in the modal.
Challenge
Challenge: Ignore the Error
➤ Delete history.plist in Finder, and build and run your app. Your loading error appears because load()
fails.
You’re not first checking to see whether history.plist exists. If it doesn’t, Data(contentsOf:)
throws an error.
Your challenge is to ignore the error, as it’s most likely that in this case the error is that the file doesn’t exist. Remember that you can use try?
to discard an error. When you’ve completed your mission, your app should load data from history.plist if it exists and take no action if it doesn’t.
You can find the answer to this challenge in load()
in the challenges directory for this chapter.
Key points
- Optionals are properties that can contain
nil
. Optionals make your code more secure, as the compiler won’t allow you to assignnil
to non-optional properties. You can useguard let
to unwrap an optional or exit the current method if the optional containsnil
. - 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 containnil
. 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 bythrows
. - If you need to handle errors, call methods marked by
throws
withdo { try ... } catch { ... }
.catch
will only be performed if thetry
fails. If you don’t need to handle errors, you can call the method withlet result = try? method()
.result
will containnil
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. A common paradigm is to pass a closure as a completion handler to be executed when an operation completes.
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 useJSON
, or Core Data, which manages objects and their persistence.