Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSUserDefaults in Swift - implementing type safety

One of the things that bugs me about Swift and Cocoa together is working with NSUserDefaults, because there is no type information and it is always necessary to cast the result of objectForKey to what you are expecting to get. It is unsafe and impractical. I decided to tackle this problem, making NSUserDefaults more practical in Swift-land, and hopefully learning something along the way. Here were my goals in the beginning:

  1. Complete type safety: each key has one type associated with it. When setting a value, only a value of that type should be accepted and when getting a value the result should come out with the correct type
  2. Global list of keys which are clear in meaning and content. The list should be easy to create, modify and extend
  3. Clean syntax, using subscripts if possible. For example, this would be perfect:

    3.1. set: UserDefaults[.MyKey] = value

    3.2. get: let value = UserDefaults[.MyKey]

  4. Support for classes that conform to the NSCoding protocol by automatically [un]archiving them

  5. Support for all property list types accepted by NSUserDefaults

I started by creating this generic struct:

struct UDKey <T>  {
    init(_ n: String) { name = n }
    let name: String
}

Then I created this other struct that serves as a container for all the keys in an application:

struct UDKeys {}

This can then be extended to add keys wherever needed:

extension UDKeys {
    static let MyKey1 = UDKey<Int>("MyKey1")
    static let MyKey2 = UDKey<[String]>("MyKey2")
}

Note how each key has a type associated with it. It represents the type of the information to be saved. Also, the name property is the string that is to be used as a key for NSUserDefaults.

The keys can be listed all in one constants file, or added using extensions on a per-file basis close to where they are being used for storing data.

Then I created an "UserDefaults" class responsible for handling the getting/setting of information:

class UserDefaultsClass {
    let storage = NSUserDefaults.standardUserDefaults()
    init(storage: NSUserDefaults) { self.storage = storage }
    init() {}

    // ...
}

let UserDefaults = UserDefaultsClass() // or UserDefaultsClass(storage: ...) for further customisation

The idea is that one instance for a particular domain is created and then every method is accessed in this way:

let value = UserDefaults.myMethod(...)

I prefer this approach to things like UserDefaults.sharedInstance.myMethod(...) (too long!) or using class methods for everything. Also, this allows interacting with various domains at the same time by using more than one UserDefaultsClass with different storage values.

So far, items 1 and 2 have been taken care of, but now the difficult part is starting: how to actually design the methods on UserDefaultsClass in order to comply with the rest.

For example, let's start with item 4. First I tried this (this code is inside UserDefaultsClass):

subscript<T: NSCoding>(key: UDKey<T>) -> T? {
    set { storage.setObject(NSKeyedArchiver.archivedDataWithRootObject(newValue), forKey: key.name) }
    get {
        if let data = storage.objectForKey(key.name) as? NSData {
            return NSKeyedUnarchiver.unarchiveObjectWithData(data) as? T
        } else { return nil }
    }
}

But then I find out that Swift doesn't allow generic subscripts!! Alright, then I guess I'll have to use functions then. There goes half of item 3...

func set <T: NSCoding>(key: UDKey<T>, _ value: T) {
    storage.setObject(NSKeyedArchiver.archivedDataWithRootObject(value), forKey: key.name)
}
func get <T: NSCoding>(key: UDKey<T>) -> T? {
    if let data = storage.objectForKey(key.name) as? NSData {
        return NSKeyedUnarchiver.unarchiveObjectWithData(data) as? T
    } else { return nil }
}

And that works just fine:

extension UDKeys { static let MyKey = UDKey<NSNotification>("MyKey") }

UserDefaults.set(UDKeys.MyKey, NSNotification(name: "Hello!", object: nil))
let n = UserDefaults.get(UDKeys.MyKey)

Note how I can't call UserDefaults.get(.MyKey). I have to use UDKeys.MyKey. And I can't do that because it's not yet possible to have static variables on a generic struct!!

Next, let's try number 5. Now that has been an headache and that's where I need lots of help.

Property list types are, as per the docs:

A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary.

That in Swift means Int, [Int], [[String:Bool]], [[String:[Double]]], etc are all property list types. At first I thought that I could just write this and trust whoever is using this code to remember that only plist types are allowed:

