You are currently viewing Create the Perfect UserDefaults Wrapper Using Property Wrapper

Create the Perfect UserDefaults Wrapper Using Property Wrapper

Imagine you have an app and you want to implement auto login functionality for your app. Thus you create an UserDefaults wrapper to encapsulate the UserDefaults read and write logic. You will use the UserDefaults wrapper to keep track on the auto login “On” / “Off” status, as well as the user’s username. This is how your UserDefaults wrapper usually looks like:

struct AppData {
    
    private static let enableAutoLoginKey = "enable_auto_login_key"
    private static let usernameKey = "username_key"
    
    static var enableAutoLogin: Bool {
        get {
            // Read from UserDefaults
            return UserDefaults.standard.bool(forKey:enableAutoLoginKey)
        }
        set {
            // Save to UserDefaults
            UserDefaults.standard.set(newValue, forKey: enableAutoLoginKey)
        }
    }
    
    static var username: String {
        get {
            // Read from UserDefaults
            return UserDefaults.standard.string(forKey: usernameKey) ?? ""
        }
        set {
            // Save to UserDefaults
            UserDefaults.standard.set(newValue, forKey: usernameKey)
        }
    }
}

With Property Wrapper introduced in Swift 5.1, you can simplify your UserDefaults wrapper into this:

struct AppData {
    
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var enableAutoLogin: Bool
    
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
}

Pretty awesome right? Wanted to know more? Just read on…


What is Property Wrapper?

Before we get into the details, let’s have a quick introduction on what is property wrapper.

Basically, a property wrapper is a generic data structure that can intercept the property’s read / write access, thus enabling custom behaviour being added during the property’s read / write operation.

To define a property wrapper, you can use the keyword @propertyWrapper. Let’s say you want to have a string type property that every time it is being read or write, a console log will be printed. You can create a property wrapper named Printable as shown below:

@propertyWrapper
struct Printable {
    private var value: String = ""

    var wrappedValue: String {
        get {
            // Intercept read operation & print to console
            print("Get value: \(value)")
            return value
        }
        set {
            // Intercept write operation & print to console
            print("Set value: \(newValue)")
            value = newValue
        }
    }
}

As you can see from the code above, property wrapper is just like any other struct in Swift. However a wrappedValue is compulsory when defining a property wrapper. The wrappedValue get and set block is where you can intercept and perform the operation you desire. In this example, a print statement is added to print out the value being get or set.

Here’s how you can use the Printable property wrapper:

struct Company {
    // Create a string type variable that wrapped by Printable property wrapper
    @Printable static var name: String
}

Company.name = "Adidas" // Trigger set value
Company.name // Trigger get value

Note that how we use the @ symbol to declare the ‘name’ variable that wrapped by property wrapper. If you try out above code in your Xcode Playground, you will see console output as shown below:

Set value: Adidas
Get value: Adidas

The UserDefaults Wrapper

After understanding how property wrapper works, we are now ready to start implementing our UserDefaults wrapper. To recap, our property wrapper need to keep track on the auto login “On” / “Off” status, as well as the user’s username.

By using the concept we discussed above, you can easily convert the Printable property wrapper into property wrapper that will write to / read from UserDefaults during a property read / write operation.

import Foundation

@propertyWrapper
struct Storage {
    private let key: String
    private let defaultValue: String
    
    init(key: String, defaultValue: String) {
        self.key = key
        self.defaultValue = defaultValue
    }
    
    var wrappedValue: String {
        get {
            // Read value from UserDefaults
            return UserDefaults.standard.string(forKey: key) ?? defaultValue
        }
        set {
            // Set value to UserDefaults
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

Here we named our property wrapper Storage. It has 2 properties which is key and defaultValue. key will be the key to use when reading and writing to UserDefaults, and defaultValue is the value to return when there is no value in UserDefaults.

With the Storage property wrapper ready, we can start implementing the UserDefaults wrapper. It is pretty straightforward, we just need to create a username variable that is wrapped by the Storage property wrapper. Do note how you can initialise the Storage property wrapper with key and defaultValue.

// The UserDefaults wrapper
struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
}

With all that, the UserDefaults wrapper is finally ready to use. Let’s see it in action:

// Write username to UserDefaults
AppData.username = "swift-senpai"

// Read username from UserDefaults & print out "swift-senpai"
print(AppData.username)

At this point, let’s try to add the enableAutoLogin variable into our UserDefaults wrapper.

struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
    
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var enableAutoLogin: Bool
}

However, you will notice that the following 2 errors occurred.

  • Cannot convert value of type ‘Bool’ to expected argument type ‘String’
  • Property type ‘Bool’ does not match that of the ‘wrappedValue’ property of its wrapper type ‘Storage’

This is because our property wrapper currently only support String data type. In order to fix both errors, we will have to make our property wrapper generic.


Making The Property Wrapper Generic

To make the property wrapper generic, we have to change property wrapper’s wrappedValue data type from String to a generic type T. Furthermore, we will have to update the wrappedValue get block to use a generic way to read from UserDefaults. Here’s the updated property wrapper:

@propertyWrapper
struct Storage<T> {
    private let key: String
    private let defaultValue: T

