2017-10-14

If you’re familiar with Cocoa developement, you probably know what UserDefaults are. Otherwise, a quick recap: (NS)UserDefaults is the macOS/iOS/tvOS name for “user preferences”. The NSUserDefaults API, while very powerful, has all the traits of Objective-C: it’s dynamic, weakly-typed, string-based, and quite verbose.

Recently, two very good implementations of Type-Safe User Defaults have made the news:

Because the world probably needs yet another one, here’s mine1:

The Problem

An entry in UserDefaults is: a value, of a given type, with a given key. Naively, a small layer above UserDefaults could look like this:

struct Prefs {
    var username: String? {
        get {
            return UserDefaults.standard.object(forKey: "username") as? String
        }
        set {
            UserDefaults.standard.set(newValue, forKey: "username")
        }
    }
    var firstLaunchDate: Date? {
        get {
            return UserDefaults.standard.object(forKey: "firstLaunchDate") as? Date
        }
        set {
            UserDefaults.standard.set(newValue, forKey: "firstLaunchDate")
        }
    }
    // ...
}

You can see where this is going: getters and setters have to be written for each entry, the key is copy-pasted, and we have to cast to the expected type manually. It’s tedious and error-prone.

The Solution My Own Quick Hack

Let’s define a TSUD struct, with a T generic type, a key attribute, and a value accessor to UserDefaults:

struct TSUD<T> {
    let key: String
    init(_ k: String = #function) { key = k }
    var value: T? {
        get { return UserDefaults.standard.object(forKey: key) as? T }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}

It can then be used like this:

struct Prefs_ {
    lazy var foo = TSUD<Date>()
    lazy var bar = TSUD<[String]>()
    lazy var baz = TSUD<TimeZone>()
}
var Prefs = Prefs_()

That’s all there is to it! We can then write:

Prefs.foo.value = Date()
Prefs.bar.value = ["nineteen", "eighty-four"]

Prefs.foo.value // "14 oct. 2017 à 16:51"
Prefs.bar.value // ["nineteen", "eighty-four"]

As you can see in the TSUD.init() method, the #function literal is used to define the key with the property name. It’s passed as the default argument value so it’s only written once, as is often the case.

The hardest part is actually to define the UserDefaults key from the property name2. Mike Ash’s solution is to define a new type for each UserDefaults entry, and then use String(describing: self); Macmade’s variant is to use the Mirror API to list the properties at runtime. Using #function is just another solution that seems simpler, but it comes with caveats.

Why lazy?

Also, what’s that weird global var Prefs = Prefs_() ?

#function, as per the documentation, “inside a property getter or setter it is the name of that property”. Unfortunately, that means we can’t write:

struct Prefs_ {
    var foo = TSUD<Date>()
}

because in this context, #function would be Prefs_. Using lazy, I guess, implicitely creates a closure, which is called as a getter the first time the property is accessed, and #function is foo.

For the same reasons, we can’t write:

struct Prefs {
    static var foo = TSUD<Date>()
}

In this context, #function evaluates to -, which doesn’t look like a good key for a UserDefaults entry. This is the reason for the global var Prefs, which is just a singleton trying to pass for a namespace.

That’s all for today! Here’s the playground as a snippet.

Apparently, I’m writing Swift now, and against all odds, I actually enjoy (parts of) it. Hopefully, next post before 2019.

  1. On the other hand, I have never written a JSON parsing library. 

  2. At least Objective-C has proper introspection! Also, Macros!