HiveBrain v1.2.0
Get Started
← Back to all entries
patternswiftMinor

Typed NSUserDefaults

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
typednsuserdefaultsstackoverflow

Problem

I was looking around for a Swift wrapper around 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 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 NSUserDefaults returns 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! UserDefaultObject


Will 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.