Type-Safe UserDefaults API

For more on this subject, check out my related post:
https://152percent.com/blog/2018/7/3/dynam...

As the Swift APIs have evolved, Apple has also been improving their own frameworks to make them easier to work with in Swift. Two API improvements I've personally appreciated are how we work with NSAttributedString keys, and Notification names.

Deriving inspiration from those additions, I thought UserDefaults could probably benefit from a similar approach.

Keys

Lets start by defining a UserDefaults.Key type similar to NSAttributedString.Key.

extension UserDefaults {

    public struct Key: Hashable, RawRepresentable, ExpressibleByStringLiteral {
        public var rawValue: String

        public init(rawValue: String) {
            self.rawValue = rawValue
        }

        public init(stringLiteral value: String) {
            self.rawValue = value
        }
    }

}

Notice we're adding ExpressibleByStringLiteral support to allow us to simplify key definitions.

let foo: UserDefaults.Key = "foo"

Type-Safety

Now lets add API that allows us to use our new Key type instead of raw strings. While we're at it lets also add type-safety to allow us to infer function calls.

Note: This is a lighweight approach that doesn't provide key-to-value type-safety. You will still need to know the expected type that's returned for a given key.

func set<T>(_ value: T?, forKey key: Key) {
    set(value, forKey: key.rawValue)
}

func value<T>(forKey key: Key) -> T? {
    return value(forKey: key.rawValue) as? T
}

While we're at it, let's add a subscript too.

subscript<T>(key: Key) -> T? {
    get { return value(forKey: key) }
    set { set(newValue, forKey: key.rawValue) }
}

Finally we need to an API we can use when registering our initial values.

func register(defaults: [Key: Any]) {
    let mapped = Dictionary(uniqueKeysWithValues: defaults.map { (key, value) -> (String, Any) in
        return (key.rawValue, value)
    })

    register(defaults: mapped)
}

API Usage

Now we have a clean API let define some keys:

public extension UserDefaults.Key {
    static let showLineNumbers: UserDefaults.Key = "ShowLineNumbers"
    static let fontFamily: UserDefaults.Key = "SourceFontFamily"
    static let fontSize: UserDefaults.Key = "SourceFontSize"
    static let defaultFontSize: UserDefaults.Key = "DefaultSourceFontSize"
}

Now we can make use of these keys and define some initial values:

let defaults = UserDefaults.standard

defaults.register(defaults: [
    .showLineNumbers: false,
    .fontFamily: defaultFont.familyName!,
    .fontSize: defaultSize,
    .defaultFontSize: defaultSize
])

showNumbers = defaults[.showLineNumbers]
defaults[.showLineNumbers] = false

Conclusion

I think this is a clean solution that feels familiar to other iOS framework APIs and is simple enough to start adding to an existing project immediately with no overhead.

There are other approaches to this problem, but I found this to be a lightweight solution that adds value from the outset, without incurring additional complexity in your code.

Checkout the link to get the full source, including more overrides and conveniences for dealing with various types.


If you liked this post or want to discuss more, leave a comment below or find me on Twitter