radex.io

aboutarchiveetsybooksmastodontwitter

Swifty APIs: NSUserDefaults

January 21, 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.

In Swifty methods, I was arguing that we should resist the temptation to write Swift the way Objective-C used to be written. I argued that we should develop new approaches and conventions to designing interfaces, and that we should, once and for all, abolish Objective-C’s excessive verbosity.

Although it was very well received, the issue I’ve had with the essay is that the arguments I was making were all in the abstract. I gave some examples here and there, but they were low-level, and mostly silly.

And so I decided to start a series of articles called Swifty APIs, where I’m going to take some class or component from an Apple framework and show how I would design it with Swift in mind.

Today, we’ll dissect NSUserDefaults and give it a little make-over. We’re going to make it less verbose, a bit cleaner, and more consistent with other classes. We’ll polish its rough edges and we’ll make use of Swift’s cool features along the way.

Verbosity

It would be exaggerated to say that NSUserDefaults, as a whole, is particularly verbose. But there’s one bit, repeated over and over, that’s a bit excessive: NSUserDefaults.standardUserDefaults(). So many words and characters, before we even get to say what we want to fetch or save there.

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

We could add an alias to standardUserDefaults() — for example, a property named standard, shared or even just s. The result would be a bit better, but let’s try something different.

let Defaults = NSUserDefaults.standardUserDefaults()
// Usage:
Defaults.integerForKey("launchCount")

I admit, it’s a little unconventional to make a global variable with an upper-case name, but I think it looks pretty neat. It looks as if it was a class name, not a variable, and we were using class methods. And I think that’s great, because 99% of the time, we just want to use the standard user defaults, and so we think of it as a single global thing. (Another reason to make the name upper-case is that the lower-case “defaults” could easily conflict with local variables.)

The subscript

I think of NSUserDefaults similarly to NSDictionary. It’s some sort of a key-value structure that serves as a data store. There are obvious differences, like the fact that you generally use only one NSUserDefaults object, but still, the primary two interactions are the same: putting stuff in under a key; and fetching stuff out. And if that’s the case, it would make sense to model the syntax similarly, make it more consistent.

Compare:

let dictionary = ["launchCount": 10]
dictionary["launchCount"]
dictionary["color"] = "red"

Defaults.integerForKey("launchCount")
Defaults.setObject("red", forKey: "color")

Wouldn’t it be nice if we could use the same square bracket subscripting syntax in both places?

Here’s my first take:

extension NSUserDefaults {
    subscript(key: String) -> NSObject? {
        get {
            return objectForKey(key)
        }
        set {
            setObject(newValue, forKey: key)
        }
    }
}

The result, at first glance, is quite nice:

Defaults["color"]
Defaults["color"] = "red"

(I hope you can see the third reason why I made Defaults a global variable — it’s not possible to define a “class subscript” in Swift)

Types

There is a serious problem with this implementation, though. The getter always returns NSObject. That won’t fly in a statically typed language. We really need a mechanism to tell compiler the type of data we want to fetch. That’s the necessary evil when interfacing outside of the type-inferred world of Swift.

We could, of course, just cast the value to desired type (e.g. as? NSString), but then again, we were supposed to make the API nicer and less verbose. Here’s an alternative I came up with:

extension NSUserDefaults {
    class Proxy {
        private let defaults: NSUserDefaults
        private let key: String

        private init(_ defaults: NSUserDefaults, _ key: String) {
            self.defaults = defaults
            self.key = key
        }

        var string: String? {
            return defaults.stringForKey(key)
        }

        var int: Int {
            return defaults.integerForKey(key)
        }
    }

    subscript(key: String) -> Proxy {
        return Proxy(self, key)
    }
}

// Usage:
Defaults["color"].string
Defaults["launchCount"].int

So essentially, since we can’t make the subscript convey type information, I made it return an object that represents the actual value in user defaults. And then you use the properties of that object to do the actual fetching.

Note that we don’t have to do this when going the other way around. When we want to put stuff in, objects already carry the type information we need. We don’t need to be explicit about it.

