Data Persistence in iOS

Data persistence means storing any type of data to disk so that the same data can be retrieved without being altered when the user opens the app next time. We are going to describe all the ways to store data locally in iOS apps.

By default when a user quits the app, all the variables and constants that were in memory get lost. To prevent this from happening we can use the following technologies:

UserDefaults

“An interface to the user’s defaults database, where you store key-value pairs persistently across launches of your app.” — Apple

As the name implies, we should use UserDefaults to store the user’s preferences. We should use it to store small pieces of information (e.g. whether the user prefers light or dark mode, at what time they want to receive a daily reminder, whether they actually want to receive notifications, etc). It’s not intended to save a lot of user generated data though! For that use case there are other possibilities on iOS.

As a rule of thumb, if you have a Settings screen in your app, UserDefaults are a good fit.

Pros

Cons

Easy to create a small extension to support the store and retrieve operations for any Codable type.

extension UserDefaults {

    func setCodable<T: Codable>(_ value: T, forKey key: String) {
        guard let data = try? JSONEncoder().encode(value) else {
            fatalError("Cannot create a json representation of \(value)")
        }
        self.set(data, forKey: key)
    }

    func codable<T: Codable>(forKey key: String) -> T? {
        guard let data = self.data(forKey: key) else {
            return nil
        }
        return try? JSONDecoder().decode(T.self, from: data)
    }
}

UserDefaults can be retrieved by using the standard static property, a singleton that gives easy access to this storage device.

Keychain

Some applications need to store private information or data we want to protect: passwords, certificates, and similar items.

For this specific use case, Apple provides the Keychain and the Security framework. The Keychain is a special file on the disk. It is hardware-encrypted and has a bunch of very low-level APIs to access it.

Consider that everything must be stored as Data. Therefore, we need to pass only objects and values that can be serialized into that type.

To use it, we need to:

    func save(_ password: String, for account: String) {
        let password = password.data(using: String.Encoding.utf8)!
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrAccount as String: account,
                                    kSecValueData as String: password]
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else { return print("save error")}
    }

    func getPassword(for account: String) -> String? {
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrAccount as String: account,
                                    kSecMatchLimit as String: kSecMatchLimitOne,
                                    kSecReturnData as String: kCFBooleanTrue]

        var retrivedData: AnyObject? = nil
        let _ = SecItemCopyMatching(query as CFDictionary, &retrivedData)


        guard let data = retrivedData as? Data else {return nil}
        return String(data: data, encoding: String.Encoding.utf8)
    }

If you actually need to use the Keychain in your app, search for a good library and let it do the dirty work for you. It will save you time and headaches. Here are the Keychain libraries with the most stars on GitHub like KeychainAccess, KeychainSwift

Pros

Cons

Saving Files to Disk

Apple makes writing, reading, and editing files inside the iOS applications very easy. Every application has a sandbox directory (called Document directory) where you can store your files. FileManager object provides all these functionalities with very simple APIs.

Files can be stored inside catalogs and sub-catalogs of your app’s Document directory. That’s why Apple recommends using the URL to specify and work with files using the FileManager object.

URL classes, in general, are used when working with a network like API requests and opening the websites. This use cases require adding https:// prefix for every URL to be created. For files, Apple requires using the “file://” prefix, which allows Apple to detect whether you are using the proper file URL. In iOS development, there are some preferred folders that all developers should know about:

The file system in the apps is sandboxed. Your app — and only your app — can access the file system at runtime. However, there are ways to access the files we stored in the disk from a backup.

For big files that need to be encrypted but cannot be stored in the Keychain, consider using the proper encryption option when writing them into a file. Reading and writing in iOS is extremely simple:

Pros

Cons

Simple example in which we would like to save Data to a file on disk:

class FilesManager {

    enum Error: Swift.Error {
        case fileAlreadyExists
        case invalidDirectory
        case writtingFailed
    }

    let fileManager: FileManager

    init(fileManager: FileManager = .default) {
        self.fileManager = fileManager
    }

    func save(fileNamed: String, data: Data) throws {
        guard let url = makeURL(forFileNamed: fileNamed) else {
            throw Error.invalidDirectory
        }
        if fileManager.fileExists(atPath: url.absoluteString) {
            throw Error.fileAlreadyExists
        }
        do {
            try data.write(to: url)
        } catch {
            debugPrint(error)
            throw Error.writtingFailed
        }
    }

    // The method is responsible for creating the URL of the file with the given name.
    // As mentioned, we can only write to the documents directory with .userDomainMask.
    private func makeURL(forFileNamed fileName: String) -> URL? {
        guard let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
            return nil
        }
        return url.appendingPathComponent(fileName)
    }
}

CoreData

Core Data is the Object-Relational Mapping (ORM) framework that comes with iOS. In most common examples CoreData is used as a wrapper for the SQLite database and it’s used to save and present any type of user data.

In Core Data, we create a DataModel that is managed by a Context. The Context then coordinates with a StorePersistor whose responsibility is to store and retrieve data from the disk.

Under the hood, Core Data uses an SQLite database that is stored in the Library/Application Support folder. However, it is possible to pass a custom URL to move the database from the default location.

Core Data shines when we need to keep track of a complex object model with several entities and relationships. It has advanced control over contexts and multi-threading, which makes it easy to modularize access to data.

