Chapters

Hide chapters

UIKit Apprentice

Second Edition · iOS 15 · Swift 5.5 · Xcode 13

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

15. Saving & Loading
Written by Fahim Farook

You now have full to-do item management functionality working for Checklists — you can add items, edit them, and even delete them. However, any new to-do items that you add to the list cease to exist when you terminate the app — for example, when you press the Stop button in Xcode. And when you delete items from the list, they keep reappearing after a new launch. That’s not how a real app should behave!

So, it’s time to consider data persistence — or, to put it simply, saving and loading items…

In this chapter you will cover the following:

  • The need for data persistence: A quick look at why you need data persistence.
  • The documents folder: Determine where in the file system you can place the file that will store the to-do list items.
  • Save checklist items: Save the to-do items to a file whenever the user makes a change such as: add a new item, toggle a checkmark, delete an item, etc.
  • Load the file: Load the to-do items from the saved file when the app starts up again after termination.

The need for data persistence

Thanks to the multitasking nature of iOS, an app stays in memory when you close it and go back to the home screen or switch to another app. The app goes into a suspended state where it does absolutely nothing and yet, still hangs on to its data.

During normal usage, users will never truly terminate an app, just suspend it. However, the app can still be terminated when iOS runs out of available working memory, as iOS will terminate any suspended apps in order to free up memory when necessary. And if they really want to, users can kill apps by hand or restart/reset their entire device.

Just keeping the list of items in memory is not good enough because there is no guarantee that the app will remain in memory forever, whether active or suspended.

Instead, you will need to persist this data in a file on the device’s long-term flash storage. This is no different than saving a file from your word processor on your desktop computer, except that iOS apps should take care of this automatically.

The user shouldn’t have to press a Save button just to make sure unsaved data is safely placed in long-term storage.

Apps need to persist data just in case the app is terminated
Apps need to persist data just in case the app is terminated

So let’s get crackin’ on that data persistence functionality!

The documents folder

iOS apps live in a sheltered environment known as the sandbox. Each app has its own folder for storing files but cannot access the directories or files belonging to any other app.

This is a security measure, designed to prevent malicious software such as viruses from doing any damage. If an app can only change its own files, it cannot modify (or affect) any other part of the system.

Your apps can store files in the “Documents” folder in the app’s sandbox.

The contents of the Documents folder are backed up when the user syncs their device with iTunes or iCloud.

When you release a new version of your app and users install the update, the Documents folder is left untouched. Any data the app has saved into this folder stays there when the app is updated.

In other words, the Documents folder is the perfect place for storing your user’s data files.

Get the save file path

Let’s look at how this works in code.

➤ Add the following methods to ChecklistViewController.swift:

func documentsDirectory() -> URL {
  let paths = FileManager.default.urls(
    for: .documentDirectory, 
    in: .userDomainMask)
  return paths[0]
}

func dataFilePath() -> URL {
  return documentsDirectory().appendingPathComponent("Checklists.plist")
}

The documentsDirectory() returns the full path to the Documents folder.

The dataFilePath() method uses documentsDirectory() to construct the full path to the file that will store the checklist items. This file is named Checklists.plist and it lives inside the Documents folder.

Notice that both methods return a URL object. iOS uses URLs to refer to files in its filesystem. Where websites use http:// or https:// URLs, to refer to a file you use a file:// URL.

Note: Double check to make sure your code says .documentDirectory and not .documentationDirectory. Xcode’s autocomplete can easily trip you up here!

➤ Still in ChecklistViewController.swift, add the following two print statements to the bottom of viewDidLoad:

override func viewDidLoad() {
  . . . 
  items.append(item5)
  // Add the following
  print("Documents folder is \(documentsDirectory())")
  print("Data file path is \(dataFilePath())")
}

➤ Run the app. Xcode’s Console will now show you where your app’s Documents folder is actually located.

If I run the app from the Simulator, on my system it shows something like this:

Console output showing Documents folder and data file locations
Console output showing Documents folder and data file locations

If you run it on your iPhone, the path will look somewhat different. Here’s what mine says:

Documents folder is file:///var/mobile/Applications/FDD50B54-9383-4DCC-9C19-C3DEBC1A96FE/Documents

