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.