Deciding where to store data

Published by donnywals on

Developers often need to store data on behalf of their users. Sometimes it’s a small amount simple data, other times it’s a lot of complex data. Maybe it’s highly sensitive data or maybe it’s less sensitive or even publicly available data. Choosing where to store this data is often not trivial, especially if you might be unaware of the options that are available to you, or the security considerations you need to keep in mind.

In this week’s blog post I will show you several storage options that are available to you as an iOS developer, and I’ll explain the pros and cons of every storage method.

In this post, you will learn about the following storage mechanisms:

  • UserDefaults
  • Files on disk
  • The Keychain
  • Databases (like CoreData and SQLite)

Disclaimer:
Please note that I am not a security or encryption expert. For this reason, I will only cover every storage mechanism in its “normal”, basic usage. I won’t cover how you can best encrypt data to increase security for any of the listed storage mechanisms. In general, you should at least consider any data that you store on a device accessible by attackers that have access to a physical device.

With that disclaimer out of the way, let’s start exploring storage options on iOS, shall we?

Utilizing UserDefaults storage

Possibly the most well-known, easiest to use storage on iOS is the user defaults store. You typically use this storage for simple user preferences or to store simple values that help improve the user’s experience. In my blog post on optimizing your app’s App Store reviews, user defaults are used to track several metrics like the number of launches, when the user first launched the app and more to figure out the best moment to show the user a review prompt.

What should be stored in UserDefaults

You can store a fairly large amount of data in user defaults but it’s not encouraged to store large blobs of data, images and other large data structures in user defaults. On tvOS, the user defaults store is even limited to a maximum 1MB of data and Apple recommends not exceeding 512KB. This amount of storage is a lot of you only store simple data like booleans, numbers or strings because they’re small and very well-suited for key-value storage. Anything larger will fill up your store rapidly.

UserDefaults and security

You should never store any sensitive data in user defaults. This storage is not encrypted at all so if an app other than your own obtains access to your user defaults store, your user’s data is compromised.

For example, simple preferences that can’t be used to identify your users are okay to store in user defaults. However, a user’s email address, password and other, similar data, should be stored somewhere more secure.

Using the UserDefaults store

If you wish to use the user defaults store, you do so through the UserDefaults class. You can use the default user store that is always available to your app, or you can create your own user defaults store with its own unique identifier. I always recommend the latter since it makes it somewhat easier to swap your user defaults store out for a different one and it allows you to separate your own user defaults from those that can be accessed by external dependencies that you might include in your app.

Reading from user defaults is really fast because your app’s preferences are loaded into memory when your app launches. This means that you don’t have to access the underlying disk storage at all to retrieve values using the UserDefaults class.

You can create and use your own user defaults store as follows:

let myDefaults = UserDefaults(suiteName: "com.donnywals.myapp")
myDefaults?.set(true, forKey: "com.donnywals.myapp.user-is-awesome")
let isUserAwesome = myDefaults?.bool(forKey: "com.donnywals.myapp.user-is-awesome")

Note that you might want to use Swift 5’s powerful property wrappers or a wrapper around UserDefaults to make accessing your user defaults a little bit cleaner since it’s easy to make mistakes while typing your user defaults keys. Especially if they’re quite long or complex like they are in the example that you just read.

Storing files on disk

Sometimes you need more than the limited storage user defaults gives you. If this is the case, you might want to look into storing files on the user’s file system. The file system typically has a large amount of storage available, but it can be relatively slow to read large amounts of data from. As with everything, this is a tradeoff and only by measuring and testing will you know whether you need something quicker than disk access, like for instance an SQLite store or in-memory storage.

What should be stored on disk

Typically, you will use disk storage for larger amounts of data. For example images, videos or large JSON files are suited to be stored on disk. These kinds of files are known as binary data and they have no properties that you might want to query in a database for example. Documents that the user creates like text files or PDFs are also typically stored on disk if possible. It’s not uncommon for developers to make data structures that they want to store on disk conform to Codable so they can easily convert their objects to Data, which can then be written to a file on the file system. Using disk storage to store your Codable objects is especially nice if you want to create a cache of responses that you receive from the network, or if you just want to make sure certain objects persist across app launches.

Disk storage and security