If you are building a big and complex application already divided into modules, Core Data could be a nice addition to your toolbox.

Functionalities

Pros

Cons

Core Data Example

To setup CoreData in the app, we need to do the following steps:

Let’s create a DataModel. Apple recommends using the application name as DataModel name.

Now, we can create a CoreData stack. The stack should store NSPersistentContainer, and give access to mainContext (which is a NSManagedObjectContext object) – this one should be used for fetching the data and to create background contexts which are responsible for saving the data.

class CoreDataManager {
    static let shared = CoreDataManager()

    private init() {}

    private lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "PillReminder")
        container.loadPersistentStores(completionHandler: { _, error in
            _ = error.map { fatalError("Unresolved error \($0)") }
        })
        return container
    }()

    var mainContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }

    func backgroundContext() -> NSManagedObjectContext {
        return persistentContainer.newBackgroundContext()
    }
}

Loading an array of pills:

func loadPills() -> [Pill] {
    let mainContext = CoreDataManager.shared.mainContext
    let fetchRequest: NSFetchRequest<Pill> = Pill.fetchRequest()
    do {
        let results = try mainContext.fetch(fetchRequest)
        return results
    }
    catch {
        debugPrint(error)
    }
}

To save a new Pill entity, we need to perform the following steps:

func savePill(name: String) throws {
    let context = CoreDataManager.shared.backgroundContext()
    context.perform {
        let entity = Pill.entity()
        let pill = Pill(entity: entity, insertInto: context)
        pill.name = name
        pill.amount = 2
        pill.dozePerDay = 1
        pill.lastUpdate = Date()
        try context.save()
    }
}

SQLite

SQLite is a relational database management system contained in a C programming library. In contrast to many other database management systems, SQLite is not a client-server database engine. In fact, it is embedded in the client application itself. So there is no networking involved in SQLite.

The most common purpose of using SQLite in the app is a possibility to make some data read/write optimizations on the database level, which cannot be done using CoreData. This topic is very wide, so I would like to encourage you to get familiar with the following wrapper for SQLite: https://github.com/groue/GRDB.swift.

Pros

Cons

Property Lists

Plist files work by storing serialised objects with key/value conventions. You’ve probably heard of the Info.plist file. It stores application settings values like the name, version number, etc.

You can store String, Number, Bool, Array, Dictionary, Date, Data

Information stored inside the application’s Info.plist file can be read using the Bundle.main.infoDictionary with subscript access. Fetching the bundle identifier is simple as:

let bundleIdentifire = Bundle.main.infoDictionary[kCFBundleIdentifierKey] as? String

Pros

Cons

Example

struct WalkthroughSinglePageViewModel {

    let headerImageName: String
    let headline: String
    let mainText: String
    let nextButtonTitle: String
    let backgroundColor: String

    init(dictionary: [String: Any]) {
        self.headerImageName = dictionary["headerImageName"] as? String ?? ""
        self.headline = dictionary["headline"] as? String ?? ""
        self.mainText = dictionary["mainText"] as? String ?? ""
        self.nextButtonTitle = dictionary["nextButtonTitle"] as? String ?? ""
        self.backgroundColor = dictionary["backgroundColor"] as? String ?? "FFFFFF"
    }
}

struct WalkthroughViewModel {
    private var pages: [WalkthroughSinglePageViewModel]

    init() {
        guard let url = Bundle.main.url(forResource: "Walkthrough", withExtension: "plist") else {
            fatalError("Missing Resource File")
        }
        guard let dictionary = NSDictionary(contentsOf: url) else {
            fatalError("Corrupted Resource File")
        }
        guard let pageDictionaries = dictionary["pages"] as? [[String : Any]] else {
            fatalError("Corrupted Resource File")
        }
        self.pages = pageDictionaries.map({ dictionary in
            return WalkthroughSinglePageViewModel(dictionary: dictionary)
        })
    }

    var numberOfPages: Int {
        return pages.count
    }

    func page(atIndex index: Int) -> WalkthroughSinglePageViewModel {
        return pages[index]
    }
}

Reading a Plist with Swift

func getPlist(withName name: String, key: String) -> Any? {
    if  let path = Bundle.main.path(forResource: name, ofType: "plist"),
        let myDict = NSDictionary(contentsOfFile: path) {
        return myDict[key]
    }
    return nil
}

// Here’s how we can use the above function:
if let fruits = getPlist(withName: "Fruit") as? [String] {
    print(fruits) // Output: ["Orange", "Pineapple", "Raspberry", ]
}

Reading a Plist with Codable

struct Preferences: Codable {
    var webserviceURL:String
    var itemsPerPage:Int
    var backupEnabled:Bool
}

if  let path        = Bundle.main.path(forResource: "Preferences", ofType: "plist"),
    let xml         = FileManager.default.contents(atPath: path),
    let preferences = try? PropertyListDecoder().decode(Preferences.self, from: xml)
{
    print(preferences.webserviceURL)
}

Writing Data to a Plist

let preferences = Preferences(webserviceURL: "https://api.twitter.com",
                              itemsPerPage: 10,
                              backupEnabled: false)

let encoder = PropertyListEncoder()
encoder.outputFormat = .xml

let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("Preferences.plist")

do {
    let data = try encoder.encode(preferences)
    try data.write(to: path)
} catch {
    print(error)
}