All about UserDefaults, KeyChain and CoreData — iOS

In this article, we will have a detailed discussion about when and how to use UserDefaults, Core Data and Keychain in our iOS app project. This write-up will touch upon the following topics and you will get a proper understanding of these concepts along with the code implementations.

  • User Defaults

  • KeyChain

  • CoreData


UserDefaults

As per Apple’s Documentation, UserDefaults is

An interface to the user’s default database, where you store key-value pairs persistently across launches of your app.

What type of data can we store in UserDefaults?

A default object must be a property list — that is, an instance of (or for collections, a combination of instances of) NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData.

What does “A default object must be a property list” means?

You may have previously seen a .plist file before, plist stands for property list. There is usually an info.plist file created for you when you start a new iOS project. When you store data in UserDefaults, the data format is similar to Info.plist as well. The UserDefaults plist is saved in the Library folder inside the app folder.

Adding, Reading and Removing values from UserDefaults

The shared defaults object is utilized by the majority of applications. The API of the UserDefaults class is easy to use in Swift. The most basic API to read or get values from the user's defaults database in Swift is object(forKey:). This method returns a value of type Any?, which is optional. As previously stated, the defaults database is a key-value store. The UserDefaults class looks up the value for the key passed to the object(forKey:) function and returns a value if one exists. If no value exists for the provided key, it returns nil.

Creating or updating a key-value pair is simple. Just simply invoke set(_:forKey:)method on the UserDefaults instance. In this example, we set the value of theme to light by invoking the set(_:forKey:) method on the shared defaults object.

import Foundation
// Access Shared Defaults Object
let userDefaults = UserDefaults.standard
// Write
userDefaults.set("light", forKey: "theme")
// Read
let themePreference = userDefaults.object(forKey: "theme") as? String
// Remove
userDefaults.removeObject(forKey: "theme")

In the documentation, Apple mentioned a few example use cases. For example, you can allow users to specify their preferred units of measurement or media playback speed. Apps store these preferences by assigning values to a set of parameters in a user’s defaults database.

