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.
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.
We could add an alias to
standardUserDefaults() — for example, a property named
shared or even just
s. The result would be a bit better, but let’s try something different.
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.)
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.
Wouldn’t it be nice if we could use the same square bracket subscripting syntax in both places?
Here’s my first take:
The result, at first glance, is quite nice:
(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)
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:
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.
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
String -> Any? setter will be executed. But you can still write:
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.
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:
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
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!
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
And in cases when you do want the standard behavior, nil coalescing comes to rescue:
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.
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
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.
In Ruby, there’s a useful operator,
||=, for conditional assignment. It’s used like this:
Essentially, the right-hand value is assigned to the left-hand variable, but only if it’s undefined,
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
The magic of Swift is that you can define it on your own:
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?
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.
Consider what you have to do if you want to increment the value of a user default:
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?
Nice! But heck, let’s make it even shorter:
Let’s fill in the blanks. I added
double property to
NSUserDefaults.Proxy that works just like
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.
data properties that mirror
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:
On my first try, I defined the subscript to return
AnyObject?, but when I added
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.
Let’s look at our brand new
NSUserDefaults API in its full glory:
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:
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.