Without any custom encryption, disk storage is very insecure. Apple encrypts devices when they are locked but as soon as the device is unlocked and your app is active, all bets are off. You should avoid storing sensitive data like usernames, passwords, and more in a file that you write to disk at all costs.

Writing files to disk

Most commonly, you will write files to the documents directory, or if the data is used as a cache and can easily be recreated, the caches directory. The following example shows how to write data to the documents directory:

do {
  let fileManager = FileManager.default
  let docs = try fileManager.url(for: .documentDirectory,
                                 in: .userDomainMask,
                                 appropriateFor: nil, create: false)
  let path = docs.appendingPathComponent("myFile.txt")
  let data = "Hello, world!".data(using: .utf8)!
  fileManager.createFile(atPath: path.absoluteString, contents: data, attributes: nil)
} catch {
  // handle error
}

The preceding code snippet creates a file called myFile.txt in the documents directory with Hello, world! as its contents. You can use any kind of Data as the value of contents. For example, you might want to encode a struct into JSON using a JSONEncoder or grab some image data instead.

Using the keychain to store data

The third storage mechanism to consider is the keychain. This is the place where iOS stores all kinds of sensitive user data like usernames, passwords, certificates and more. If you have secure data that you need to store somewhere, the keychain should be your place to go.

What should be stored in the keychain

The keychain is intended to store your user’s sensitive, secret data. I personally use the keychain to store things like OAuth tokens that authenticate the user against a web service. Apple themselves use the keychain for much more than just tokens, they even store entire security certificates in the keychain.

The keychain and security

On iOS, the keychain is probably the best-secured place for you to store data. However, like everything on a device, the keychain can be cracked and you should keep that in mind. Even though Apple is so confident in the keychain that they use it to store username and password combinations, I’m a little bit too paranoid for that. If I can avoid storing passwords, I will. I’d much rather store a token that has no special meaning to the user than storing (and potentially losing) something as sensitive as their username and password.

Reading and writing to the keychain

The Keychain’s APIs are unfortunately very non-trivial to navigate and use. The good news is that there are wrappers available on CocoaPods, Carthage and Swift Package Manager that make using the Keychain much, much easier. I personally like KeychainAccess by kishikawakatsumi a lot.

Database

The last storage option I’ll cover in this comparison is using a database. Databases are very good at efficiently storing large amounts of structured data. And they’re not just good at storing data, they’re also very good at retrieving and filtering data. So whenever you have a larger set of data that you want to filter, store or expand upon later, a database that’s accessed using plain SQLite, or a database wrapper/manager like Realm or CoreData might be ideal for you. I’m not going to compare the performance and usage of every one of these options since in the end, they’re all good at the same thing that all databases are good at.

What should be stored in a database

If you have large objects of structured data like a user’s to-do list or metadata for documents the user creates in your app, data from your user’s weekly running exercises you’re probably going to need a database. If any of these objects have large binary blobs attached to them, like images, you might want to consider mixing disk storage and database storage by writing the large blob of data to disk and storing the path to your data in the database.

Databases and security

Databases on iOS are stored on the device, often in the documents directory. This means that you should consider any data that’s written to your databases compromised unless you’ve applied your own layer of encryption. As always, be very careful with what you store and avoid storing any sensitive data like usernames, passwords, and access token. The Keychain is a better fit for these kinds of data.

Reading and writing to a database

Depending on the database of your choice you will use SQL queries, predicates or something else to access and filter the data in your database. Keep in mind that databases often aren’t thread-safe so you need to be careful when reading and writing data from different threads.

In summary

In this blog post, you’ve learned a lot about different storage options on iOS. You learned that the Keychain is the most secure place to store user secrets. User defaults are great for simple key-value pairs that represent user preferences. File storage is great for storing large blobs of data and lastly, databases should be used to store structured data.

Security-wise, it’s good to reiterate that any data stored on the device should be considered compromised, it’s really just a matter of how complicated it is to extract and decrypt data. Always ask yourself whether you really need to store something that’s user-sensitive.

I hope this blog post will help you to make informed decisions about data storage. If you have questions, doubts, feedback or comments don’t hesitate to reach out to me on Twitter.

Thanks to Ahmed Khalaf for suggesting that I add notes about the max storage for user defaults and that it’s loaded into memory when your app launched. And to Vadim Bulavin for pointing out that I omitted that User Defaults storage is only limited to 1MB on tvOS.

Receive weekly updates about my posts