func set <T: AnyObject>(key: UDKey<T>, _ value: T) {
    storage.setObject(value, forKey: key.name)
}
func get <T: AnyObject>(key: UDKey<T>) -> T? {
    return storage.objectForKey(key.name) as? T
}

But as you'll notice, while this works fine:

extension UDKeys { static let MyKey = UDKey<NSData>("MyKey") }

UserDefaults.set(UDKeys.MyKey, NSData())
let d = UserDefaults.get(UDKeys.MyKey)

This doesn't:

extension UDKeys { static let MyKey = UDKey<[NSData]>("MyKey") }
UserDefaults.set(UDKeys.MyKey, [NSData()])

And this doesn't either:

extension UDKeys { static let MyKey = UDKey<[Int]>("MyKey") }
UserDefaults.set(UDKeys.MyKey, [0])

Not even this:

extension UDKeys { static let MyKey = UDKey<Int>("MyKey") }
UserDefaults.set(UDKeys.MyKey, 1)

The problem is that they are all valid property list types yet Swift obviously interprets arrays and ints as structs, not as their Objective-C class counterparts. However:

func set <T: Any>(key: UDKey<T>, _ value: T)

won't work either, because then any value type, not just the ones that have a class cousin courtesy of Obj-C, is accepted, and storage.setObject(value, forKey: key.name) is no longer valid because value has to be a reference type.

If a protocol existed in Swift that accepted any reference type and any value type that can be converted to a reference type in objective-c (like [Int] and the other examples I mention) this problem would be solved:

func set <T: AnyObjectiveCObject>(key: UDKey<T>, _ value: T) {
    storage.setObject(value, forKey: key.name)
}
func get <T: AnyObjectiveCObject>(key: UDKey<T>) -> T? {
    return storage.objectForKey(key.name) as? T
}

AnyObjectiveCObject would accept any swift classes and swift arrays, dictionaries, numbers (ints, floats, bools, etc that convert to NSNumber), strings...

Unfortunately, AFAIK this doesn't exist.

Question:

How can I have write a generic function (or collection of overloaded generic functions) whose generic type T can be any reference type or any value type that Swift can convert to a reference type in Objective-C?


Solved: With the help of the answers I got, I arrived at what I wanted. In case anyone wants to take a look at my solution, here it is.

like image 533
Alex Avatar asked Jan 25 '15 11:01

Alex


People also ask

Is NSUserDefaults thread safe?

Thread SafetyThe UserDefaults class is thread-safe.

What is NSUserDefaults in Swift?

A property list, or NSUserDefaults can store any type of object that can be converted to an NSData object. It would require any custom class to implement that capability, but if it does, that can be stored as an NSData. These are the only types that can be stored directly.

Where are NSUserDefaults stored?

All the contents saved by NSUserDefaults is saved inside a plist file that can be found under Library -> Preferences -> $AppBundleId.

How do you save NSUserDefaults in Objective C?

static func setObject(value:AnyObject ,key:String) { let pref = NSUserDefaults. standardUserDefaults() pref. setObject(value, forKey: key) pref. synchronize() } static func getObject(key:String) -> AnyObject { let pref = NSUserDefaults.


1 Answers

I don't mean to brag but ... oh who am I kidding, I totally do!

Preferences.set([NSData()], forKey: "MyKey1")
Preferences.get("MyKey1", type: type([NSData]))
Preferences.get("MyKey1") as [NSData]?

func crunch1(value: [NSData])
{
    println("Om nom 1!")
}

crunch1(Preferences.get("MyKey1")!)

Preferences.set(NSArray(object: NSData()), forKey: "MyKey2")
Preferences.get("MyKey2", type: type(NSArray))
Preferences.get("MyKey2") as NSArray?

func crunch2(value: NSArray)
{
    println("Om nom 2!")
}

crunch2(Preferences.get("MyKey2")!)

Preferences.set([[String:[Int]]](), forKey: "MyKey3")
Preferences.get("MyKey3", type: type([[String:[Int]]]))
Preferences.get("MyKey3") as [[String:[Int]]]?

func crunch3(value: [[String:[Int]]])
{
    println("Om nom 3!")
}

crunch3(Preferences.get("MyKey3")!)
like image 187
Vatsal Manot Avatar answered Nov 06 '22 15:11

Vatsal Manot