radex.io

aboutarchive fediversetwitter

Statically-typed NSUserDefaults

August 31, 2015
Hey! This is an old post. It reflects my views and the state of the world in 2015, but is likely to be outdated. You’ll find me on Twitter, building Nozbe, and making engineering blueprints.

A year ago, not long after Swift became a thing, I noticed a tendency among programmers to write it the way you’d write Objective-C. I thought that Swift was a different language, with different syntax, philosophy and capabilities, and so we should develop new conventions and approaches to it. I responded with Swifty methods, where I argued for a better, clearer way of naming things. Then, some time later, I started the Swifty APIs series to put those ideas in practice and explore how to design easy-to-use interfaces.

In the first article in said series, we took the NSUserDefaults API:

NSUserDefaults.standardUserDefaults().stringForKey("color")
NSUserDefaults.standardUserDefaults().setObject("red", forKey: "color")

… and we made it look like this:

Defaults["color"].string
Defaults["color"] = "red"

The result was clearer, less verbose, and nicer looking than the original. We fixed some consistency issues and made it all fit better with Swift. This felt like a significant improvement.

And yet, as I’ve been actually using the new API, and learning Swift along the way, I realized that it wasn’t actually very Swifty at all. I drew inspiration from Ruby’s and Swift’s syntax in designing it, and that matters, but we didn’t improve it on a semantic level at all. We only put a Swifty coat of paint on a fundamentally Objective-C-like mechanism.

Shortcomings

Of course, “not Swifty” isn’t a good reason to start from scratch. Familiarity makes an API easier to learn, but we don’t want to be dogmatic about it. We don’t just want a Swift-like design, we want what works best in Swift, period. So here’s a few issues with NSUserDefaults:

Suppose you have a preference for user’s favorite color:

Defaults["color"] = "red"
// elsewhere in the app:
Defaults["colour"].string // => nil

Ooops, but you’ve made a typo in the key name. Boom, that’s a bug.

Let’s say you keep a date object in the defaults:

Defaults["deadline"] = NSDate.distantFuture()
Defaults["deadline"].data // => nil

This time you mistyped the date getter, and once again, you have a bug. Unlikely to happen? Probably. But why do we have to specify the return type every single time we want to fetch, anyway? It’s kind of annoying.

Here’s one more:

Defaults["deadline"] = NSData()
Defaults["deadline"].date // => nil

Clearly we meant “right now”, not “empty data”. Oh well.

What about this:

Defaults["magic"] = 3.14
Defaults["magic"] += 10
Defaults["magic"] // => 13

The only reason why += works at all is because we defined it as a magic operator. But it can only infer types (Int or Double) from the argument passed. So if you pass an integer 10, the result will be stored as an integer, cutting off the fractional part. And we have a bug.

You might be thinking that these are purely theoretical problems that wouldn’t arise in the real world. But think twice. These are the same kinds of bugs as mistyping a variable or method name, or passing a parameter of the wrong type. These things happen all the time. If you work with a compiled, statically-typed language, you grow to appreciate getting feedback from the compiler more quickly than by testing it. More importantly, investment in compile-time checks pays dividends over time. This isn’t just for you writing the code for the first time, this is for preventing bugs as you change and refactor it later. This is about protecting the future-you from past-you.

The power of static typing

The root of all evil here is that there is no statically-defined structure of the user defaults.

With the previous redesign, I recognized this problem and tried to slap types on the subscript by having it return a Proxy object providing typed accessors. This, in my estimation, was better than manually casting AnyObject or having getter methods instead of a subscript.

But it was a hack, not a solution. To make a meaningful improvement to the API, we need to centralize information about user default keys and make it available to the compiler.

How? Consider how people sometimes define string keys ahead of time to avoid typos and get name auto-completion for free:

let colorKey = "color"

What if we do just that, but with type information?

class DefaultsKey<ValueType> {
    let key: String

    init(_ key: String) {
        self.key = key
    }
}

let colorKey = DefaultsKey<String?>("color")

We wrapped the key name in an object, and embedded the value’s type in a generic type parameter. Now we can define a new subscript on NSUserDefaults that accepts these keys:

extension NSUserDefaults {
    subscript(key: DefaultsKey<String?>) -> String? {
        get { return stringForKey(key.key) }
        set { setObject(newValue, forKey: key.key) }
    }
}

And here’s the result:

let key = DefaultsKey<String?>("color")
Defaults[key] = "green"
Defaults[key] // => "green", typed as String?

Boom, as simple as that. More refinements to the syntax and functionality are possible (as we’ll soon explore), but with This One Simple Trick™, we fixed many of our problems. We can’t easily make a key name typo, because it’s defined only once. We can’t assign a value of the wrong type, because the compiler won’t let us. And we don’t need to write .string, because the compiler already knows what we want.