    init(key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            // Read value from UserDefaults
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            // Set value to UserDefaults
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

With a generic property wrapper, our UserDefaults wrapper can now store a boolean value without any problem.

// The UserDefaults wrapper
struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
    
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var enableAutoLogin: Bool
}

AppData.enableAutoLogin = true
print(AppData.enableAutoLogin)  // true

Storing Custom Object

At this point, our UserDefaults wrapper is able to store any basic data types such as String, Bool, Int, Float, Array, etc. But what if we need to store a custom object? Currently we will encounter an error if we try to store a custom object. In this section, let’s make our UserDefaults wrapper more awesome by enabling it to support custom object.

The concept here is simple, we will store the custom object as data in UserDefaults. In order to achieve that, we must update the Storage property wrapper generic type T to conform to the Codable protocol.

After that, in the wrappedValue set block we will use JSONEncoder to convert the custom object to data and write it to UserDefaults. Meanwhile in the wrappedValue get block, we will use JSONDecoder to convert the data we retrieved from UserDefaults back to the desired data type.

Here’s the updated Storage property wrapper:

@propertyWrapper
struct Storage<T: Codable> {
    private let key: String
    private let defaultValue: T

    init(key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            // Read value from UserDefaults
            guard let data = UserDefaults.standard.object(forKey: key) as? Data else {
                // Return defaultValue when no data in UserDefaults
                return defaultValue
            }

            // Convert data to the desire data type
            let value = try? JSONDecoder().decode(T.self, from: data)
            return value ?? defaultValue
        }
        set {
            // Convert newValue to data
            let data = try? JSONEncoder().encode(newValue)
            
            // Set value to UserDefaults
            UserDefaults.standard.set(data, forKey: key)
        }
    }
}

To see how you can use the updated Storage property wrapper, let’s take a look at the following example.

Imagine you need to store the user information return by server side after a user successfully login. First, you will need a struct to hold the user information return by server. The struct must conform to the Codable protocol so that it can be converted to data and store into UserDefaults.

struct User: Codable {
    var firstName: String
    var lastName: String
    var lastLogin: Date?
}

Next step is to declare a User object in the UserDefaults wrapper:

struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String

    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var enableAutoLogin: Bool
    
    // Declare a User object
    @Storage(key: "user_key", defaultValue: User(firstName: "", lastName: "", lastLogin: nil))
    static var user: User
}

That’s it! The UserDefaults wrapper is now able to store custom object. 🎉

let johnWick = User(firstName: "John", lastName: "Wick", lastLogin: Date())

// Set custom object to UserDefaults wrapper
AppData.user = johnWick

print(AppData.user.firstName) // John
print(AppData.user.lastName) // Wick
print(AppData.user.lastLogin!) // 2019-10-06 09:40:26 +0000

Storing Encrypted String

We have gone a long way by making our UserDefaults wrapper generic and able to store basically anything that we desire. But wait, what if you need to store the user password or any sensitive data using the UserDefaults wrapper? Currently all the string that being stored in our UserDefaults wrapper are plain text, and we all know that storing passwords as plain text is an extremely bad practice!

To go about this, we can use the concept that we have just discussed, create another property wrapper that will encrypt its value before setting it into UserDefaults. We will call this property wrapper EncryptedStringStorage.

@propertyWrapper
struct EncryptedStringStorage {

    private let key: String

    init(key: String) {
        self.key = key
    }

    var wrappedValue: String {
        get {
            // Get encrypted string from UserDefaults
            return UserDefaults.standard.string(forKey: key) ?? ""
        }
        set {
            // Encrypt newValue before set to UserDefaults
            let encrypted = encrypt(value: newValue)
            UserDefaults.standard.set(encrypted, forKey: key)
        }
    }

    private func encrypt(value: String) -> String {
        // Encryption logic here
        return String(value.reversed())
    }
}

For demo purpose, the encryption we do here is just a simple operation of reversing the entire string. The way to use the EncryptedStringStorage property wrapper is fairly straightforward:

struct AppData {
    @EncryptedStringStorage(key: "password_key")
    static var password: String
}

AppData.password = "password1234"
print(AppData.password) // 4321drowssap

Wrapping Up

By using property wrapper introduced in Swift 5.1, we have reduced a lot of boilerplate code in our UserDefaults wrapper. Furthermore, both Storage property wrapper and EncryptedStringStorage property wrapper can also be re-use in other projects. Next time when you need to create a UserDefaults wrapper, give the property wrapper method a shot, I am sure you will like it. 🙂

You can find the full source code here.


[Updated: 20 January 2020]

If you would like to know how to store an optional values into UserDefaults using property wrapper. Checkout this awesome article at swiftbysundell.com.

[Updated: 5 February 2020]

One of the reader, Guillaume, has reported that he is facing unexpected crash when using the UserDefaults wrapper to store an enum on macOS Mojave. However, everything works fine in Catalina. It seems like it might be a bug in the JSONEncoder framework.

To workaround the problem, he put the enum in a struct and store the struct in UserDefaults instead.

Once again, I would like to thank Guillaume for sharing this with me. 🙂

[Updated: 26 April 2020]

A caveat discovered by Donny Wals when using property wrapper with collection types asynchronously.

Why your @Atomic property wrapper doesn’t work for collection types

I think all developers should take note of this.


Hope you find this article useful. Please leave a comment if you have any questions or thoughts regarding the UserDefaults wrapper. If you like this article, feel free to share it.

Thanks and happy coding! 👨🏼‍💻


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.