patternswiftMinor
Typed NSUserDefaults
Viewed 0 times
typednsuserdefaultsstackoverflow
Problem
I was looking around for a Swift wrapper around
Eventually, I tried something myself and came up with the code below. I'm tempted to use it in production, but do not have much experience with user defaults and would be grateful for any comments regarding the safety of the solution below:
We start by declaring a pair of protocols that we can use to tag the types we wish to support:
Tag all the classes supported by
We declare a
Now we can extend as many types as we wish. Let's start with
W
NSUserDefaults and found some very nice projects (see e.g. SwiftyUserDefaults). Unfortunately, it all boils down to stringy keys galore...Eventually, I tried something myself and came up with the code below. I'm tempted to use it in production, but do not have much experience with user defaults and would be grateful for any comments regarding the safety of the solution below:
We start by declaring a pair of protocols that we can use to tag the types we wish to support:
public protocol UserDefaultsObject: AnyObject {}
public protocol UserDefaultsValue {
var userDefault: UserDefaultsObject { get }
init(UserDefaultsObject)
}Tag all the classes supported by
NSUserDefaults:import Foundation
extension NSNumber: UserDefaultsObject {}
extension NSString: UserDefaultsObject {}
// etc...We declare a
struct because that allows us to decide on mutability simply but instantiating a UserDefault value as a let or a var:public struct UserDefault {
public let key: String
public var value: T? {
get { return store }
set {
store = newValue
NSUserDefaults.standardUserDefaults().setObject(store?.userDefault, forKey: key)
}
}
public init(key: String, defaultValue: T?) {
self.key = key
if let object: AnyObject = NSUserDefaults.standardUserDefaults().objectForKey(key) {
let userDefaultsObject = object as! UserDefaultsObject // this is the crucial line
store = T(userDefaultsObject)
} else {
store = defaultValue
}
}
private var store: T?
}Now we can extend as many types as we wish. Let's start with
Int:extension Int: UserDefaultsValue {
public var userDefault: UserDefaultsObject {
return NSNumber(integer: self)
}
public init(_ object: UserDefaultsObject) {
self = (object as! NSNumber).integerValue
}
}W
Solution
Our constructors would be better as failable initializers.
Nothing prevents us from passing a wrapped
First, we need to change the protocol to define a failable initializer rather than a non-failable initializer:
Now, write the initializers as failable:
We can clean up our
Like our initializers which should be failable, this runs the risk of crashing if
The conditional will fail if:
There is one major concern I have, however, and I don't know of the right solution right now.
Consider the following situation, I save an
In your version of the code, this will happen will crash because this line:
Will have no problem converting an
Because the type of
Our
Nothing prevents us from passing a wrapped
NSNumber to the String initializer, or a wrapped String to the Int initializer. In your code, this would simply crash. It would be better if our initializers were allowed to fail.First, we need to change the protocol to define a failable initializer rather than a non-failable initializer:
public protocol UserDefaultsValue {
var userDefault: UserDefaultsObject { get }
init?(UserDefaultsObject)
}Now, write the initializers as failable:
extension Int: UserDefaultsValue {
public var userDefault: UserDefaultsObject {
return NSNumber(integer: self)
}
public init?(_ object: UserDefaultsObject) {
if let number = object as? NSNumber {
self = number.integerValue
} else {
return nil
}
}
}We can clean up our
UserDefault constructor a bit as well.if let object: AnyObject = NSUserDefaults.standardUserDefaults().objectForKey(key) {
let userDefaultsObject = object as! UserDefaultsObject // this is the crucial line
store = T(userDefaultsObject)
}Like our initializers which should be failable, this runs the risk of crashing if
object turns out to be some type which isn't a UserDefaultsObject. We can clean this up slightly though. This can look cleaner and be safer:if let object = NSUserDefaults.standardUserDefaults().objectForKey(key) as? UserDefaultsObject {
store = T(object)
}The conditional will fail if:
- There isn't already an object for this key.
- Whatever
NSUserDefaultsreturns for this key cannot be interpreted as a UserDefaultsObject.
There is one major concern I have, however, and I don't know of the right solution right now.
Consider the following situation, I save an
NSNumber object to the key "abckey". Now, I do this:let s = UserDefault(key: "abckey", defaultValue: "Hello World")In your version of the code, this will happen will crash because this line:
let userDefaultsObject = object as! UserDefaultObjectWill have no problem converting an
NSNumber to a UserDefaultObject (assuming you fixed it with extension NSNumber: UserDefaultsObject {}), but the following line will be problematic:store = T(userDefaultsObject)Because the type of
T will be a String (we passed a String for defaultValue argument).Our
extension String: UserDefaultsValue will have to include several constructors which take every sort of possible thing that can be stored in NSUserDefaults. And some of those simply won't make sense.Code Snippets
public protocol UserDefaultsValue {
var userDefault: UserDefaultsObject { get }
init?(UserDefaultsObject)
}extension Int: UserDefaultsValue {
public var userDefault: UserDefaultsObject {
return NSNumber(integer: self)
}
public init?(_ object: UserDefaultsObject) {
if let number = object as? NSNumber {
self = number.integerValue
} else {
return nil
}
}
}if let object: AnyObject = NSUserDefaults.standardUserDefaults().objectForKey(key) {
let userDefaultsObject = object as! UserDefaultsObject // this is the crucial line
store = T(userDefaultsObject)
}if let object = NSUserDefaults.standardUserDefaults().objectForKey(key) as? UserDefaultsObject {
store = T(object)
}let s = UserDefault(key: "abckey", defaultValue: "Hello World")Context
StackExchange Code Review Q#87572, answer score: 3
Revisions (0)
No revisions yet.