You can also use UserDefaults for storing user settings (eg: settings page in your app with UISwitch, Segmented Control or simple Textfield or, store non-sensitive data such as the high score for a game, recently played song etc.

⚠️ The UserDefault class loads the contents of the property list once into memory when your app launches to improve performance and writes any changes you make to disk. Thus, avoid storing large amounts of data in UserDefaults as it can affect the performance of your app significantly.


KeyChain

As previously explained, UserDefaults saves data into a plist file. Users can get access to the Library/Preferences folder of their iPhone and read or modify the UserDefaults plist data easily (eg: the user can change the boolean value of “isPremiumUser” from false to true, or change the value of trials left).

Other than in-app purchase status, you shouldn’t keep user passwords or API keys in UserDefaults for the same reason. This is where KeyChain comes in to save us. For sensitive information, such as a username or an access token, I strongly recommend using the keychain.

Keychain Services is a secure storage interface for macOS and iOS best used for small pieces of private data like passwords, cookies, and authentication tokens.

From Apple documentation, the KeyChain services API helps you solve this problem by giving your app a mechanism to store small bits of user data in an encrypted database called a keychain. When you securely remember the password for them, you free the user to choose a complicated one.

Most of the Keychain services API provided by Apple are written in C language and its APIs are not as Swifty as other modern frameworks by Apple. Now, let’s get start to implement a helper class that saves, updates, reads and deletes data using the keychain service.

Let’s make the KeychainHelper a helper class as a singleton class, that manages our keychain services.

class KeychainHelper {
    static let shared = KeychainHelper()
    private init() {}

    // Class implementation here...
}

Saving Data To Keychain

SecItemAdd is used to save new items to Keychain. An item is uniquely identified by query, a CFDictionary that specifies the items:

  • kSecValueData: A key that represents the data being saved to the keychain.

  • kSecClass: A key that represents the type of data being saved. Here we set its value as kSecClassGenericPassword indicates that the data we are saving is a generic password item.

  • kSecAttrService and kSecAttrAccount: These 2 keys are mandatory when kSecClass is set to kSecClassGenericPassword. The values for both of these keys will act as the primary key for the data being saved. In other words, we will use them to retrieve the saved data from the keychain later on. For example, if we are saving the Facebook access token, we can set kSecAttrService as “password-token” and kSecAttrAccount as ““.

func save(data: Data, service: String, account: String) {
    let query = [
        kSecValueData: data,
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: service,
        kSecAttrAccount: account,
    ] as CFDictionary

    let status = SecItemAdd(query, nil)

    if status != errSecSuccess {
        // Print out the error
        print("Error: \(status)")
    }
}

Here in the above method save(data:service:account), we first create the query, then we call the SecItemAdd(_:_:) method to save the data to the keychain. The SecItemAdd(_:_:) method will then return an OSStatus indicates the status of the save operation. If we get the errSecSuccess status, means that the data has been successfully saved to the keychain.

Here’s how to use the save(data:service:account) method we just created:

let passwordToken = "Password@123"
let data = Data(passwordToken.utf8)
KeychainHelper.shared.save(data: data, service: "password-token", account: "abc@gmail.com")

Updating Existing Data in Keychain

If we try to save another token using the same kSecAttrService and kSecAttrAccount value, we won’t be able to save the access token to the keychain, and will get an error because the keys that we use already exist in the keychain. Let’s update our previous save(data:service:account) method.

func save(data: Data, service: String, account: String) {
    // ... ...

    if status == errSecDuplicateItem {
        // Item already exist, thus update it.
        let query = [
            kSecAttrService: service,
            kSecAttrAccount: account,
            kSecClass: kSecClassGenericPassword,
        ] as CFDictionary

        let attributesToUpdate = [kSecValueData: data] as CFDictionary

        // Update existing item
        SecItemUpdate(query, attributesToUpdate)
    }
}

SecItemUpdate is used to override existing data in Keychain. Similar to the save operation, we must first create a query object that consists of kSecAttrService and kSecAttrAccount. But this time, we will have to create another dictionary that consists of kSecValueData and feed it to the SecItemUpdate(_:_:) method. SecItemUpdate expects the new value of the Keychain item to be passed inside a different argument attributesToUpdate using the same key, kSecValueData.

Reading Data from Keychain

The way to read data from the keychain is very similar to how we save data to it. We first create a query object and then we use SecItemCopyMatching method to get the data from keychain.

func read(service: String, account: String) -> Data? {
    let query = [
        kSecAttrService: service,
        kSecAttrAccount: account,
        kSecClass: kSecClassGenericPassword,
        kSecReturnData: true
    ] as CFDictionary

    var itemCopy: AnyObject?
    let status = SecItemCopyMatching(query, &itemCopy)

    if status == errSecItemNotFound {
        print("Error: ItemNotFound")
        return nil
    }
    if status != errSecSuccess {
        print("Error: UnexpectedError")
        return nil
    }
    let password = itemCopy as? Data
    return password
}

Let’s use the read(service:account) method we just created:

let data = KeychainHelper.shared.read(service: "password-token", account: "abc@gmail.com")!
let password = String(data: data, encoding: .utf8)!
print(password)

Delete Data In Keychain

Like the other Keychain methods, SecItemDelete takes in a query and returns an OSStatus. Keychain will delete permanently associate data with the items matching the query.

func delete(service: String, account: String) {
    let query = [
        kSecAttrService: service,
        kSecAttrAccount: account,
        kSecClass: kSecClassGenericPassword,
        ] as CFDictionary

    let status = SecItemDelete(query)
    if status == errSecSuccess {
        print("Delete Successful")
    }
}

This is all about how we can use KeyChain Services to sensitive information instead of in UserDefaults. Here, we have implemented all the necessary functionalities of the keychain helper class but it only supports reading and writing items of type Data. We can extend our helper class to create a generic save method that accepts any object with the data type.


Core Data

Core Data is an object graph and persistence framework provided by Apple. It allows data to be organized in a relational entity–attribute model manner so that it can be serialized into XML, binary, or SQLite stores.

You can use Core Data to save your application’s permanent data for offline use, cache temporary data, and add undo functionality to your app on a single device.

💡 Remember that Core Data isn’t a database nor it consists of a table of rows and columns. Core Data abstracts the details of mapping your objects to a store or database, making it easy to save data from Swift and Objective-C without administering a database directly.

The first step in working with Core Data is to create a data model file to define the structure of your app’s objects, including their object types, properties, and relationships.
You can add a Core Data model file to your Xcode project when you create the project, or you can add it to an existing project. Let’s build a core data model for a notes app.

Add Core Data to a New Xcode Project

While creating a new project, select the Use Core Data checkbox. The resulting project includes an .xcdatamodeld file and the AppDelegate.swift file with Core Data Stack code.

Add a Core Data Model to an Existing Project

Choose File →New →File and select the iOS platform tab. Scroll down to the Core Data section, select Data Model, and click Next and save it to the desired directory. Let’s save it in the Models folder.
Xcode adds an MyNotes.xcdatamodeld file in the Models directory.

Data Modeling: Add Entities and Attributes

The next step is to create a data structure that will hold our notes. A Note class looks like this:

class Note {
    let id = UUID()
    var text: String = ""
    var lastUpdated: Date = Date()
}

We will create an entity for this Note class. An entity describes an object, including its name, attributes, and relationships. Create an entity for each of your app's objects.

  1. Click Add Entity at the bottom of the editor area. A new entity with a placeholder name Entity appears in the Entities list.

  2. In the Entities list, double-click the newly added entity and rename it as Note. This step updates both the entity name and class name visible in the Data Model inspector.

  3. After you create an entity, you can add attributes to that entity. An attribute describes a property of an entity. Add id,text,lastUpdated as attributes to Note entity.

Generating Code

Model, specify the classes that you’ll use to create instances of your entities. Core Data optionally generates two files to support your class: a class file and a properties file.

To select a code generation option:

  1. Select an entity from the Entities list.

  2. In the Data Model inspector, below Class, the Codegen pop-up menu offers three options: Manual/None, Class Definition, and Category/Extension.

  3. Choose Manual/None and from the Xcode menu bar, choose Editor -> create NSManagedObject Subclass -> select MyNotes as Data Model → select Note as the entity to manage → choose Models directory.

The Xcode will generate two files CoreDataClass and CoreDataProperties. CoreDataProperties file is sort of the representation of our data model(entity) which will have attributes and relationship properties. CoreDataClass is a class that is available to us to extend or add some properties to the Note class.

— While selecting Manual configuration, if we change any property to Note entity, we manually have to configure those changes in these two files.

— In Class Definition configuration, Xcode will automatically generate the required NSManagedObject subclass as part of the project’s derived data. If we have to change anything, Xcode automatically syncs these files with the corresponding entity. But, we will not have access to use the class file and extend its properties.

— In Category/Extension configuration, Xcode will only automatically generate CoreDataProperties.swift for you and you will have to manage and maintain CoreDataClass.swift yourself. So, we can use this configuration without worrying about the entity properties.

Creating the Core Data Manager

While including a CoreData at project initialization, you will see some extra code added there like persistent container in AppDelegates. Since we added CoreData after our project initialization and we don’t want to clutter the application delegate with the setup of the Core Data stack. we are going to create a separate class that is responsible for setting up and managing the Core Data stack.

  1. Create a new Swift file in the Models group and name the file CoreDataManager. This is the class that will be in charge of the Core Data stack of the application.

  2. We start by adding an import statement for the CoreData framework and define a singleton CoreDataManager class.

  3. Now let's dig in AppDelegate.swift file and discuss code. The AppDelegate file contains application life cycle methods and code stubs related to core data.

  4. Initialize NSPersistentContainer class and thus core data stack objects (Managed object Model, PersistentStoreCoordinator, Managed Object Context).

  5. A method named save(). It saves managed object models in to store.

  6. Application life cycle method named applicationWillTerminate calls saveContext() also to save data in-store when the app is about to terminate.

The below code creates a NSPersistentContainer object. An instance of NSPersistentContainer includes all objects needed to represent a functioning Core Data stack.**NSPersistentContainer is a container that encapsulates the Core Data stack (Heart of the core data) in your application. NSPersistentContainer simplifies the creation/initialization and management of the Core Data stack by handling the creation of the below objects:-
⊛ managed object model (NSManagedObjectModel),
⊛ persistent store coordinator (NSPersistentStoreCoordinator),
⊛ and the managed object context (NSManagedObjectContext).

import Foundation
import CoreData

class CoreDataManager {
    static var shared = CoreDataManager(modelName: "MyNotes")
    let persistentContainer: NSPersistentContainer

    private init(modelName: String){
        persistentContainer = NSPersistentContainer(name: modelName)
    }

    // Core Data stack
    var viewContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }

    func load(completion : (() -> Void)? = nil) {
        persistentContainer.loadPersistentStores { (description, error) in
            guard error == nil else {
                fatalError("Unresolved error \(error?.localizedDescription)")
            }
            completion?()
        }
    }

    func save() {
        if viewContext.hasChanges {
            do {
                try viewContext.save()
            }
            catch {
                print("An error occured while saving \(error.localizedDescription)")
            }
        }
    }
}

