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 about 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 defaults 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
, orNSDictionary
. 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 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 utilised 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 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 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 amount of data into UserDefaults as it can affect performance of your app significantly.
KeyChain
As perviously explained, UserDefaults saves data into a plist file. User can get access to Library/Preferences folder of their iPhone and read or modify the UserDefaults plist data easily (eg: user can change the boolean value of “isPremiumUser” from false to true, or change the value of trails 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 item's:
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 askSecClassGenericPassword
indicating that the data we are saving is a generic password item.kSecAttrService
andkSecAttrAccount
: These 2 keys are mandatory whenkSecClass
is set tokSecClassGenericPassword
. 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 setkSecAttrService
as “password-token” andkSecAttrAccount
as “abc@gmail.com“.
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 above method
save(data:service:account)
, we first create the query, then we call theSecItemAdd(_:_:)
method to save the data to the keychain. TheSecItemAdd(_:_:)
method will then return anOSStatus
that indicates the status of the save operation. If we get theerrSecSuccess
status, it 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
andkSecAttrAccount
value, we won’t be able to save the access token to the keychain, and will get error because keys that we use already exist in the keychain. Let’s update our previoussave(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 ofkSecAttrService
andkSecAttrAccount
. But this time, we will have to create another dictionary that consists ofkSecValueData
and feed it to theSecItemUpdate(_:_:)
method.SecItemUpdate
expects the new value of the Keychain item to be passed inside a different argumentattributesToUpdate
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 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 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, to cache temporary data, and to add undo functionality to your app on a single device.
💡 Remember that Core Data isn’t a database nor it consists of 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 desired directory. Let’s save it in 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 look like this:
class Note {
let id = UUID()
var text: String = ""
var lastUpdated: Date = Date()
}
We will create a 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.
- Click Add Entity at the bottom of the editor area. A new entity with placeholder name Entity appears in the Entities list.
- 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. - After you create an entity, you can add attributes to that entity. An
attribute
describes a property of an entity. Addid
,text
,lastUpdated
as attributes toNote
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:
- Select an entity from the Entities list.
- In the Data Model inspector, below Class, the Codegen pop-up menu offers three options: Manual/None, Class Definition, and Category/Extension.
- Choose
Manual/None
and from the Xcode menu bar, chooseEditor
-> createNSManagedObject Subclass
-> selectMyNotes
as Data Model → selectNote
as the entity to manage → chooseModels
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 property. CoreDataClass is a class that is available to us extend or add some properties to the Note class.
— While selecting
Manual configuration
, if we change any property toNote
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 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 initialisation, you will see some extra code added there like persistent container in AppDelegates. Since, we added CoreData after our project initialisation 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.
- Create a new Swift file in the
Models
group and name the fileCoreDataManager
. This is the class that will be in charge of the Core Data stack of the application. - We start by adding an import statement for the
CoreData
framework and define a singletonCoreDataManager
class. - Now lets dig in
AppDelegate.swift
file and discuss about code. The AppDelegate file contains application life cycle methods and code stubs related to core data. - Initialize NSPersistentContainer class and thus core data stack objects (Managed object Model, PersistentStoreCoordinator, Managed Object Context).
- A method named save(). It saves managed object models in to store.
- Application life cycle method named applicationWillTerminate calls saveContext() also to save data in store when 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 below objects:-
⊛ managed object model (NSManagedObjectModel),
⊛ persistent store coordinator (NSPersistentStoreCoordinator),
⊛ and the managed object context (NSManagedObjectContext).
// CoreDataManager.swift
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") is the name of the NSPersistentContainer object and return a persistent container initialized with the given name.
When persistent container 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 persistent container’s view context object. This view context is name of property of type ManagedObjectContext class. Function save
looks for the changes made on 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 a 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 this articles very useful and informative :-
- UserDefaults in Swift
- Secure Data With Keychain In Swift
- Getting Started Core Data (CRUD) with Swift
- Featured | Apple Developer Documentation
Thanks for Reading!
If you liked this article, please 👏 so other people can read it too :)