A type-safe UserDefaults in Swift

Luca Angeletti
The Startup
Published in
3 min readSep 17, 2019

--

As an iOS developer, you probably resort to UserDefaults all the time to save and retrieve local data.

// This is how you save a value
UserDefaults.standard.set("123", forKey: "userID")
// This is how you read it
let userID = UserDefaults.standard.string(forKey: "userID")

However, interacting with UserDefaults in a big project can be risky, and the following errors may happen:

  • Wrong key
  • Wrong type
  • Keys overlapping

Wrong key ⚠️

We could save a value with a key and try to read it with a different key.

UserDefaults.standard.set("123", forKey: "userID")
...
let userID = UserDefaults.standard.string(forKey: "UserID")

Since the key is case sensitive userID is different from UserID. This seems like an easy error to avoid, but in a big project with many keys, it could happen.

Wrong type ⚠️

Each time we interact with UserDefaults, we must remember the type of the value we are saving and loading.

The following example contains an error because we are saving the userID as a String but then we try to retrieve it as an Int.

UserDefaults.standard.set("123", forKey: "userID")
...
let userID = UserDefaults.standard.int(forKey: "userID")

Again, this seems something easy to spot in a code snipped, but in a large project, it could happen.

Keys overlapping ⚠️

We could forget we are using a given key for a value in another part of the project and decide to use the same key for another value.

let currentUserID = "123"
UserDefaults.standard.set(currentUserID, forKey: "userID")
// and then in another file of our project 👇
let messageRecipientUserID = "456"
UserDefaults.standard.set(messageRecipientUserID, forKey: "userID")

A safer way

Can we improve UserDefaults to address the three issues discussed above?

This is a problem we can solve with Computed Properties.

In addition to stored properties, classes, structures, and enumerations can define computed properties, which do not actually store a value. Instead, they provide a getter and an optional setter to retrieve and set other properties and values indirectly. — The Swift Programming Language

Let’s add the following extension to our project

extension UserDefaults {var userID: String? {
get {
return string(forKey: #function)
}
set {
set(newValue, forKey: #function)
}
}
}

The userID Computed Property does not store any value in the current instance of UserDefaults. Instead, it provides a getter (which retrieves the value from local storage) and a setter (which writes the value to local storage).

What is #function?

It is a special keyword and is automatically replaced with the name of the function that contains it.

E.g., in our example, it is replaced with the String userID.

This will make sure the key we use to save (and retrieve) the value to UserDefaults is the name of the property we defined.

Test 🔨

Let’s run a quick test

UserDefaults.standard.userID = "123"if let userID = UserDefaults.standard.userID {
print(userID)
}
> 123

Cool right? 🎉

If we define a computed property for each key/value, we want to store into UserDefaults, we automatically solve the three issues discussed above.

  1. Wrong key: ✅ fixed because the compiler will not allow us to use an undefined property.
  2. Wrong type: ✅ fixed because each computed property has a type.
  3. Keys overlapping: ✅ fixed because now the key is the name of the computed property and the Swift compiler will make sure every property defined in UserDefaults is unique.

Conclusion

There’s one last benefit from this approach: encapsulation. We are now hiding (into our extension) some details about how the value is saved. Any caller can simply use this property without worrying about the low-level details.

--

--