Now add the following code in didFinishLaunchingWithOptions in AppDelegate to load our data store.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {        
    ...
    CoreDataManager.shared.load()
    ...
}

In the above code, persistentContainer = NSPersistentContainer(name: modelName) Initializes a persistent container with the given name "MyNotes". Where parameters (name :"MyNotes") are the name of the NSPersistentContainer object and return a persistent container initialized with the given name.

When persistent containers have been initialized, we need to execute load to instruct the container to load the persistent stores and complete the creation of the Core Data stack. Once the completion handler has fired, the stack is fully initialized and is ready for use.

storeDescription → As the name of the class suggests, NSPersistentStoreDescription class encapsulates the information and configuration to add a persistent store to the persistent store coordinator. In other words, it describes a persistent store.

var viewContext: NSManagedObjectContext {
    return persistentContainer.viewContext
}

In above code, we are calling the persistent container’s view context object. This view context is the name of the property of the type ManagedObjectContext class. Function save looks for the changes made on the managed object model. If there is any change we call viewContext.save() method of context object to finally save our data.

Create, Fetch and Delete Note in CoreData

extension CoreDataManager {
    func createNote() -> Note {
        let note = Note(context: viewContext)
        note.id = UUID()
        note.lastUpdated = Date()
        note.text = ""
        save()
        return note
    }
}

