Persisting data across application launches is a requirement that most iOS applications have, from storing user preferences in the defaults system to managing large data sets in a relational database. In this article, we'll explore the most common strategies used for storing data in an iOS application. I will also talk about the file system on iOS and how application sandboxing affects data persistence.
Introduction
You've come a long way, grasshopper, and you've learned a lot. But there's one vital aspect of iOS development that we haven't discussed yet, data persistence. Virtually every iOS application stores data for later use. The data your application stores can be anything from user preferences to temporary caches or even large relational data sets.
Before discussing the most common data persistence strategies developers have on the iOS platform, I'm going to spend a few minutes discussing the file system and the concept of application sandboxing. Did you really think you could store your application's data wherever you'd like on the file system? Think again, padawan.
File System and Application Sandboxing
Security on the iOS platform has been one of Apple's top priorities ever since the iPhone was introduced in 2007. In contrast to OS X applications, an iOS application is placed in an application sandbox. The sandbox of an application doesn't only refer to an application's sandbox directory in the file system. It also includes controlled and limited access to user data stored on the device, system services, and hardware.
With the introduction of the Mac App Store, Apple has begun to enforce application sandboxing on OS X as well. Even though the constraints put on OS X applications are not as stringent as the ones imposed on iOS applications, the basic concept is similar. There are differences, though. The application sandbox of an iOS application, for example, contains the application bundle, which isn't true for OS X applications. The reasons for these differences are mainly historical.
Sandboxing and Directories
The operating system installs each iOS application in a sandbox directory that contains the application bundle directory and three additional directories, Documents, Library, and tmp. The application's sandbox directory, often referred to as its home directory, can be accessed by calling a simple Foundation function, NSHomeDirectory()
.
print(NSHomeDirectory())
You can try this yourself. Create a new Xcode project based on the Single View Application template and name it Data Persistence.
Open AppDelegate.swift and add the above code snippet to application(_:didFinishLaunchingWithOptions:)
. If you run the application in the simulator, the output in the console should look something like this:
/Users/Bart/Library/Developer/CoreSimulator/Devices/14F00EFB-2EAB-438C-B401-7FEFDA1D94AB/data/Containers/Data/Application/81B23594-3BA2-4AF9-B91A-F74A53FD6945
However, if you run the application on a physical device, the output looks a bit different as you can see below. The application sandbox and the limitations imposed are identical, though.
/var/mobile/Containers/Data/Application/41E7939B-6A39-4005-9C28-372FD9C7AD99
Retrieving the path to the application's Documents directory requires a bit more work as you can see in the next code snippet.
let directories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true) if let documents = directories.first { print(documents) }
We invoke the NSSearchPathForDirectoriesInDomains()
function, which is defined in the Foundation framework. As the first argument, we pass in DocumentDirectory
of type NSSearchPathDirectory
to indicate that we're only interested in the application's Documents directory. The second and third argument are of less importance for our discussion. The function returns an array of type [String]
, containing one result, the path to the application's Documents directory.
Why Sandboxing?
What is the benefit of sandboxing? The primary reason for sandboxing applications is security. By confining applications to their own sandbox, compromised applications cannot cause damage to the operating system or other applications.
By compromised applications, I mean both applications that have been hacked, applications that are intentionally malicious, as well as applications that contain critical bugs that may inadvertently cause damage.
Even though applications are sandboxed on the iOS platform, applications can request access to certain files or assets that are outside of their application sandbox through a number of system interfaces. An example of this is the music library stored on a device. Know, however, that the system frameworks are in charge of any operations related to file access.
What Goes Where?
Even though you can do pretty much anything you want in your application's sandbox, Apple has provided a few guidelines with regards to what should be stored where. It's important to know about these guidelines for several reasons. When an iOS device is backed up to your computer or to iCloud, not all the files in the sandbox are included in the backup.
The tmp directory, for example, should only be used for temporarily storing files. The operating system is free to empty this directory at any time, for example, when the device is low on disk space. The tmp directory isn't included in backups.
The Documents directory is meant for user data, whereas the Library directory is used for application data that isn't strictly tied to the user. The Caches directory in the Library directory is another directory that isn't backed up.
Also keep in mind that your application isn't supposed to modify the contents of the application bundle directory. The application bundle directory is signed when the application is installed. By modifying the contents of the application bundle directory in any way, the aforementioned signature is altered, which means the operating system doesn't allow the application to launch again. This is another security measure put into place by Apple to protect customers.
Data Persistence Options
There are several strategies for storing application data on disk. In this article, we take a brief look at four common approaches on iOS:
- defaults system
- property lists
- SQLite
- Core Data
The options described in this article shouldn't be considered as interchangeable. Each strategy has benefits and drawbacks. Let's start by taking a look at the defaults system.
User Defaults
The defaults system is something that iOS inherited from OS X. Even though it was created and designed for storing user preferences, it can be used for storing any type of data as long as it's a property list type, NSString
, NSNumber
, NSDate
, NSArray
,NSDictionary
, and NSData
, or any of their mutable variants.
What about Swift data types? Fortunately, Swift is smart enough. It can store strings and numbers by converting them to NSString
and NSNumber
. The same applies to Swift arrays and dictionaries.
The defaults system is nothing more than a collection of property lists, one property list per application. The property list is stored in a Preferences folder in the application's Library folder, hinting at the property list's purpose and function.
One of the reasons that developers like the defaults system is because it's so easy to use. Take a look at the following example to see what I mean.
let userDefaults = NSUserDefaults.standardUserDefaults() // Setting Values userDefaults.setBool(true, forKey: "Key1") userDefaults.setInteger(123, forKey: "Key2") userDefaults.setObject("Some Object", forKey: "Key3") userDefaults.setObject([1, 2, 3, 4], forKey: "Key4") // Getting Values userDefaults.boolForKey("Key1") userDefaults.integerForKey("Key2") userDefaults.objectForKey("Key3") userDefaults.objectForKey("Key4") userDefaults.synchronize()
By calling standardUserDefaults()
on NSUserDefaults
, a reference to the shared defaults object is returned.
In the last line, we call synchronize()
on the shared defaults object to write any changes to disk. It's rarely necessary to invoke synchronize()
, because the defaults system saves changes when necessary. However, if you store or update a setting using the defaults system, it can sometimes be useful or necessary to explicitly save the changes to disk.
At first glance, the defaults system seems to be nothing more than a key-value store located at a specific location. However, the NSUserDefaults
class, defined in the Foundation framework, is more than an interface for managing a key-value store. Take a look at its class reference for more information.
Before we move on, paste the above code snippet in the application delegate's application(_:didFinishLaunchingWithOptions:)
method and run the application in the simulator. Open a new Finder window and navigate to Library > Developer > CoreSimulator > Devices > <DEVICE_ID> > data > Containers > Data > Application > <APPLICATION_ID>.
<DEVICE_ID> and <APPLICATION_ID> are two identifiers unique to the simulator and your application respectively. The location of the application sandbox for the simulator depends on the version of Xcode you're using. If you're not using Xcode 7, the path is probably going to be different.
You can make your life easier by printing the path to your application's home directory to Xcode's console. Add the following print statement to application(_:didFinishLaunchingWithOptions:)
.
print(NSHomeDirectory())
The cryptically named folder is the application sandbox directory. In the application sandbox directory, open the Preferences folder, located in the Library folder, and inspect its contents.
You should see a property list with a name identical to the application's bundle identifier. This is the user defaults store for your application. This is what the property list looks like in a text editor. As you can see, the property list is stored on disk as an XML file.
<?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"><dict><key>Key1</key><true/><key>Key2</key><integer>123</integer><key>Key3</key><string>Some Object</string><key>Key4</key><array><integer>1</integer><integer>2</integer><integer>3</integer><integer>4</integer></array></dict></plist>
If you want to make accessing the sandbox of an application in the simulator easier, then I encourage you to take a look at SimPholders. It's a small utility that makes working with the simulator much, much easier.
Property Lists
We've already covered property lists in this series. As a matter of fact, the backing store of the user defaults database is a property list. Using property lists is a convenient strategy to store and retrieve an object graph. Property lists have been around for ages, are easy to use, and, therefore, they're a great option for storing data in an iOS application.
As I mentioned earlier, it's important to keep in mind that a property list can only store property list data. Does this mean that it's not possible to store custom model objects using property lists? That is possible. However, custom model objects need to be archived—a form of serialization—before they can be stored in a property list. Archiving an object simply means that the object needs to be converted to a data type that can be stored in a property list, such as an NSData
instance.
Archiving Objects
Do you remember the NSCoding
protocol defined in the Foundation framework? The NSCoding
protocol defines two methods,init(coder:)
and encodeWithCoder(_:)
. A class implements these methods to allow instances of the class to be encoded and decoded.
Encoding and decoding are the underlying mechanisms for object archival and distribution. How object archival works will become clear a bit later in this series. In this lesson, I only show you how to write arrays and dictionaries to disk using property lists.
Writing to File
The following code snippet should give you an idea of how easy it is to write an array or dictionary to disk. In theory, the object graph stored in a property list can be as complex or as large as you'd like. However, keep in mind that property lists are not meant to store tens or hundreds of megabytes of data, attempting to use them that way will likely result in degraded performance.
let directories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true) if let documents = directories.first { if let urlDocuments = NSURL(string: documents) { let urlFruits = urlDocuments.URLByAppendingPathComponent("fruits.plist") let urlDictionary = urlDocuments.URLByAppendingPathComponent("dictionary.plist") // Write Array to Disk let fruits = ["Apple", "Mango", "Pineapple", "Plum", "Apricot"] as NSArray let dictionary = ["anArray" : fruits, "aNumber" : 12345, "aBoolean" : true] as NSDictionary fruits.writeToFile(urlFruits.path!, atomically: true) dictionary.writeToFile(urlDictionary.path!, atomically: true) // Load from Disk let loadedFruits = NSArray(contentsOfURL: urlFruits) if let fruits = loadedFruits { print(fruits) } let loadedDictionary = NSDictionary(contentsOfURL: urlDictionary) if let dictionary = loadedDictionary { print(dictionary) } } }
Let's take a look at the above code snippet. We start by storing a reference to an array literal in a variable named fruits
. We create the file URL for storing the property list that we're about to make. The file URL is created by appending a string to the file URL of the Documents directory. The string we append is the name of the property list that we create in a second, including its extension, .plist.
Writing the array to disk is as easy as calling writeToFile(_:atomically:)
on the array. You can ignore the atomically
flag for now. As the example illustrates, writing a dictionary to disk follows a similar pattern. The example also illustrates how to create arrays and dictionaries from a property list, but this is something that we already covered earlier in this series. Run the application in the simulator and navigate to the application's Documents directory. In this directory, you should see the two property lists we just created.
This is what the property list of the dictionary looks like when you open it in a text editor.
<?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"><dict><key>aBoolean</key><true/><key>aNumber</key><integer>12345</integer><key>anArray</key><array><string>Apple</string><string>Mango</string><string>Pineapple</string><string>Plum</string><string>Apricot</string></array></dict></plist>
SQLite
If your application is data driven and works with large amounts of data, then you may want to look into SQLite. What is SQLite? The tagline on the SQLite website reads "Small. Fast. Reliable. Choose any three.", which sums it up nicely.
SQLite is a library that implements a lightweight embedded relational database. As its name implies, it's based on the SQL standard (Structured Query Language) just like MySQL and PostgreSQL.
The main difference with other SQL databases is that SQLite is portable and very lightweight. Instead of a separate process accessed from the client application, SQLite is serverless. In other words, it's embedded in the application or managed by the system the application runs on, which means it's very fast.
The SQLite website claims that it's the most widely deployed SQL database. I don't know if that's still the case, but it's certainly a popular choice for client-side data storage. The advantage SQLite has over working directly with objects is that SQLite is much, much faster. This is largely due to how relational databases and object-oriented programming languages fundamentally differ.
To bridge the gap between SQLite and Objective-C, a number of Object Relational Mapping (ORM) solutions have been created over time. The ORM that Apple has created for iOS and OS X is named Core Data, which we'll take a look at later in this lesson.
Flying Meat's FMDB
Using SQLite on iOS means working with a C-based library. If you prefer an object-oriented solution, then I highly recommend Gus Mueller's (Flying Meat, Inc.) Objective-C wrapper for SQLite, FMDB.
The library is very performant. I've used FMDB in the past and have been very happy with its API and the library's robustness and reliability.
Core Data
Developers new to Core Data often mistake Core Data for a database while it really is an object relational mapping solution created and maintained by Apple. Matt Gallagher has written a great post about the differences between Core Data and a database. Core Data provides a relational object-oriented model that can be serialized into an XML, binary, or SQLite store. Core Data even has support for in-memory stores.
Why should you use Core Data instead of SQLite? By asking this question, you wrongly assume that Core Data is a database. The advantage of using Core Data is that you work with objects instead of raw data, such as rows in a SQLite database or data stored in an XML file. Even though Core Data has had some difficult years when it was first released, it has grown into a robust framework with many features, including automatic migrations, change tracking, faulting, and integrated validation.
Another great feature that many developers appreciate is the Core Data model editor, built into Xcode. The editor lets developers model the application's data model through a graphical interface.
Whether Core Data is the right solution for your application depends on the data that you plan to manage, both in terms of the amount of data as well as the underlying model. If you plan to manage very large data sets, then Core Data could become a performance bottleneck over time. In that case, SQLite may be a better solution.
iCloud
You've probably heard of iCloud and you may be wondering where iCloud fits into the story of data persistence. Unlike SQLite and Core Data, iCloud isn't a form of data persistence. Instead, it's a platform or service for making user data available across multiple devices and multiple instances of an application—or even a family of applications.
The iCloud platform encompasses several services or components. The component that interests us is iCloud Storage, which includes four types of storage:
- CloudKit
- key-value storage
- document storage
- Core Data storage
If you want to read more about iCloud Storage, I recommend reading my series about iCloud Storage.
Conclusion
You should now have a good idea of the options you have to store data when developing for the iOS platform. Keep in mind that not all the strategies that we've covered are equal.
This series is slowly coming to a close. In the next two installments, we'll create another application to put what we've learned so far into practice. The best way to learn is by doing.
If you have any questions or comments, you can leave them in the comments below or reach out to me on Twitter.