Published on

Safer and cleaner UserDefaults and AppStorage

If you’ve ever written code for an iOS or macOS application you’d have, for sure, encountered User Defaults. Still, for the sake of the structure of the article, here’s a textbook definition of what UserDefaults is:

UserDefaults: An interface to the user’s defaults database, where you store key-value pairs persistently across launches of your app.

The problem

UserDefaults is a key-value pair store where you can set and retrieve elements from persistent memory using a “key”. I detest using hard-coded strings for anything in my application. I try to avoid stringly typed values until it’s necessary. The UIKit framework and Swift language in general do a very good job of avoiding unnecessary string matching (Foundation still has it in a few places such as HTTP methods like “POST” or “GET”). According to me, there are a few issues with using string matching in our code- spelling mistakes, casing mismatch, and might I argue that it looks really out of place in our codebase.

Currently, to set a value to the UserDefaults store you’d have to write something like this:

UserDefaults.standard.set(true, forKey: "isLoggedIn")

This code sets true to a key isLoggedIn and stores it in the UserDefaults store. We’d usually use something like this when we have to check if the user has logged in to then show them a different set of views compared to if they haven’t logged in.

To access the value you’d have to do this:

UserDefaults.standard.bool(forKey: "isLoggedIn")

This works fine and is how UserDefaults is meant to be accessed. But, I’d like to argue that this method is highly unsafe. For starters, it is extremely easy to make a typo while typing out the key. Additionally, if another developer joins your team or contributes to your open source project and they’re used to using snake case they might mistakenly type “is_logged_in” out of habit. Finally, it’s hard to keep a track of what keys go where and ultimately you might end up with a mess of strings in your codebase.

Older Solution

During my work at VideoLAN, I learned to use a struct for storing constants. The struct would have constants for the keys equalling to the string value. Something like this:

struct Constants {
	static let isLoggedIn = "isLoggedIn"
}

Then, you’d use it to access UserDefaults value like this:

UserDefaults.standard.bool(forKey: Constants.isLoggedIn)

My solution

Alright, enough build-up. I’ve built an extension on UserDefaults to make this whole process very safe. First, we create an enum called UserDefaultKey. Here, we create each of our keys as a case.

enum UserDefaultsKey: String {
    case isLoggedIn
}

We conform UserDefaultKey to String so that we get access to the raw value as a String. In this case, even if we don’t provide an explicit rawValue, Swift will be able to generate one from the associated case.

Now, we create the extension. Here, we create a kind of an overload that, instead of accepting a forKey value, we make it accept a key of type UserDefaultKey .

extension UserDefaults {

    func set(_ value: Any, for key: UserDefaultsKey) {
        self.set(value, forKey: key.rawValue)
    }

    func bool(for key: UserDefaultsKey) -> Bool {
        return self.bool(forKey: key.rawValue)
    }

    func data(for key: UserDefaultsKey) -> Data? {
        return self.data(forKey: key.rawValue)
    }

    func string(for key: UserDefaultsKey) -> String? {
        return self.string(forKey: key.rawValue)
    }

    func integer(for key: UserDefaultsKey) -> Int? {
        return self.integer(forKey: key.rawValue)
    }

    func float(for key: UserDefaultsKey) -> Float? {
        return self.float(forKey: key.rawValue)
    }

    func url(for key: UserDefaultsKey) -> URL? {
        return self.url(forKey: key.rawValue)
    }

    func value(for key: UserDefaultsKey) -> Any? {
        return self.value(forKey: key.rawValue)
    }
}

We can then use this extension like this

UserDefaults.standard.bool(for: .isLoggedIn)

Additional- AppStorage

We can use the same principle for the new property wrapper introduced in iOS 14. Creating an extension first:

extension AppStorage {

    init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == Bool {
        self.init(wrappedValue: wrappedValue, key.rawValue, store: store)
    }

    init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == Int {
        self.init(wrappedValue: wrappedValue, key.rawValue, store: store)
    }

    init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == Double {
        self.init(wrappedValue: wrappedValue, key.rawValue, store: store)
    }

    init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == URL {
        self.init(wrappedValue: wrappedValue, key.rawValue, store: store)
    }

    init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == String {
        self.init(wrappedValue: wrappedValue, key.rawValue, store: store)
    }

    init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == Data {
        self.init(wrappedValue: wrappedValue, key.rawValue, store: store)
    }
}

And then using the extension like this:

@AppStorage(.isLoggedIn) var isLoggedIn = false

Conclusion

In this article, we learned some new ways to work with UserDefaults. We built an extension on UserDefaults and AppStorage and along with an enum-based structure for storing our keys, we were able to make the process super Swifty, safe, and clean.