14.
Saving and Loading
Written by Joey deVilla
Checklist is now a fully CRUD app: The user can create, report on, update and delete checklist items. It would be a fully-functional basic checklist app if not for two issues. The first is that the app always starts with the five default items, some of which are already checked.
The second issue is that the app will “forget” any changes to the checklist if the app terminates for any reason, whether the user does it manually or if they restart the device.
It’s time to make some changes so that the app behaves in the following ways:
- When the user launches Checklist for the first time, they’ll start with an empty checklist instead of the five default checklist items that the app’s had since the beginning.
- The app will remember the state of its checklist after it terminates. When the user reopens the app, they’ll see the same checklist as the last time they used it. The contents of the checklist should persist over time.
In this chapter, you’ll cover the following topics:
- Data persistence: In most cases, apps need to remember something from the last time you used them.
- The Documents folder: Each app has its own place where it can store data. You’ll learn how to find this place and use it to store checklist items.
- 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.
Data persistence
Modern smartphone operating systems are technological wonders. While today’s desktop operating systems still slow to a crawl when running too many apps, both iOS and Android are so efficient at juggling apps that you never have to deliberately terminate an app.
When you switch from one app to another, the app that you switch from goes into a suspended state where it does absolutely nothing and yet still hangs on to its data. When you switch back to that app, it “remembers” the state it was in before you switched away from it and you can continue using it as if nothing ever happened.
However, it’s not a perfect world, and sometimes an app will terminate. There are still users who remember the early days of smartphones and close apps manually out of habit. Apps and operating systems can also crash, requiring a restart. And sometimes a device will run out of power before you can recharge it.
Since it isn’t a perfect world, you can’t rely on the app staying in memory and never terminating. Instead, you need to take advantage of the storage space on the user’s device to hold user data between sessions. It’s not just for cat pictures and videos!
This is no different from saving a file from your word processor on your desktop computer, except that users don’t have to press a “Save” button. Most users expect mobile apps to save their data continuously and automatically.
Saving data between app launches is called data persistence. You’ll add this feature to Checklist in this chapter.
If you were working on a traditional desktop app, implementing data persistence might take a fair bit of code. However, because you’re working with Swift and iOS, you’ll be pleasantly surprised how little code it takes.
It’s time to start working on that data persistence functionality! The first step is to figure out where you’ll store the data.
The Documents directory
Unlike desktop apps, which mostly have unfettered access to the computer’s hard drive, each iOS app goes into a sandbox when installed. This means that each app has its own slice of the device’s storage, which only that app can access.
This is a security measure designed to prevent malicious software from doing any serious damage. If an app can change only its own files, it can’t modify or mess with any other part of the system.
Think of the sandbox as your app’s very own hard drive. Within that hard drive is the Documents directory, which is the designated place for your app to store data.
Note: If you’re unfamiliar with the term “directory”, it’s just the more computer science-y term for “folder.”
The Documents folder has a couple of benefits:
- Automatic backup: When the user syncs their device with their computer or iCloud, the system also backs up the Documents directories for their apps.
- Persistence between updates: When you release a new version of your app, the update doesn’t touch the app’s Documents directory. Any saved data in this directory persists between updates.
With its security features and benefits, the Documents folder is the perfect place to store your app’s data files.
Now that you know about the Documents folder, it’s time to find it so that you can put it to use.
Finding the Documents directory to save checklist data
➤ Add the following methods to Checklist.swift after the moveListItem(whichElement:destination:) method:
func documentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)
return paths[0]
}
func dataFilePath() -> URL {
return documentsDirectory().appendingPathComponent("Checklist.plist")
}
The first method, documentsDirectory(), returns the location of the app’s Documents directory. It does this by using the built-in FileManager.default object, which is the preferred way to access the file system in an app’s sandbox.
FileManager.default has a method called urls(for:in:), which lets you specify a kind of directory to look for — in this case, the Documents directory — and returns an array containing one or more URLs where you may find them. Even though each app has just one Documents directory, urls(for:in:) returns an array of results. The path for the Documents directory is in that array’s first and only element, which is what documentsDirectory() returns.
The second method, dataFilePath(), uses the result of documentsDirectory() to construct the full path to the file that will store the checklist items. This file is named Checklist.plist and it will live inside the Documents folder.
Notice that both methods return a URL object. You may think of a URL as a “web address”, but it’s really just a path for a given directory or file, which can be either online or on the local system. iOS uses URLs to refer to files in its file system. When a URL begins with http:// or https://, it refers to a directory or file on the web. When it refers to a local file, a URL will begin with file://.
Note: Double check to make sure your code says
.documentDirectoryand not.documentationDirectory. Xcode’s autocomplete can easily trip you up here!
Now that you have these methods, go ahead and put them to use.
➤ Still in Checklist.swift, add the following method to the start of the Methods section, immediately after the “Methods” comment:
init() {
print("Documents directory is: \(documentsDirectory())")
print("Data file path is: \(dataFilePath())")
}
init() is a method that you haven’t seen before. Its name comes from the term “initializer.” It’s a special method built into structs and classes that are automatically called when a new instance is created.
You use Initializers to set up or initialize an object at the moment when it’s created, and you can also use them to perform tasks at that moment.
Right now, you’re using the init() method to print the paths of the Documents directory and the file where you’ll save the checklist to Xcode’s debug console. Later on, you’ll use it to restore the saved checklist when the app starts up.
➤ Run the app and look at Xcode’s debug console. It will display the file paths of the Documents directory and where the app will eventually save Checklist.plist.
The full names of the file paths will differ from device to device. When I run the app on the Simulator on my computer. the Xcode debug console displays this:
If you run the app on your iPhone, your path will look slightly different. Here’s what mine says:
Documents directory is: file:///var/mobile/Containers/Data/Application/5F4CB154-1CAD-4F54-8673-4ADCCEF98D78/Documents/
Data file path is: file:///var/mobile/Containers/Data/Application/5F4CB154-1CAD-4F54-8673-4ADCCEF98D78/Documents/Checklist.plist
As you’ll notice, the sandbox’s directory name is a set of random characters that are determined when you install the app. Anything inside that directory, such as the Documents directory, is part of the app’s sandbox.
Browsing the Documents directory
For the rest of this chapter, run the app on the simulator instead of a device. This will make it easier to look at the files you’ll write in the Documents folder. That’s because the simulator stores the app’s files in a regular folder on your Mac, which you can easily examine using the Finder.
➤ Run the app in the simulator. When the path for the Documents directory appears in Xcode’s debug console, copy it. Don’t include the file:// bit — the path starts with /Users/yourname/….
➤ 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:
➤ Paste the full path of the Documents folder into the text field in the dialog box and click the Go button:
The Finder window shows you the contents of that folder.
Keep this window open; you’ll want to be able to verify that Checklist.plist, the file containing the checklist data, is actually created when the time comes.
Note: If you want to navigate to the simulator’s app directory by traversing your folder structure, you should know that the Library folder, which is in your home folder, is normally hidden. If you can’t see the Library folder, hold down the Alt/Option key and click on Finder’s Go menu (or hold down the Alt/Option key while the Go menu is open). This should reveal a shortcut to the Library folder on the Go menu, if it wasn’t visible before.
You can see several folders inside the app’s sandbox folder:
- The Documents folder, where the app will put its data files. It’s currently empty.
- The Library folder has cache files and preferences files. The operating system manages the contents of this folder.
- The SystemData folder is for the operating system to use to store any system-level information relevant to the app.
- The tmp folder is for temporary files. Sometimes, apps need to create files for temporary use. You don’t want these to clutter up your Documents folder, so tmp is a good place to put them. iOS will clear out this folder from time to time.
You can also get an overview of the Documents folder of other apps on your device.
➤ On your device, open Settings and then select General ▸ iPhone Storage. Scroll down to the list of installed apps and tap the name of an app.
You’ll see the size of its Documents folder, but not the actual content:
Now that you have a good understanding of where you’ll save your app’s information, it’s time to move on to implementing your save functionality.
Saving checklist items
For your next step, you’ll write the code that will save the list of to-do items. These items will save to a file named Checklist.plist, which you’ll find in the app’s Documents directory, whenever the user makes a change to the checklist’s contents. Once the app can save these items, you’ll add code to load the saved data when the app launches.
.plist files
You probably looked at the name of the checklist data file, Checklist.plist and wondered, “What’s a .plist file?”
You’ve already seen a file named Info.plist back when you were working on Bullseye. All apps, Checklist included, have such a file, which you can see if you look at its files in Xcode’s Project navigator. Info.plist contains information about the app for iOS to use, such as what name to display under the app’s icon on the home screen.
“.plist” is short for Property List. It’s an XML file format that stores app settings and their corresponding values. In iOS, .plist files are often used for storing app data, as they’re simple to read for both apps and their human programmers.
The Codable protocol, encoding, and decoding
In the past few chapters, you’ve had so much new information thrown at you that you might have forgotten what a protocol is — at least in the Swift sense. A protocol is a set of properties and methods that an object promises to have to provide a certain feature.
For the app to save its checklist items, you’ll use the Codable protocol, which gives objects the ability to save their data to and load their data from the file system.
The beauty of Codable is that it insulates you from having to know much about the format of the files it writes. In this case, Codable will save the checklist item data in a .plist file. You won’t have to work with the file directly. All you care about is that the data is stored as a file in the app’s Documents folder, and Codable will do most of the work.
The name Codable captures the two kinds of tasks it will perform, namely:
-
Encoding, which is converting an object’s data from its form in system RAM into a form that you can write to “disk”… or, in this case, the device’s flash drive. Think of encoding as saving a file in a word processor.
-
Decoding, which is reading data stored on “disk” and converting it back into a form that an app’s object can use. Think of decoding like loading a file in a word processor.
The process of converting objects to files and back again is known as serialization. It’s a big topic in software engineering.
Programmers use all sorts of metaphors for serialization, and many of them revolve around food preservation techniques. Some programmers think of serialization like taking a living object and freezing it, preserving it and suspending it in time. You store that frozen object in a file on the device’s flash drive, where it will spend some time in cryostasis. Later, you can read that file into memory and defrost the object, bringing it back to life again.
Saving data to a file
Now that you have a method that determines 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 saveListItems() {
// 1
let encoder = PropertyListEncoder()
// 2
do {
// 3
let data = try encoder.encode(items)
// 4
try data.write(to: dataFilePath(),
options: Data.WritingOptions.atomic)
// 5
} catch {
// 6
print("Error encoding item array: \(error.localizedDescription)")
}
}
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:
-
First, the method creates an instance of
PropertyListEncoder, a type of object that Apple operating systems use to encode the data stored in an app’s objects into a property list. -
The
dokeyword, which you haven’t encountered before, sets up the first of two blocks of code, which are Swift’s way of catching errors that might come up when the program is running.
The do block contains code that might fail or result in a error — or, as you say in programming, throw an error. Under normal circumstances, this would cause the app to come to a crashing halt. The do block changes all that: 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.
- Here, you call the encoder’s
encode()method to encode theitemsarray. 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.
-
If the call to
encode()succeeds,datanow contains the contents of theitemsarray in encoded form. This line attempts to write this encoded data to a file using the file path returned by a call todataFilePath(). Thewrite()method, like many file operations, can fail for many reasons and throw an error. Once again, you have to make use of atrystatement, so thecatchblock can handle the case wherewrite()fails. -
This is the start of the
catchblock, which contains the code to execute if any line of code in thedoblock threw an error. -
This is the code that executes if code in the
doblock 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 references an error variable. Where did that come from?
When you create a pair of do — catch code blocks, you can explicitly check for specific types of errors. This chapter won’t get into that. All you need to know is that if you have a catch block, Swift will automatically create a local variable named error. It will contain 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 indicates the error’s source.
You’ll notice that Xcode is showing one of its cryptic error messages: “Referencing instance method ‘encode’ on ‘Array’ requires that ‘ChecklistItem’ conform to ‘Encodable’”. This is because any object encoded or decoded by a PropertyListEncoder — or for that matter, any of the other encoders and decoders compatible with the Codable protocol — must support the Codable protocol.
A closer look at the Codable protocol
Swift arrays — as well as most other standard Swift objects and data types — already conform to the Codable protocol. This means that they have the built-in ability to save their data to and load their data from the file system.
The items property of Checklist is an array, so it conforms to Codable. However, the objects contained within the items array must also support Codable in order for the array to be serialized. The question becomes: Is your ChecklistItem class Codable compliant? Apparently not…
Note: Sometimes when working with code dealing with
Codablesupport, you’ll see error messages or references toEncodableorDecodableprotocols. So, it might be good to know thatCodableis actually a protocol which combines these two other protocols,EncodableandDecodable— one for each side of the serialization process.
➤ Switch to ChecklistItem.swift and modify the struct line as follows:
struct ChecklistItem: Identifiable, Codable {
In the above code, you’re telling Swift that ChecklistItem is not just a kind of Identifiable, but also a kind of Codable. This tells the compile that ChecklistItem conforms to the Codable protocol. That’s all you need to do!
“Now, hold on,” you might say. “In Bullseye, I had to write additional code to support the ViewModifier protocol. How come I don’t have to do that here?”
In case you’ve forgotten, you used the ViewModifier protocol to style different parts of Bullseye’s user interface in Chapter 7, “The New Look.” To make use of it, you had to add a body property to objects that adopted it.
That’s because protocols can have default implementations, which means that objects that adopt them don’t need any additional code. It’s often useful for a protocol to have a default implementation that provides functionality that makes things easier or covers a lot of standard scenarios.
In this case, all of the properties of ChecklistItem — id, name and isChecked — are standard Swift types that conform to the Codable protocol. As a result, Swift already knows how to encode and decode them. So, you can simply piggyback on existing functionality without having to write any code of your own to implement encoding or decoding in ChecklistItem. Handy, eh?
Putting saveListItems() to use
Now that you have the saveListItems() method, you need to be able to call it from the places in the code where the user can modify the list of items.
Challenge: Before continuing, ask yourself: Where in the source code would you call
saveListItems()?
You should call saveListItems() when any of the following happens:
- The user adds a new item to the checklist.
- The user changes an existing item in the checklist, either by changing its name or its checked status.
- The user deletes an item from the checklist.
- The user moves an item to a different location within the checklist.
The code for deleting and moving checklist items is in Checklist, so you’ll add calls to saveListItems() to handle those cases first.
➤ Open Checklist.swift and add calls to saveListItems() to the end of deleteListItem(whichElement:) and moveListItem(whichElement:). They should end up looking like this:
func deleteListItem(whichElement: IndexSet) {
items.remove(atOffsets: whichElement)
printChecklistContents()
saveListItems()
}
func moveListItem(whichElement: IndexSet, destination: Int) {
items.move(fromOffsets: whichElement, toOffset: destination)
printChecklistContents()
saveListItems()
}
The code for creating a new checklist item is in NewChecklistItemView, so another call to saveListItems() should go there.
➤ Switch to NewChecklistItemView.swift. In body, add a call to saveListItems() in the Button’s action: parameter so that the lines defining the button look like this:
Button(action: {
var newChecklistItem = ChecklistItem(name: self.newItemName)
self.checklist.items.append(newChecklistItem)
self.checklist.printChecklistContents()
self.checklist.saveListItems()
self.presentationMode.wrappedValue.dismiss()
}) {
There’s one more situation where you need to call saveListItems(): When the user makes a change to a checklist item. This happens in EditChecklistItemView.
➤ Open EditChecklistItemView.swift and look at the code that defines the view:
struct EditChecklistItemView: View {
@Binding var checklistItem: ChecklistItem
var body: some View {
Form {
TextField("Name", text: $checklistItem.name)
Toggle("Completed", isOn: $checklistItem.isChecked)
}
}
}
There’s a problem here: Unlike NewChecklistItemView, EditChecklistItemView doesn’t have a property that contains a reference to the checklist. Without access to the checklist object, there’s no way to call its saveListItems().
This is a good time to step back and take a look at what happens the user edits a checklist item:
- The user taps on a checklist item.
- The app responds by displaying the “Edit checklist item” screen.
- The user has the option of changing the item’s name, checked status or both. The user can also opt to not change anything.
- The user returns to the checklist screen by pressing the Checklist button located in the upper-left corner of the “Edit checklist item” screen.
- The app responds by displaying the checklist screen.
That last event in the list — “The app responds by displaying the checklist screen” — always happens after the user closes the “Edit checklist item” screen, whether they have made any changes to the item or not. The checklist screen has access to the checklist object, which means that the call to saveListItems() should be made when the “Edit checklist item” screen closes and the checklist screen appears.
Take a look at the checklist screen’s code.
➤ Switch to ChecklistView.swift. Look near the end of ChecklistView’s body and you’ll see onAppear():
.onAppear() {
self.checklist.printChecklistContents()
}
Add a call to saveListItems() to onAppear(), which causes the app to save the checklist any time the checklist displays:
.onAppear() {
self.checklist.printChecklistContents()
self.checklist.saveListItems()
}
This takes care of all the cases where the checklist or one of its items changes. Next, you’ll confirm that our calls to saveListItems() works.
Verifying the saved file
➤ Run the app now and do something that results in a save. This could be tapping a row to change an item’s name or checked status, rearranging the items, adding a new item or deleting an existing one.
Remember to run the app in the simulator. You’ll need access to the simulator’s file system, which is easy to view on your Mac.
In my case, I made the following changes to the checklist items:
- 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.”
➤ Go to the Finder window that has the app’s Documents directory open:
There’s now a Checklist.plist file in the Documents folder, which contains the items from the list.
You can look inside this file if you want. What you see depends on which app you use to open it. If you use a general-purpose text editor, the file’s contents won’t make much sense. Here’s what it looks like in Visual Studio Code on my computer:
Even though it’s XML, the .plist file is stored in a binary format, which is why it looks so garbled in Visual Studio Code.
Some text editors, especially those designed specifically for macOS, support this file format and can read it as if it were text. TextWrangler is a good option, and it’s a free download from the Mac App Store. Here’s what Checklist.plist looks like when you view it in one of these editors:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>id</key>
<string>FA9D1B56-7BB9-4233-83AC-8761C9F7A19E</string>
<key>isChecked</key>
<false/>
<key>name</key>
<string>Walk the cat</string>
</dict>
<dict>
<key>id</key>
<string>3C397BFB-575E-4B84-912A-379F5CB1DD98</string>
<key>isChecked</key>
<true/>
<key>name</key>
<string>Learn iOS development</string>
</dict>
<dict>
<key>id</key>
<string>5279F8E8-F25D-46FB-85DD-B5DD7FF0B69F</string>
<key>isChecked</key>
<true/>
<key>name</key>
<string>Eat ice cream</string>
</dict>
<dict>
<key>id</key>
<string>23395DAA-F08C-468B-AED2-771534ADB308</string>
<key>isChecked</key>
<true/>
<key>name</key>
<string>Soccer practice</string>
</dict>
</array>
</plist>
Checklist.plist is considerably readable in this form. You can even see how it corresponds to the items in the app’s checklist.
Naturally, you can also open the plist file with Xcode, which displays the contents of plist files in an even more user-friendly way.
➤ Right-click the Checklist.plist file and choose Open With ▸ Xcode.
You’ll see the following window appear:
To see the contents of the checklist items, expand each item by clicking on its disclosure triangle. You’ll see each item’s ID value, name and checked status:
You’ll also see that the contents of the plist file correspond to the current state of the checklist in the app. This confirms that the checklist data is saving properly.
It’s now time to take care of loading the data.
Loading the file
Saving is all well and good, but it’s only half of what the app needs. It also needs to load the data from the Checklist.plist file.
Fortunately, loading saved data is very straightforward. You’re going to do the same thing you just did for encoding the items array — but in reverse.
Reading data from a file
➤ Open Checklist.swift and add the following new method, just after saveListItems():
func loadListItems() {
// 1
let path = dataFilePath()
// 2
if let data = try? Data(contentsOf: path) {
// 3
let decoder = PropertyListDecoder()
do {
// 4
items = try decoder.decode([ChecklistItem].self,
from: data)
// 5
} catch {
print("Error decoding item array: \(error.localizedDescription)")
}
}
}
As you did with saveListItems(), go through the commented lines in loadListItems() step-by-step:
-
First, you store the results of
dataFilePath()— the path to the Checklist.plist file — in a temporary constant namedpath. -
The method tries to load the contents of Checklist.plist into a new
Dataobject. Thetry?command attempts to create theDataobject, but returnsnil— Swift’s way of saying “no result” — if it fails. That’s why you put it in anif letstatement.
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.
-
When the app does find a Checklist.plist file, the method creates an instance of
PropertyListDecoder. -
The method loads the saved data back into
itemsusing the decoder’sdecodemethod. The only item of interest here is the first parameter passed todecode. 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 ofChecklistItemobjects.
This populates the array with exact copies of the ChecklistItem objects that you froze into the Checklist.plist file.
- This is the start of the
catchblock, which contains the code that executes if any line of code in thedoblock throws an error.
As with saveListItems(), 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 loadListItems() to use
You now have the loadListItems() method, which restores the app’s data from Checklist.plist.
Challenge: Before continuing, ask yourself: Where in the source code would you call the
saveListItems()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 init() method — its initializer — comes in handy. It’s called at that very moment, making it the perfect place to put the call to loadListItems().
➤ Open Checklist.swift and add a call to loadListItems() at the end of its init() method. init() should look like this:
init() {
print("Documents directory is: \(documentsDirectory())")
print("Data file path is: \(dataFilePath())")
loadListItems()
}
It’s time to test loadListItems() by seeing if the app “remembers” changes to the checklist after the user closes and reopens it.
➤ Run the app and make some changes to the checklist.
In my case, I made the same changes that I made when testing saveListItems():
- 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:
➤ Close the app. You can do this by clicking the Stop button in Xcode or by terminating the app in the simulator.
➤ 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:
Before you make the final change to the app, take a closer look at the init() method where you placed loadListItems().
A closer look at initializers
Methods named init are special in Swift. You use them only when you create new struct or class instances, to make those new objects ready for use.
Think of it like buying new clothes. The clothes are in your possession (the memory for the object is allocated) but they’re still in the bag. You need to put the new clothes on (initialization) before you’re ready to go out and party.
When you write the following to create a new object:
let checklist = Checklist()
Swift first allocates a chunk of memory big enough to hold the new object and then calls Checklist’s init() method with no parameters.
Every object blueprint has a built-in init() method with no parameters. If you don’t write your own object blueprint, Swift simply uses the built-in one, which just creates a new instance.
It’s pretty common for objects to have more than one init method. Which one the app uses depends on the circumstances.
Consider ChecklistItem. Here’s its code:
struct ChecklistItem: Identifiable, Codable {
let id = UUID()
var name: String
var isChecked: Bool = false
}
It has two properties that can be set: name and isChecked. name doesn’t have a value assigned to it, which means that it must be assigned a value when the ChecklistItem instance is created. isChecked can also be assigned a value when the instance is created, but it has a default value of false. This means that setting its value at instance creation is optional.
As a result, ChecklistItem has two init() methods:
-
ChecklistItem(name:), which you call when you want to create aChecklistIteminstance and specify just its name. ItsisCheckedproperty will take the default value offalse. -
ChecklistItem(name:isChecked), which you call when you want to create aChecklistIteminstance and specify both its name and its checked status.
Now, consider the case of Checklist, where you defined your own init() method:
init() {
print("Documents directory is: \(documentsDirectory())")
print("Data file path is: \(dataFilePath())")
loadListItems()
}
By writing this method, you’re overriding the built-in init(), which means that you’re replacing the default initializer with one of your own. You did this because you wanted to do more than simply creating a Checklist instance. You also wanted to perform some tasks as the instance was being created.
Note that unlike other methods, init does not have the func keyword.
Sometimes, you’ll see it written as override init or required init?. That’s necessary when you’re adding the init method to an object that’s a subclass of some other object. Much more about that later.
You use the version with the question mark when init? can potentially fail and return a nil value instead of a real object. Decoding an object can fail if not enough information is present in the plist file.
Inside the init method, you first need to make sure that all your instance variables and constants have a value. Recall that in Swift, all variables must always have a value, except for optionals.
When you declare an instance variable, you can give it an initial value (or initialize it), like so:
var checked = false
It’s also possible to write just the variable name and its type (or declare the variable), but not give the variable a value yet:
var checked: Bool
In the latter case, you have to give this variable a value in your init method:
init() {
checked = false
}
You must use one of these approaches. If you don’t give the variable a value at all, Swift considers this an error. The only exception is optionals, which don’t need to have a value. If they don’t, they are nil. You’ll learn more about optionals later in this book.
Swift’s rules for initializers can be a bit complicated but fortunately, the compiler will remind you if you forget to provide an init method.
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.
➤ Go to the Finder window displaying Checklist.plist file. Delete the file.
➤ Run the app. You should now see an empty checklist, ready for you to fill it:
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:
➤ Close the app. Once again, you can do this by clicking the Stop button in Xcode or by terminating theÏapp in the simulator.
➤ Run the app again. You’ll see that the checklist is the same as when you closed the app:
With this final change, Checklist is complete! You have a fully-functional checklist app that remembers its contents between sessions.
This is a good time to go back and repeat those parts you’re still a bit fuzzy about. Don’t rush through these chapters — there are no prizes for finishing first. Rather than going fast, take your time to truly understand what you’re doing.
As always, feel free to change the app and experiment. Here at iOS Apprentice Academy, we not only allow breaking things — we encourage it! You can find the project files for the app up to this point under 14 – Saving and Loading in the Source Code folder.
Next steps
iOS should start to make sense by now. You’ve written an entire app from scratch! Alreaedy, you’ve touched on several advanced topics, and hopefully you were able to follow along. Kudos for sticking with it until the end!
It’s okay if you’re still a bit fuzzy on the details. Sleep on it for a bit and keep tinkering with the code. Programming requires its own way of thinking, and you won’t learn that overnight. Don’t be afraid to create this app again from the start — it will make more sense the second time around!
The first two sections of this book focused mainly on the SwiftUI framework, which is the newest way of building iOS apps. The next sections of this book will introduce you to UIKit, the framework that iOS developers have been using since the beginning of the iPhone, and which many will continue to use for some time.
The next section will also take a step back and cover more details about the Swift language. Pay particular attention, as it’s helpful not just for understanding the code in your upcoming projects… it’ll also give you a better understanding of the code in the projects you’ve already completed.