Data file path is file:///var/mobile/Applications/FDD50B54-9383-4DCC-9C19-C3DEBC1A96FE/Documents/Checklists.plist

As you’ll notice, the folder name is a random 32-character ID. Xcode picks this ID when it installs the app on the Simulator or the device. Anything inside that folder is part of the app’s sandbox.

Browse the documents folder

For the rest of this app, run the app on the Simulator instead of a device. That makes it easier to look at the files you’ll be writing into the Documents folder. Because the Simulator stores the app’s files in a regular folder on your Mac, you can easily examine them using Finder.

➤ Open a new Finder window by clicking on the Desktop and typing ⌘+N. Or, by clicking the Finder icon in your dock, if you have one. Then press ⌘+Shift+G — or, select Go ▸ Go to Folder… from the menu, copy the Documents folder path from Xcode Console, and paste the full path to the Documents folder in the dialog. (Don’t include the file:// bit. The path starts with /Users/<yourname>/…)

➤ You will probably see an empty folder at this point. To navigate one level up in Finder to see the sandbox folder for the app, use the ⌘+↑ (command + up arrow) key combination or the Go ▸ Enclosing Folder menu option.

Keep this window open so you can verify that the Checklists.plist file is actually created when you get to that part.

The app’s directory structure in the Simulator
The app’s directory structure in the Simulator

Tip: If you want to navigate to the Simulator’s app directory by traversing your folder structure, then 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 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 previously.

You can see several folders inside the app’s sandbox folder:

  • The Documents folder where the app will put its data files. Currently the Documents folder is empty.

  • The Library folder has cache files and preferences files. The contents of this folder are managed by the operating system.

  • The SystemData folder, as the name implies, is for use by the operating system 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 usage. 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.

It is also possible to get an overview of the Documents folder of apps on your device.

➤ On your device, go to Settings ▸ General ▸ iPhone Storage, scroll down to the list of installed apps — you might have to wait for the list to load — and tap the name of an app.

You’ll now see the size of the contents of its Documents folder, but not the actual contents themselves:

Viewing the Documents folder info on the device
Viewing the Documents folder info on the device

Save checklist items

In this section you are going to write code that saves the list of to-do items to a file named Checklists.plist when the user adds a new item or edits an existing item. Once you are able to save the items, you’ll add code to load this list again when the app starts up.

Plist files

So what is a .plist file?

You’ve already seen a file named Info.plist in the Bull’s Eye lesson. All apps have one, including the Checklists app — see the project navigator. Info.plist contains several configuration options that give iOS additional information about the app, such as what name to display under the app’s icon on the home screen.

“plist” stands for Property List and it is an XML file format that stores structured data, usually in the form of a list of settings and their values. Property List files are very common in iOS. They are suitable for many types of data storage, and best of all, they are simple to use. What’s not to like?

To save the checklist items, you’ll use Swift’s Codable protocol, which lets objects which support the Codable protocol to store themselves in a structured file format.

You actually don’t have to care much about the format used by Codable. In this case it happens to be a .plist file, but you’re not going to mess with that file directly. All you care about is that the data gets stored in some kind of file in the app’s Documents folder, and you’ll leave the technical details to Codable.

Incidentally, you have already used Codable‘s’ Objective-C cousin, NSCoder, behind the scenes in storyboards. When you add a view controller to a storyboard, Xcode uses the NSCoder system to write this object to a file – encoding. Then when your application starts up, it uses NSCoder again to read the objects from the storyboard file – decoding. The Codable protocol works similarly.

The process of converting objects to files and back again is also known as serialization. It’s a big topic in software engineering.

I like to think of this whole process as freezing objects. You take a living object and freeze it so that it is suspended in time.

You store that frozen object into 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 to bring it back to life again.

The process of freezing (saving) and unfreezing (loading) objects
The process of freezing (saving) and unfreezing (loading) objects

Save data to a file

➤ Add the following method to ChecklistViewController.swift:

func saveChecklistItems() {
  // 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 a file. Let’s take the commented lines step-by-step to understand the code:

  1. First, create an instance of PropertyListEncoder which will encode the items array, and all the ChecklistItems in it, into some sort of binary data format that can be written to a file.

  2. The do keyword, which you have not encountered before, sets up a block of code to catch Swift errors. Swift handles errors under certain conditions by throwing an error. In such cases, you need a block of code to catch the error and to handle it. The do keyword indicates the start of such a block. You will see the error catching code after comment #5, where the catch keyword is.

  3. The encoder you created earlier is used to try to encode the items array. The encode method throws a Swift error if it is unable to encode the data for some reason — for example, the data is not in the expected format, or it is corrupted etc. The try keyword indicates that the call to encode can fail and if that happens, that it will throw an error. If you do not have the try keyword before a call to a method which throws an error, you will get an Xcode error. Try it and see. If the call to encode fails, execution will immediately jump to the catch block instead of proceeding on to the next line.

  4. If the data constant was successfully created by the call to encode in the previous line, then you write the data to a file using the file path returned by a call to dataFilePath(). Note that the write method also can throw an error. So again, you have to precede the method call with another try statement.

  5. The catch statement indicates the block of code to be executed if an error was thrown by any line of code in the enclosing do block.

  6. Handle the caught error. Here, you simply print out an error message to the Xcode Console, but you might notice that the print statement references an error variable. Where did that come from?

    When you create a do - catch block of code, you can explicitly check for specific types of errors. We won’t get into that at this point, but what you need to know is that if you simply have a catch block, Swift will automatically populate a local variable named error which will contain the error thrown by one of the statements within the do block.

    So, you can simply refer to that error variable in any code you write in the catch block. This can be handy for outputting a descriptive error message which indicates what the source of the error/failure was.

You will notice that Xcode shows an error at this point saying: Class ‘PropertyListEncoder’ 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/decoders compatible with the Codable protocol — must support the Codable protocol, and ChecklistItem does not.

The Codable protocol

Swift arrays — as well as most other standard Swift objects and structures — conform to the Codable protocol. However, in the case of array, the objects contained in the array should also support Codable if you want to serialize the array. That’s why we need ChecklistItem class to be Codable compliant.

Note: Sometimes when working with code dealing with Codable support, you will see error messages or references to Encodable or Decodable protocols. So, it might be good to know that Codable is actually a protocol which combines these two other protocols, Encodable and Decodable — one for each side of the serialization process.

➤ Switch to ChecklistItem.swift and modify the class line as follows:

class ChecklistItem: NSObject, Codable {

In the above, you tell the compiler that ChecklistItem will conform to the Codable protocol. That’s all you need to do!

“Now, hold on,” I hear you say. “We had to implement methods to support a protocol before. How come we don’t have to do that here?”

Remember how I mentioned previously that protocols can have default implementations? No? OK, it was in the Delegates and Protocols chapter in the section about protocols :] Sometimes, it is useful to have a default implementation for a protocol to provide functionality that would make things easier — or would cover a lot of standard scenarios.

In our case, all of the properties of ChecklistItem are standard Swift types, and Swift already knows how to encode/decode those types. So, we can simply piggyback on existing functionality without having to write any code of our own to implement encoding/decoding in ChecklistItem. Handy, eh?

Using the new method

You have to call the new saveChecklistItems() method whenever the list of items is modified.

Exercise: Where in the source code would you call this method?

Answer: Look at where the items array is modified. This happens inside the ItemDetailViewControllerDelegate methods. That’s where the party’s at!

➤ Add a call to saveChecklistItems() to the end of these methods in ChecklistViewController.swift:

func itemDetailViewController(
  _ controller: ItemDetailViewController, 
  didFinishAdding item: ChecklistItem
) {
  . . .
  saveChecklistItems()
}
func itemDetailViewController(
  _ controller: ItemDetailViewController, 
  didFinishEditing item: ChecklistItem
) {
  . . .
  saveChecklistItems()
}

➤ Let’s not forget the swipe-to-delete function:

override func tableView(
  _ tableView: UITableView,
  commit editingStyle: UITableViewCellEditingStyle,
  forRowAt indexPath: IndexPath
) {
  . . .
  saveChecklistItems()
}

➤ And toggling the checkmark on a row:

override func tableView(
  _ tableView: UITableView,
  didSelectRowAt indexPath: IndexPath
) {
  . . .
  saveChecklistItems()
}

Verify the saved file

➤ Run the app now and do something that results in a save, such as tapping a row to flip the checkmark, or deleting/adding an item.

➤ Go to the Finder window that has the app’s Documents directory open:

The Documents directory now contains a Checklists.plist file
The Documents directory now contains a Checklists.plist file

There is now a Checklists.plist file in the Documents folder, which contains the items from the list.

You can look inside this file if you want, but the contents won’t make much sense. Even though it is XML, this file wasn’t intended to be read by humans, only by something like PropertyListDecoder, the counterpart to the PropertyListEncoder that we already used.

If you’re having trouble viewing the XML, it may be because the plist file isn’t stored as text but as a binary format. Some text editors support this file format and can read it as if it were text — BBEdit is a good option and is a free download on the Mac App Store.

You can also use Finder’s Quick Look feature to view the file. Simply select the file in Finder and press the space bar.

Naturally, you can also open the plist file with Xcode.

➤ Right-click the Checklists.plist file and choose Open With ▸ Xcode.

Checklist.plist in Xcode
Checklist.plist in Xcode

It still won’t make much sense but it’s fun to look at anyway.

Expand some of the rows and you’ll see that the names of the ChecklistItems are in there as well as their checked/unchecked state. But exactly how all these data items fit together, might not make much sense to you just yet.

“NS” objects & Documentation

Objects whose name start with the “NS” prefix, like NSObject, NSString, or NSCoder, are provided by the Foundation framework. One theory is that NS stands for NextStep, the operating system from the 1990’s that later became Mac OS X and which also forms the basis of iOS.

If you are curious about exactly how objects such as NSObbject and NSString work, you can Alt/Option-click any item in your source code to bring up a popup with a brief description. And this works for non-NS prefixed objects too :] In fact, you can look up details about any class, object, variable, or method this way in Xcode.

I use this all the time to remind myself of how to use framework objects and their methods. You can click on any blue color items on the popup since they are links to detailed documentation that will take you to the Developer Documentation app which lets you read up further on the selected subject.

It’s good to have a general idea of what objects are available in the frameworks, but no one can remember all the specifics. So get into the habit of looking up the documentation for any new objects and methods that you encounter. It’ll help you learn the iOS frameworks that much quicker!

Load the file

Saving is all well and good, but pretty useless by itself. So, let’s also implement the loading of the Checklists.plist file. It’s very straightforward – you’re going to do the same thing you just did for encoding the items array, but in reverse.

Read data from a file

➤ Switch to ChecklistViewController.swift and add the following new method:

func loadChecklistItems() {
  // 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)
    } catch {
      print("Error decoding item array: \(error.localizedDescription)")
    }
  }
}