By the way. We should probably just use generics to define the NSUserDefaults subscripts instead of typing all of the needed variants by hand. Well, that would be wonderful, but sadly, the Swift compiler doesn’t support generic subscripts right now. Sigh. The square brackets might be nice, but let’s not be stupid about the syntax, and just make generic methods for setting and getting… Right?

Ahh, but you haven’t seen what subscripts can do yet!

Subscripts are awesome

Consider this:

var array = [1, 2, 3]
array.first! += 10

It won’t compile. We’re trying to increment an integer inside an array, but this makes no sense in Swift. Integers have value semantics. They’re immutable. You can’t just change them when they’re returned from someplace else, because they don’t really exist outside of that expression. They’re merely transient copies.

It’s variables that have the concept of mutability. When you do:

var number = 1
number += 10

You’re not actually changing the value, you’re changing the variable — assigning a new value to it.

But take a look at this:

var array = [1, 2, 3]
array[0] += 10
array // => [11, 2, 3]

This just works. It does exactly what you’d expect, but it’s actually not obvious why. Huh!

See, subscripts have semantics that play really well with value types in Swift. The reason why you can change values in an array through the subscript is because it defines both a getter and a setter. This allows the compiler to rewrite array[0] += 10 to array[0] = array[0] + 10. If you make a getter-only subscript, it won’t work.

Again, this isn’t some magic on Array’s part. This is a consequence of carefully designed semantics of subscripts. We get the exact same behavior for free, and it allows us to do things like:

Defaults[launchCountKey]++
Defaults[volumeKey] -= 0.1
Defaults[favoriteColorsKey].append("green")
Defaults[stringKey] += "… can easily be extended!"

You know, it’s funny. In 1.0, we used subscripts purely for their syntactic value (because it made user defaults look like a dictionary), and missed out completely on semantic benefits.

We added a few operators like += and ++, but their behavior was dangerous and it relied on magic implementations. Here, by encapsulating type information in the key, and by defining both the subscript getter and setter, all of this actually just works.

Taking shortcuts

One nice thing about using plain old string keys is that you could just use them in place, without having to create any intermediate objects.

Obviously, creating the key object every time we want to use it doesn’t make much sense. This would be awfully repetitive and would eliminate the benefits of static typing. So let’s see how we can organize our defaults keys better.

One way is to define keys at the class level where we use them:

class Foo {
    struct Keys {
        static let color = DefaultsKey<String>("color")
        static let counter = DefaultsKey<Int>("counter")
    }

    func fooify() {
        let color = Defaults[Keys.color]
        let counter = Defaults[Keys.counter]
    }
}

This seems to already be a standard Swift practice with string keys.

Another way is to take advantage of Swift’s implicit member expressions. The most common use of this feature is with enum cases. When calling a method that takes a parameter of enum type Direction, you can pass .Right, and the compiler will infer that you meant Direction.Right. Less known is the fact that this also works with any static members of argument’s type. So you can call a method taking a CGRect with .zeroRect instead of typing CGRect.zeroRect.

In fact, we can do the same thing here by defining our keys as static constants on DefaultsKey. Well, almost. We need to define it slightly differently to work around a compiler limitation at the moment:

class DefaultsKeys {}
class DefaultsKey<ValueType>: DefaultsKeys { ... }

extension DefaultsKeys {
    static let color = DefaultsKey<String>("color")
}

And now, oh wow!

Defaults[.color] = "red"

Isn’t this cool? At caller site, verbosity is now lower than with the traditional stringly-typed approach. We do less work writing and reading the code, and get all the benefits almost for free.

(One shortcoming of this technique is that there is no mechanism for namespacing. In a large project, it might be better to go with the Keys struct approach.)

The optionality conundrum

In the original API redesign, we made all getters return optionals. I disliked the lack of consistency in how NSUserDefaults treats different types. For strings, a missing value would get you nil, but for numbers and booleans, you’d get 0 and false.

The downside of this approach, as I quickly realized, was verbosity. Much of the time, we don’t really care about the nil case — we just want to get some default value. When subscripts return an optional, we have to coalesce it every time we fetch.

A solution to this problem was proposed by Oleg Kokhtenko. In addition to the standard, Optional-returning getters, another set of getters was added. Those have names ending with -Value and replace nil with a default value that makes sense for the type:

Defaults["color"].stringValue            // defaults to ""
Defaults["launchCount"].intValue         // defaults to 0
Defaults["loggingEnabled"].boolValue     // defaults to false
Defaults["lastPaths"].arrayValue         // defaults to []
Defaults["credentials"].dictionaryValue  // defaults to [:]
Defaults["hotkey"].dataValue             // defaults to NSData()

We can do the same thing under the static typing regime by providing subscript variants for optional and non-optional types, like so:

extension NSUserDefaults {
    subscript(key: DefaultsKey<NSData?>) -> NSData? {
        get { return dataForKey(key.key) }
        set { setObject(newValue, forKey: key.key) }
    }

    subscript(key: DefaultsKey<NSData>) -> NSData {
        get { return dataForKey(key.key) ?? NSData() }
        set { setObject(newValue, forKey: key.key) }
    }
}

