第一部分:Swifty APIs: NSUserDefaults01传统弊端(方法太长)Verbosity02使用下边改进: The subscript03类型的问题Types04可能为空Optionals05Existence: key是否存在?06Optional assignment可选赋值07算法方面Arithmetic08简单的封装Wrapping upResult最后的效果下一步Next steps第二部分:Statically-typed NSUserDefaults01 不足之处Shortcomings02 静态类型The power of static typing03 下标访问的进阶Subscripts are awesome04走捷径Taking shortcuts05 The optionality conundrum缺省值的问题06 更多的类型More types07 序列化的问题Archiving08 结论Result and conclusions
http://radex.io/swift/nsuserdefaults/
https://github.com/radex/SwiftyUserDefaults
https://github.com/radex/SwiftyUserDefaults
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.
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.)
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)
这里说不能定义
class subscript
,应该是可以的(swift2.1)
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.
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!(swift的思想是返回的一个optionalNil)
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 ?? ""
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.
是什么意思呢?
就是这个符号: ??
也就是如果前面有值,返回就是前面的值. 如果前面是值,就返回后面的值.
(当然后面的值被当成了一个闭包.)
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.
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à!
可以看到,值的改变,也是将实现封装到了一个自定义运算符中去了.
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.
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")
}
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.
https://github.com/radex/SwiftyUserDefaults
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.(在OC的机制上披上了一层swift的外衣)
语法层次上的提示:来自Ruby的灵感
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.
将key拼写错误了.
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.
将getter方法拼错了.
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.
- key值写错
- 方法名写错
- 传入了错误的参数类型
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 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.
现在的问题在于怎么把keys集中化,暴露给编译器
How? Consider how people sometimes define string keys ahead of time to avoid typos and get name auto-completion for free:
xlet colorKey = "color"
借鉴定义常量字符串:
1.避免了拼写错误
2.编译器自动补全
What if we do just that, but with type information?
xclass 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.
上面的代码做了什么:
- 将一个key封装到一个类对象中了.
- key对应的值呢?被嵌入到了类需要使用的泛型参数.
Now we can define a new subscript on NSUserDefaults that accepts these keys:
xextension NSUserDefaults {
subscript(key: DefaultsKey<String?>) -> String? {
get { return stringForKey(key.key) }
set { setObject(newValue, forKey: key.key) }
}
}
And here’s the result:
xlet 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.
解决的问题:
1) 只定义了一次,减少拼写错误
2)赋值时候编译器做类型检查
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.
上面的下标访问只支持字符串,如果我们不想全部手动写,使用泛型,但是问题是:
swift编译器不支持下标访问的时候使用泛型.
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!
Consider this:
xvar 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!
奇怪了,上面的array.first就不能编译通过,但是array[0]却能达到预期的效果,这是为什么?
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.
原因在于下标访问方法定义了getter和setter两个方法,而不仅仅是一个只读的getter方法.
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:
xDefaults[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.
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:
xclass 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.
举个例子,有个方法带一个参数: Direction枚举类型,
如果直接传入
.Righ
,那么编译器认为你传入的是Direction.Right
其实,这种情况也适用于静态成员变量作为函数参数的情况.
例如,带一个CGRect的参数的时候,可以传入
.zeroRect
,而不用传入CGRect.zeroRect
enum Direction {
case Left,Right,Up,Down
}
class Person {
class func walkTo(direction: Direction){
print("now walk to \(direction)")
}
}
Person.walkTo(.Right)
//now walk to Right
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:
xclass DefaultsKeys {}
class DefaultsKey<ValueType>: DefaultsKeys { ... }
extension DefaultsKeys {
static let color = DefaultsKey<String>("color")
}
And now, oh wow!
xDefaults[.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.)
不足之处: 工程变大的时候,由于没有命名空间,更好的解决方法是使用结构体
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.
默认值返回的问题:
string类型,返回nil
数值类型,返回0
布尔类型,返回false
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:
xextension 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) }
}
}
在get方法的时候,给出了默认值.
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.
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.
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.
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 += " 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.