Let’s create a method createNote() that will initialize a Note object. We can’t just simply initialize a new note as let note = Note(), we need to initialize it with a context. And after initialization, we call save() function to save the changes in our viewContext.

func fetchNotes(filter: String? = nil) -> [Note] {
    let request: NSFetchRequest<Note> = Note.fetchRequest()
    if let filter = filter {
        let predicate = NSPredicate(format: "text contains[cd] %@", filter)
        request.predicate = predicate
    }
    let sortDescriptor = NSSortDescriptor(keyPath: \Note.lastUpdated, ascending: false)
    request.sortDescriptors = [sortDescriptor]
    return (try? viewContext.fetch(request)) ?? []
}

Method fetchNotes() will return an array of Notes. First, we create a request of type NSFetchRequest and assigning equal to Note.fetchRequest() which is a class method in Properties file.

To filter the contents of the Notes, we will use NSPredicate Predicates are simple tests, which are used to filter out the data which you need in our resultant array of data. The test will be applied to every object in your Core Data entity. Next thing we want to do is sort which one will be most recently updated notes. We create a sortDescriptor of type NSSortDescriptor and add it to the request and in last return viewContext.fetch

func deleteNote(note: Note){
    viewContext.delete(note)
    save()
}

Method deleteNote(), we will pass a note object to viewContext.delete()We can call these methods for CRUD operations according to our needs.

💡 Core Data provides on-disk persistence, which means your data will be accessible even after terminating your app or shutting down your device. This is different from in-memory persistence, which will only save your data as long as your app is in memory, either in the foreground or in the background.

Summary

In summary, UserDefault is ideal for storing small, non-sensitive data such as user preference, while KeyChain provides secure storage for sensitive information. On the other hand, CoreData is a comprehensive framework for managing complex data models, offering more advanced features and scalability compared to UserDefaults and Keychain.

For reading references, I have found these articles very useful and informative:-

Thanks for Reading!
If you liked this article, please like so other people can read it too :)