The UserDefaults
class makes it straightforward to store data in the user's defaults database, but remember that only strings, numbers, Date
objects, and Data
objects are supported by the defaults system. Is it possible to store a custom object in the user's defaults database? It is, but it requires a bit of additional work.
Several Options
You have several options if you want to store a custom object in the defaults database, but the idea is similar. The custom object needs to be converted to a data type that is supported by the defaults system.
The solution I discuss in this post converts the custom object to binary data using the Codable
type. This isn't difficult and it can be applied in a wide range of scenarios.
Encoding and Decoding
The Codable
type is a type alias for the Encodable
and Decodable
protocols. This means that any type conforming to Codable
can be encoded and decoded. Let me show you an example of this technique.
Let's assume we are building an application to create notes. The notes the user creates are stored in the user's defaults database. We define a struct, Note
, with three properties, id
of type Int,
titleof type
String, and
bodyof type
String`.
import Foundation
struct Note {
// MARK: - Properties
let id: Int
// MARK: -
let title: String
let body: String
}
The next step is conforming the Note
struct to the Encodable
and Decodable
protocols. Remember that Codable
is a type alias for the Encodable
and Decodable
protocols.
import Foundation
struct Note: Codable {
// MARK: - Properties
let id: Int
// MARK: -
let title: String
let body: String
}
We don't need to implement methods or define properties because the properties of the Note
struct also conform to the Encodable
and Decodable
protocols.
Writing/Setting a Custom Object To User Defaults
To store a Note
object in the defaults database, we need to encode it. This simply means that the Note
object is converted to a Data
object. Let's start by creating a Note
object.
import Foundation
struct Note: Codable {
// MARK: - Properties
let id: Int
// MARK: -
let title: String
let body: String
}
// Create Note
let note = Note(id: 1, title: "My Note", body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
The next step is using the JSONEncoder
class to encode the Note
object. Because encoding an object is a throwing operation, we encode the Note
object in a do-catch
statement.
import Foundation
struct Note: Codable {
// MARK: - Properties
let id: Int
// MARK: -
let title: String
let body: String
}
// Create Note
let note = Note(id: 1, title: "My Note", body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
do {
// Create JSON Encoder
let encoder = JSONEncoder()
// Encode Note
let data = try encoder.encode(note)
} catch {
print("Unable to Encode Note (\(error))")
}
The last step is storing the Data
object returned by the encode(_:)
method in the defaults database. This should look familiar.
import Foundation
struct Note: Codable {
// MARK: - Properties
let id: Int
// MARK: -
let title: String
let body: String
}
// Create Note
let note = Note(id: 1, title: "My Note", body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
do {
// Create JSON Encoder
let encoder = JSONEncoder()
// Encode Note
let data = try encoder.encode(note)
// Write/Set Data
UserDefaults.standard.set(data, forKey: "note")
} catch {
print("Unable to Encode Note (\(error))")
}
Reading/Getting a Custom Object From User Defaults
Retrieving the custom object from the defaults database isn't difficult either. We ask the shared defaults object for the Data
object and decode it. In other words, we convert the Data
object to a Note
object.
The first step is getting the Data
object from the defaults database. This is straightforward. We use an if
statement and optional binding to safely unwrap the value returned by the data(forKey:)
method.
import Foundation
// Read/Get Data
if let data = UserDefaults.standard.data(forKey: "note") {
}
Because decoding a Data
object is a throwing operation, we perform this task in a do-catch
statement. We create an instance of the JSONDecoder
class and invoke the decode(_:)
method, passing in the resulting type and the Data
object.
import Foundation
// Read/Get Data
if let data = UserDefaults.standard.data(forKey: "note") {
do {
// Create JSON Decoder
let decoder = JSONDecoder()
// Decode Note
let note = try decoder.decode(Note.self, from: data)
} catch {
print("Unable to Decode Note (\(error))")
}
}
That's it. If you need to perform this task in several places in your project, I recommend creating an extension for the UserDefaults
class to make that task easier. This avoids code duplication and it also results in a cleaner API. You can read more about this technique in this post.
Storing an Array of Custom Objects in User Defaults
We laid the foundation to store an array of custom objects in the defaults database. Let's update the first example. We create a note and store the Note
object in an array. This gives us an array of Note
objects. It doesn't matter whether the array contains zero or a dozen Note
objects.
import Foundation
struct Note: Codable {
// MARK: - Properties
let id: Int
// MARK: -
let title: String
let body: String
}
// Create Note
let note = Note(id: 1, title: "My Note", body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
// Create Array of Notes
let notes = [note]
In the do-catch
statement, we pass the notes
constant to the encode(_:)
method of the JSONEncoder
class and store the resulting Data
object in the defaults database. We use the key notes
.
import Foundation
struct Note: Codable {
// MARK: - Properties
let id: Int
// MARK: -
let title: String
let body: String
}
// Create Note
let note = Note(id: 1, title: "My Note", body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
// Create Array of Notes
let notes = [note]
do {
// Create JSON Encoder
let encoder = JSONEncoder()
// Encode Note
let data = try encoder.encode(notes)
// Write/Set Data
UserDefaults.standard.set(data, forKey: "notes")
} catch {
print("Unable to Encode Array of Notes (\(error))")
}
To retrieve the array of notes from the defaults database, we ask the shared defaults object for the Data
object. We then use a JSONDecoder
instance to convert the Data
object to an array of Note
objects in the do-catch
statement.
import Foundation
// Read/Get Data
if let data = UserDefaults.standard.data(forKey: "notes") {
do {
// Create JSON Decoder
let decoder = JSONDecoder()
// Decode Note
let notes = try decoder.decode([Note].self, from: data)
} catch {
print("Unable to Decode Notes (\(error))")
}
}
Notice that we also updated the first argument of the decode(_:from:)
method. We expect an array of Note
objects, not a single Note
object.
As I mentioned earlier, if you need to perform this task in several places in your project, I recommend creating an extension for the UserDefaults
class to make that task easier. You can read more about this technique in this post.