extension NSUserDefaults {
    subscript(key: String) -> Any? {
        get {
            return self[key]
        }
        set {
            if let v = newValue as? String {
                setObject(v, forKey: key)
            } else if let v = newValue as? Int {
                setInteger(v, forKey: key)
            }
        }
    }
}

You might wonder why I made a String -> Any? subscript instead of adding separate subscripts for each accepted type. Well, I tried doing it that way and it didn’t work. I’m not sure if it’s a bug or a consequence of how type system works in Swift. Either way, this implementation works just fine. You can write

Defaults["color"] = "red"

And the String -> Any? setter will be executed. But you can still write:

Defaults["color"].string

And the compiler will do the right thing — run String -> Proxy getter. So, while we have to define some getter that will return Any? (you can’t define a setter-only subscript), it won’t actually be used in practice.

Optionals

Making the API concise and nice is just one part of the equation. But if there are inconsistencies or other issues with the actual behavior, you’ve got a real problem.

Consider what happens if you try to fetch a value from user defaults that doesn’t exist:

Defaults.stringForKey("color")        // => nil
Defaults.arrayForKey("lastPaths")     // => nil
Defaults.integerForKey("launchCount") // => 0
Defaults.boolForKey("loggingEnabled") // => false

Huh? You’ll get nil in some cases, but not if what you want to fetch is a number or a boolean — then you’ll get 0 and false, respectively. It’s understandable why they did that — in Objective-C, those types are dumb “primitive types”, and nil only makes sense for pointers. But in Swift, anything can be optional. So let’s bring consistency!

extension NSUserDefaults {
    class Proxy {
        var object: NSObject? {
            return defaults.objectForKey(key) as? NSObject
        }

        var number: NSNumber? {
            return object as? NSNumber
        }

        var int: Int? {
            return number?.integerValue
        }

        var bool: Bool? {
            return number?.boolValue
        }
    }
}

If key doesn’t exist, objectForKey() will return nil. And if it does exist, but isn’t a number, the optional cast to NSNumber will fail, and you’ll also get nil.

And in cases when you do want the standard behavior, nil coalescing comes to rescue:

Defaults["launchCount"].int ?? 0
Defaults["loggingEnabled"].bool ?? false
Defaults["color"].string ?? ""

Existence

Believe it or not, NSUserDefaults doesn’t have a method for checking if a key exists. It only takes a quick Google search to figure out that objectForKey() will return nil if a value doesn’t exist. Still, this should be just an implementation detail, and there should be a proper interface for it.

extension NSUserDefaults {
    func hasKey(key: String) -> Bool {
        return objectForKey(key) != nil
    }
}

And while we’re at it, let’s mention removing things. There is a removeObjectForKey() method for it, but I decided to shorten it to remove(). Also, it’s possible to remove objects by setting key’s value to nil, so I also added that feature to our String -> Any? subscript in case someone tried doing it this way.

I was also playing with the idea of adding those two features to our NSUserDefaults.Proxy object.

extension NSUserDefaults {
    class Proxy {
        var exists: Bool {
            return defaults.hasKey(key)
        }

        func remove() {
            defaults.removeObjectForKey(key)
        }
    }
}

// Usage:
Defaults["color"].exists
Defaults["color"].remove()

I was quite torn on this. On one hand, it plays well with the idea that NSUserDefaults’s subscript returns an object representing a value. On the other hand, the Proxy class was just a necessary evil; checking for existence and removing objects is an operation on the entire data structure, not an element of it. In the end, I sided with the latter argument.

Optional assignment

In Ruby, there’s a useful operator, ||=, for conditional assignment. It’s used like this:

a = nil     # => nil
a ||= "foo" # => "foo"
a ||= "bar" # => "foo"

Essentially, the right-hand value is assigned to the left-hand variable, but only if it’s undefined, nil or false.

I think Swift should also have this operator, only that I’d call it ?=, the optional assignment operator. It would set the value of a variable (an optional) if it’s nil.