Let’s go through this step-by-step:

  1. First, you put the results of dataFilePath() in a temporary constant named path.

  2. Try to load the contents of Checklists.plist into a new Data object. The try? command attempts to create the Data object, but returns nil if it fails. That’s why you put it in an if let statement. Why would it fail? If there is no Checklists.plist file, then there are obviously no ChecklistItem objects to load. This is what happens when the app is started up for the very first time. In that case, you’ll skip the rest of this method.

    Also, do notice that this is another way to use the try statement — instead of enclosing the try statement within a do block, like you did previously, you can have a try? statement which indicates that the try could fail and if it does, that it will return nil. Whether you use the do block approach or this one, is completely up to you.

  3. When the app does find a Checklists.plist file, you’ll load the entire array and its contents from the file using a PropertyListDecoder. So, create the decoder instance.

  4. Load the saved data back into items using the decoder’s decode method. The only item of interest here would be the first parameter passed to decode . The decoder needs to know what type of data will be the result of the decode operation and you let it know by indicating that it will be an array of ChecklistItem objects.

This populates the array with exact copies of the ChecklistItem objects that were frozen into the Checklists.plist file.

You now have your loadChecklistItems() method, but it needs to be called from somewhere in order for this to work. There are several places from which you can do this.

Take a look at the current code in ChecklistViewController.swift — it would seem that using viewDidLoad() is the obvious choice since that’s where we currently load the static data for the app. So let’s clear out the static data items and simply load the saved data from viewDidLoad!

