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.

How to Store a Custom Object in User Defaults in Swift

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 typeString, andbodyof typeString`.

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.