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.
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.
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:
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)
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:
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
And the 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.
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:
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!
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:
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.
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.
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:
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:
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.
Arithmetic
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:
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:
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:
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:
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.