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:
- Mike Ash on Friday Q&A, protocol-based, using Codable;
- Macmade’s version (xs-labs) using Mirror and KVO.
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.