The magic of Swift is that you can define it on your own:

infix operator ?= {
    associativity right
    precedence 90
}

func ?= <T>(inout variable: T?, @autoclosure expr: () -> T) {
    if variable == nil {
        variable = expr()
    }
}

What does it have to do with NSUserDefaults? Well, if we can optionally assign values to variables, why not optionally assign values to user defaults keys?

func ?= (proxy: NSUserDefaults.Proxy, @autoclosure expr: () -> Any) {
    if !proxy.defaults.hasKey(proxy.key) {
        proxy.defaults[proxy.key] = expr()
    }
}

// Usage:
Defaults["color"]            // => nil
Defaults["color"] ?= "white" // => "white"
Defaults["color"] ?= "red"   // => "white"

Note that this is different from using registerDefaults. The optional assignment operator changes the actual user defaults data and saves the new values to disk. registerDefaults has similar behavior, but it only modifies the defaults in memory.

Arithmetic

Consider what you have to do if you want to increment the value of a user default:

Defaults["launchCount"].int = (Defaults["launchCount"] ?? 0) + 1

If it was a variable, you could make it shorter and clearer by using the += operator. Well, who says we can’t do that as well?

func += (proxy: NSUserDefaults.Proxy, b: Int) {
    let a = proxy.defaults[proxy.key].int ?? 0
    proxy.defaults[proxy.key] = a + b
}

// Usage:
Defaults["launchCount"] += 1

Nice! But heck, let’s make it even shorter:

postfix func ++ (proxy: NSUserDefaults.Proxy) {
    proxy += 1
}

// Usage:
Defaults["launchCount"]++

Voilà!

Wrapping up

Let’s fill in the blanks. I added double property to NSUserDefaults.Proxy that works just like int and bool (using conversion from NSNumber, not the doubleForKey method). I did not add a float property, because you’re supposed to just use Double in Swift.

I added array, dictionary and data properties that mirror arrayForKey, dictionaryForKey and dataForKey. I also added date property, because NSDate is one of the types supported by NSUserDefaults, and yet it doesn’t have a built-in getter.

And finally, I updated our setter:

subscript(key: String) -> Any? {
    get {
        return self[key]
    }
    set {
        if let v = newValue as? Int {
            setInteger(v, forKey: key)
        } else if let v = newValue as? Double {
            setDouble(v, forKey: key)
        } else if let v = newValue as? Bool {
            setBool(v, forKey: key)
        } else if let v = newValue as? NSObject {
            setObject(v, forKey: key)
        } else if newValue == nil {
            removeObjectForKey(key)
        } else {
            assertionFailure("Invalid value type")
        }
    }
}

On my first try, I defined the subscript to return AnyObject?, but when I added Double and Bool after Int, I was surprised that it didn’t work properly. You could set and fetch back doubles and booleans, but they would be encoded as integers. Turns out, when you try to pass numbers or booleans as AnyObject, they get automatically mapped to NSNumber under the hood, which caused this odd behavior. Simple change to Any fixed the issue.

Result

Let’s look at our brand new NSUserDefaults API in its full glory:

// Fetching data
Defaults["color"].string
Defaults["launchCount"].int ?? 0
Defaults["lastPaths"].array?.firstObject

// Setting data
Defaults["color"] = "red"
Defaults["firstLaunchAt"] ?= NSDate()
Defaults["launchCount"]++
Defaults["totalTime"] += 3600

// Checking & removing
if !Defaults.hasKey("hotkey") {
    Defaults.remove("hotkeyOptions")
}

Next steps

Now, the journey doesn’t stop here. Although we made great progress on syntax and noise reduction, this isn’t the best we can do. Check out Statically-typed NSUserDefaults to see how we can take User Defaults to the next level, simplify their use, and get compiler checks for free by adopting typed, statically-defined keys.

Try it out

As always, I published the full source code on GitHub: SwiftyUserDefaults. If you like it, you can easily 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 January 21, 2015. Last updated October 05, 2015. Send feedback.