Swift中的NSUserDefaults - 实现类型安全

Ale*_*lex 10 nsuserdefaults type-safety swift

关于Swift和Cocoa的问题之一就是使用NSUserDefaults,因为没有类型信息,并且总是需要将结果转换为objectForKey您期望获得的结果.这是不安全和不切实际的.我决定解决这个问题,让NSUserDefaults在Swift-land更加实用,并希望在此过程中学到一些东西.这是我一开始的目标:

  1. 完整的类型安全:每个键都有一个与之关联的类型.设置值时,只应接受该类型的值,并且在获取值时,结果应以正确的类型显示
  2. 全局密钥列表,其含义和内容清晰.该列表应易于创建,修改和扩展
  3. 清除语法,尽可能使用下标.例如,这将是完美的:

    3.1.set:UserDefaults [.MyKey] = value

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

  4. 通过自动[取消]归档来支持符合NSCoding协议的类

  5. 支持NSUserDefaults接受的所有属性列表类型

我开始创建这个通用结构:

struct UDKey <T>  {
    init(_ n: String) { name = n }
    let name: String
}
Run Code Online (Sandbox Code Playgroud)

然后我创建了另一个结构,用作应用程序中所有键的容器:

struct UDKeys {}
Run Code Online (Sandbox Code Playgroud)

然后可以将其扩展为在需要的地方添加密钥:

extension UDKeys {
    static let MyKey1 = UDKey<Int>("MyKey1")
    static let MyKey2 = UDKey<[String]>("MyKey2")
}
Run Code Online (Sandbox Code Playgroud)

请注意每个键如何与其关联的类型.它表示要保存的信息的类型.此外,name属性是用作NSUserDefaults的键的字符串.

密钥可以在一个常量文件中列出,或者在每个文件的基础上使用扩展名添加,靠近它们用于存储数据的位置.

然后我创建了一个"UserDefaults"类,负责处理信息的获取/设置:

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

    // ...
}

let UserDefaults = UserDefaultsClass() // or UserDefaultsClass(storage: ...) for further customisation
Run Code Online (Sandbox Code Playgroud)

我们的想法是创建一个特定域的实例,然后以这种方式访问​​每个方法:

let value = UserDefaults.myMethod(...)
Run Code Online (Sandbox Code Playgroud)

我喜欢这种方法,如UserDefaults.sharedInstance.myMethod(...)(太长!)或使用类方法的一切.此外,这允许通过使用具有不同存储值的多个UserDefaultsClass同时与各个域进行交互.

到目前为止,第1项和第2项已经完成,但现在困难的部分已经开始:如何在UserDefaultsClass上实际设计方法以符合其余部分.

例如,让我们从第4项开始.首先我尝试了这个(此代码在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 }
    }
}
Run Code Online (Sandbox Code Playgroud)

但后来我发现Swift不允许通用下标!! 好吧,那么我想我将不得不使用函数.第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 }
}
Run Code Online (Sandbox Code Playgroud)

这很好用:

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

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

注意我怎么不能打电话UserDefaults.get(.MyKey).我必须使用UDKeys.MyKey.我不能这样做,因为它还不可能在通用结构上有静态变量!

接下来,让我们尝试5号.现在这很令人头疼,这就是我需要很多帮助的地方.

根据文档,属性列表类型是:

默认对象必须是属性列表,即(或集合的实例组合)的实例:NSData,NSString,NSNumber,NSDate,NSArray或NSDictionary.

在斯威夫特手段Int,[Int],[[String:Bool]],[[String:[Double]]],等都是属性列表类型.起初我以为我可以写这个并信任使用此代码的人记住只允许plist类型:

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
}
Run Code Online (Sandbox Code Playgroud)

但正如你会注意到的,虽然这很好:

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

UserDefaults.set(UDKeys.MyKey, NSData())
let d = UserDefaults.get(UDKeys.MyKey)
Run Code Online (Sandbox Code Playgroud)

这不是:

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

这也不是:

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

甚至不是这样的:

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

问题是它们都是有效的属性列表类型,但Swift显然将数组和整数解释为结构,而不是它们的Objective-C类对应物.然而:

func set <T: Any>(key: UDKey<T>, _ value: T)
Run Code Online (Sandbox Code Playgroud)

也不会工作,因为任何值类型,不仅仅是具有类似于Obj-C的类表兄弟的类型,都被接受,并且storage.setObject(value, forKey: key.name)不再有效,因为值必须是引用类型.

如果Swift中存在一个协议,它接受任何引用类型和任何可以转换为objective-c中的引用类型的值类型(比如[Int]我提到的其他示例),这个问题就可以解决了:

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
}
Run Code Online (Sandbox Code Playgroud)

AnyObjectiveCObject 会接受任何快速类和快速数组,字典,数字(整数,浮点数,布尔值等转换为NSNumber),字符串......

不幸的是,AFAIK这个不存在.

题:

如何编写泛型函数(或重载泛型函数的集合),其泛型类型T可以是任何引用类型 Swift可以在Objective-C中转换为引用类型的任何类型?


解决:在我得到的答案的帮助下,我到达了我想要的东西.如果有人想看看我的解决方案,这里就是.

Vat*_*not 4

我不是故意吹嘘但是......哦,我在开玩笑,我完全做到了!

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")!)
Run Code Online (Sandbox Code Playgroud)