I like it, because we’re not relying on convention (type and typeValue) to convey nullability. We’re using the actual type when defining a user defaults key, and then letting the compiler handle the rest.

More types

Filling in the blanks, I extended the scope of supported types by adding subscripts for all of these: String, Int, Double, Bool, NSData, [AnyObject], [String: AnyObject], NSString, NSArray, NSDictionary, and their optional variants (plus NSDate?, NSURL?, and AnyObject?, which don’t have a non-optional counterpart, because there isn’t a reasonable default value).

And yes, strings, dictionaries and arrays have both Swift and Foundation variants. Native Swift types are preferrable, but they don’t have all of the capabilities of their NS counterparts. If someone wants those, I want to make that easy.

And speaking of arrays, why limit ourselves to untyped arrays only? In most cases, arrays stored in user defaults are going to have homogenous elements of a simple type, like String, Int, or NSData.

Since we can’t define generic subscripts, we’ll create a pair of generic helper methods:

extension NSUserDefaults {
    func getArray<T>(key: DefaultsKey<[T]>) -> [T] {
        return arrayForKey(key.key) as? [T] ?? []
    }

    func getArray<T>(key: DefaultsKey<[T]?>) -> [T]? {
        return arrayForKey(key.key) as? [T]
    }
}

… and then copy&paste this stub for all types we’re interested in:

extension NSUserDefaults {
    subscript(key: DefaultsKey<[String]?>) -> [String]? {
        get { return getArray(key) }
        set { set(key, newValue) }
    }
}

And now we can do this:

let key = DefaultsKey<[String]>("colors")
Defaults[key].append("red")
let red = Defaults[key][0]

Subscripting the array returns String, and appending to it verifies at compile time that you are passing a string. Win for convenience, win for safety.

Archiving

A limitation of NSUserDefaults is that it only supports a handful of types. A common workaround for storing custom types is to serialize them with NSKeyedArchiver.

Let’s make this easy to do. Similarly to the getArray helper, I defined generic archive() and unarchive() methods. With those, you can easily define a stub subscript for whatever NSCoder-compliant type you want:

extension NSUserDefaults {
    subscript(key: DefaultsKey<NSColor?>) -> NSColor? {
        get { return unarchive(key) }
        set { archive(key, newValue) }
    }
}

extension DefaultsKeys {
    static let color = DefaultsKey<NSColor?>("color")
}

Defaults[.color] // => nil
Defaults[.color] = NSColor.whiteColor()
Defaults[.color] // => w 1.0, a 1.0
Defaults[.color]?.whiteComponent // => 1.0

Clearly not perfect, but it’s nice that with just a few lines of boilerplate, we can make any custom type work naturally with NSUserDefaults.

Result and conclusions

And that’s it! Here’s how our new API presents itself:

// Define keys ahead of time
extension DefaultsKeys {
    static let username = DefaultsKey<String?>("username")
    static let launchCount = DefaultsKey<Int>("launchCount")
    static let libraries = DefaultsKey<[String]>("libraries")
    static let color = DefaultsKey<NSColor?>("color")
}

// Use the dot syntax to access user defaults
Defaults[.username]

// Define key’s type without `?` to use default values instead of optionals
Defaults[.launchCount] // Int, defaults to 0

// Modify value types in place
Defaults[.launchCount]++
Defaults[.volume] += 0.1
Defaults[.strings] += "… can easily be extended!"

// Use and modify typed arrays
Defaults[.libraries].append("SwiftyUserDefaults")
Defaults[.libraries][0] += " 2.0"

// Easily work with custom serialized types
Defaults[.color] = NSColor.whiteColor()
Defaults[.color]?.whiteComponent // => 1.0

Static typing doesn’t hurt in Swift

I hope you can see the significant benefits of this statically-typed approach. Yes, there’s a small price to opt in. We need to pay our respects to the type system and define a DefaultsKey ahead of time. But in return, the compiler brings us a sack full of presents:

And more benefits might be possible as Swift advances.

Truly Swifty APIs take advantage of static typing. I don’t mean to be dogmatic about it — there are surely problems that are best solved in different ways. But do consider the benefits before you fall back to the way you’d do things in Objective-C or JavaScript. Remember, this isn’t your grandfather’s static typing we’re talking about. A rich type system Swift has usually allows for really expressive and nice-to-use APIs with little overhead.

Try it out

As always, the result of these explorations is available as a library on GitHub, and if you like it, you can include it in your app:

# with CocoaPods:
pod 'SwiftyUserDefaults'

# with Carthage:
github "radex/SwiftyUserDefaults"

I also encourage you to check out my other Swifty API project with NSTimer, and my article about clarity and naming things that sparked this series: Swifty methods.

And if you have comments, complaints or ideas for improvement, please let me know on Twitter or make an issue on GitHub.

Published August 31, 2015. Last updated October 05, 2015. Send feedback.