Load the saved data on app start

Here’s what you need to do:

➤ Remove the existing lines for creating the five static ChecklistItem instances and the print statements from viewDidLoad and replace that with a call to loadChecklistItems as follows:

override func viewDidLoad() {
  super.viewDidLoad()
  navigationController?.navigationBar.prefersLargeTitles = true
  // Load items
  loadChecklistItems()
}

Note: If you opted to enable large titles via the storyboard, then you wouldn’t have the line immediately after the call to super.viewDidLoad().

You don’t need to add the comments in there but its always good to have some comments in your source so that you can understand your own code a month or two (or a few years) down the line :] But as I mentioned before, if you have good method names, comments are really unnecessary most of the time.

All that’s new in the above (apart from the deleted code) is the addition of a call to loadChecklistItems() to ensure that the saved item data is loaded back when the view controller is first loaded.

➤ Run the app and make some changes to the to-do items. Press Stop to terminate the app. Start it again and notice that your changes are still there.

➤ Stop the app again. Go to the Finder window with the Documents folder and remove the Checklists.plist file. Run the app once more. You should now have an empty list of items.

➤ Add an item and notice that the Checklists.plist file re-appears.

Awesome! You’ve written an app that not only lets you add and edit data, but which also persists the data between sessions. These techniques form the basis of many, many apps.

