Custom Object in UserDefaults : Swift


In continuation to my previous blog UserDefaults in Swift, where we understand the basic functionality of UserDefaults, we'll try to understand today how can we store custom objects in UserDefaults.

UserDefaults provides us with direct functions for storing primitive data types like Int, Double, Bool, and String. But for custom data types, there is no direct way to store them in UserDefaults. But there is a workaround with which we can store the custom object to UserDefaults. Let's try to understand in detail along with an example.

What are Custom Objects and Codable?

User-defined class or struct objects are known as custom objects. These classes or structs can have any number of properties.

To store them in UserDefaults, we need to implement or confirm Codable to the user-defined class or struct.

Codable is a typealias of Decodable & Encodable protocols. It adds the functionality of Encoding and Decoding to the class or struct.
  
/// A type that can convert itself into and out of an external representation.
///
/// `Codable` is a type alias for the `Encodable` and `Decodable` protocols.
/// When you use `Codable` as a type or a generic constraint, it matches
/// any type that conforms to both protocols.
public typealias Codable = Decodable & Encodable
To implement or confirm the Codable protocol, we can add the protocol and go with coding keys as the variable names or can define our own custom keys according to the need.
  
//MARK :- Employee
struct Employee: Codable {
let employeeId: Int
let name, department: String
}

// OR

//MARK :- Employee
struct Employee: Codable {
let employeeID: Int
let name, department: String

enum CodingKeys: String, CodingKey {
case employeeID = "employeeId"
case name, department
}
}
Keep in mind that any custom class used inside the Codable class or struct should also confirm the Codable protocol.

How to store custom object in UserDefaults?

Codable provides the functionality to convert an object into a Data class object.

UserDefaults has a function similar to primitive data types which takes an Any? class object and store the value against the key provided.
  
/**
-setObject:forKey: immediately stores a value (or removes the value if nil is passed as the value) for the provided key in the search list entry for the receiver's suite name in the current user and any host, then asynchronously stores the value persistently, where it is made available to other processes.
*/
open func set(_ value: Any?, forKey defaultName: String)
We can't provide the object directly but we need to convert the object into a Data class object using JSONEncoder, else it will throw the below error.
  
Attempt to insert non-property list object <object name> for key <key>
Also, since JSONEncoder can throw an error if the object is not encoded properly, we need to wrap it around the try-catch block.

Once the codable is converted to the Data class object, we can put it into UserDefaults using the above set method.
The final code will look like the below.
  
let employee = Employee(employeeId: 100,
name: "Suneet Agrawal",
department: "Engineering")

do {
let data = try JSONEncoder().encode(employee)
UserDefaults.standard.set(data, forKey: "employee")
} catch let error {
print("Error encoding: \(error)")
}

How to retrieve custom object from UserDefaults?

UserDefaults has a function to retrieve Data class objects similar to any other primitive data type.
The same can be used to retrieve data from UserDefaults but the returned result will be in the Data? class object.
  
/// -dataForKey: is equivalent to -objectForKey:, except that it will return nil if the value is not an NSData.
open func data(forKey defaultName: String) -> Data?
Keep in mind that it can also return nil if no such data is stored with the key. In that case, we need to add a null safety also.
  
if let data = UserDefaults.standard.data(forKey: "employee") {
//do something here
}

Now that we got the data back, we can convert it into the same original class or struct object (Employee in our case) using JSONDecoder.

But similar to JSONEncoder, JSONDecoder can also throw an error if the object is not decoded properly, we need to wrap it around the try-catch block.
  
do {
if let data = UserDefaults.standard.data(forKey: "employee") {
let employee = try JSONDecoder().decode(Employee.self, from: data)
print(employee)
}
} catch let error {
print("Error decoding: \(error)")
}

Generic Extension

We can even add a generic functionality to the UserDefaults class which will store the Codable class or struct object of Template type and retrieve back the same object using the same key.

The above functions can be used as below.
  
let employee = Employee(employeeId: 100,
name: "Suneet",
department: "Engineering")

//setting the object
UserDefaults.standard.storeCodable(employee, key: "employee")

//getting the object
let emp : Employee? = UserDefaults.standard.retrieveCodable(for: "employee")

print(emp)
//this will print optional

if let emp = emp {
print(emp)
}