Being able to use a navigation controller, show secondary screens, and pass data around through delegates are also essential iOS development skills.

Initializers

Methods named init are special in Swift. They are only used when you’re creating new objects, to make those new objects ready for use.

Think of it as having bought 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 go change and 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 item = ChecklistItem()

Swift first allocates a chunk of memory big enough to hold the new object and then calls ChecklistItem’s init() method with no parameters.

It is pretty common for objects to have more than one init method. Which one is used depends on the circumstances.

For example, amongst the init methods for UITableViewController you’ll find — init(nibName:bundle:), init(style:) and init?(coder:). init?(coder:) is used when the view controller is instantiated from a storyboard. But you can also create a UITableViewController instance directly by calling either init(nibName:bundle:) or init(style:) . So, how you initialize an object depends on the circumstances.

The implementations of these init methods, whether they’re just called init() or init?(coder:) or something else, always follow the same series of steps. When you write your own init methods, you need to stick to those steps as well.

This is the standard way to write an init method:

init() {
  // Put values into your instance variables and constants.

  super.init()

  // Other initialization code, such as calling methods, goes here.
}

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 is necessary when you’re adding the init method to an object that is a subclass of some other object. Much more about that later.

The question mark is for when init? can potentially fail and return a nil value instead of a real object. You can imagine that 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
  super.init()
}

You must use either 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, they do not need to have a value (in which case they are nil). Once you’ve given all your instance variables and constants values, you call super.init() to initialize the object’s superclass. If you haven’t done any object-oriented programming at all, you may not know what a superclass is. That’s fine; we’ll completely ignore this topic till later.

Just remember that sometimes objects need to send messages to something called super and if you forget to do this, bad things are likely to happen. After calling super.init(), you can do additional initialization, such as calling the object’s own methods. You’re not allowed to do that before the call to super.init() because Swift has no guarantee that your object’s variables all have proper values until then.

You don’t always need to provide an init method. If your init method doesn’t need to do anything — if there are no instance variables to fill in — then you can leave it out completely and the compiler will provide one for you. As an example, take a look at ChecklistItem — it doesn’t have an init() method since all its variables are initialized when they are declared.

Swift’s rules for initializers can be a bit complicated, but fortunately, the compiler will remind you when you forget to provide an init method.

What next?

Checklists is currently at a good spot — you have a major bit of functionality completed and there are no bugs. This is a good time to take a break, put your feet up, and daydream about all the cool apps you’ll soon be writing :]

It’s also smart 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’ve been doing.

As always, feel free to change the app and experiment. Breaking things is allowed — even encouraged — here at UIKit Apprentice Academy!

You can find the project files for the app up to this point under 15-Saving-loading in the Source Code folder.

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