NSUserDefaults:以可读格式存储 struct/class 只有 属性 列表兼容类型
NSUserDefaults: storing struct/class with only property list compliant types, in a readable format
假设一个字典按照以下代码存储在UserDefaults中:
UserDefaults.standard.set(["name": "A preset", "value": 1], forKey: "preset")
运行 这个命令产生的 plist 是:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>preset</key>
<dict>
<key>name</key>
<string>A preset</string>
<key>value</key>
<integer>1</integer>
</dict>
</dict>
</plist>
现在,考虑该数据应由以下结构表示:
struct Preset: Codable {
var name: String
var value: Int
}
我想调用下面的代码并得到与上面相同的结果(数据存储在 plist 中使用完全相同的布局):
UserDefaults.standard.set(Preset(name: "A preset", value: 1), forKey: "preset")
不幸的是,这会导致错误:
Attempt to set a non-property-list object
TableViewToUserDefaults.Preset(name: "A preset", value: 1)
as an NSUserDefaults/CFPreferences value for key preset
我怎样才能做到这一点,保持相同的 plist 布局,如果可能的话,以通用的方式? (即适用于由可以在 plist 中编码的属性组成的任何结构,而无需硬编码结构的属性,例如 name
和 value
在这种情况下)
下面对UserDefaults的扩展解决了这个问题,时间不够我没有概括,但有可能:
extension UserDefaults {
func set(_ preset: Preset, forKey key: String) {
set(["name": preset.name, "value": preset.value], forKey: key)
}
}
这也适用于数组:
extension UserDefaults {
func set(_ presets: [Preset], forKey key: String) {
let result = presets.map { ["name":[=11=].name, "value":[=11=].value] }
set(result, forKey: key)
}
}
虽然 UserDefaults.standard.set(:forKey:)
是问题所在,但我的目标实际上是让它与 Cocoa 绑定一起使用以与 NSArrayController
一起使用。我决定按如下方式对 NSArrayController
进行子类化(参见 Hamish 对 的评论,这是使这个通用的最后一个缺失的拼图):
extension Encodable {
fileprivate func encode(to container: inout SingleValueEncodingContainer) throws {
try container.encode(self)
}
}
struct AnyEncodable: Encodable {
var value: Encodable
init(_ value: Encodable) {
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try value.encode(to: &container)
}
}
class NSEncodableArrayController: NSArrayController {
override func addObject(_ object: Any) {
let data = try! PropertyListEncoder().encode(AnyEncodable(object as! Encodable))
let any = try! PropertyListSerialization.propertyList(from: data, options: [], format: nil)
super.addObject(any)
}
}
一种解决方案是在 Struct 上编写一个函数,将其转换为字典。
struct Preset {
var name: String
var value: Int
func toDictionary() -> [String:Any] {
return ["name": self.name, "value": self.value]
}
}
然后,要将其保存到 UserDefaults,您只需这样做:
let p = Preset(name: "A preset", value: 1)
UserDefaults.standard.set(p.toDictionary(), forKey: "preset")
希望对您有所帮助!
你可以做一个面向协议的方式来解决你的问题。
protocol UserDefaultStorable: Codable {
// where we store the item
var key: String { get }
// use to actually load/store
func store(in userDefaults: UserDefaults) throws
init(from userDefaults: UserDefaults) throws
}
enum LoadError: Error {
case fail
}
// Default implementations
extension UserDefaultStorable {
var key: String { return "key" }
func store(in userDefaults: UserDefaults) throws {
userDefaults.set(try JSONEncoder().encode(self), forKey: key)
}
init(from userDefaults: UserDefaults) throws {
guard let data = userDefaults.data(forKey: key) else { throw LoadError.fail }
self = try JSONDecoder().decode(Self.self, from: data)
}
}
然后让任何 Codable
类型符合 UserDefaultStorable
。这种方法非常有用,因为假设您有另一个结构:
struct User: Codable {
let name: String
let id: Int
}
无需在 UserDefaults
上定义单独的函数,您只需要一行代码:
extension User: UserDefaultStorable {}
假设一个字典按照以下代码存储在UserDefaults中:
UserDefaults.standard.set(["name": "A preset", "value": 1], forKey: "preset")
运行 这个命令产生的 plist 是:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>preset</key>
<dict>
<key>name</key>
<string>A preset</string>
<key>value</key>
<integer>1</integer>
</dict>
</dict>
</plist>
现在,考虑该数据应由以下结构表示:
struct Preset: Codable {
var name: String
var value: Int
}
我想调用下面的代码并得到与上面相同的结果(数据存储在 plist 中使用完全相同的布局):
UserDefaults.standard.set(Preset(name: "A preset", value: 1), forKey: "preset")
不幸的是,这会导致错误:
Attempt to set a non-property-list object
TableViewToUserDefaults.Preset(name: "A preset", value: 1)
as an NSUserDefaults/CFPreferences value for key preset
我怎样才能做到这一点,保持相同的 plist 布局,如果可能的话,以通用的方式? (即适用于由可以在 plist 中编码的属性组成的任何结构,而无需硬编码结构的属性,例如 name
和 value
在这种情况下)
下面对UserDefaults的扩展解决了这个问题,时间不够我没有概括,但有可能:
extension UserDefaults {
func set(_ preset: Preset, forKey key: String) {
set(["name": preset.name, "value": preset.value], forKey: key)
}
}
这也适用于数组:
extension UserDefaults {
func set(_ presets: [Preset], forKey key: String) {
let result = presets.map { ["name":[=11=].name, "value":[=11=].value] }
set(result, forKey: key)
}
}
虽然 UserDefaults.standard.set(:forKey:)
是问题所在,但我的目标实际上是让它与 Cocoa 绑定一起使用以与 NSArrayController
一起使用。我决定按如下方式对 NSArrayController
进行子类化(参见 Hamish 对
extension Encodable {
fileprivate func encode(to container: inout SingleValueEncodingContainer) throws {
try container.encode(self)
}
}
struct AnyEncodable: Encodable {
var value: Encodable
init(_ value: Encodable) {
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try value.encode(to: &container)
}
}
class NSEncodableArrayController: NSArrayController {
override func addObject(_ object: Any) {
let data = try! PropertyListEncoder().encode(AnyEncodable(object as! Encodable))
let any = try! PropertyListSerialization.propertyList(from: data, options: [], format: nil)
super.addObject(any)
}
}
一种解决方案是在 Struct 上编写一个函数,将其转换为字典。
struct Preset {
var name: String
var value: Int
func toDictionary() -> [String:Any] {
return ["name": self.name, "value": self.value]
}
}
然后,要将其保存到 UserDefaults,您只需这样做:
let p = Preset(name: "A preset", value: 1)
UserDefaults.standard.set(p.toDictionary(), forKey: "preset")
希望对您有所帮助!
你可以做一个面向协议的方式来解决你的问题。
protocol UserDefaultStorable: Codable {
// where we store the item
var key: String { get }
// use to actually load/store
func store(in userDefaults: UserDefaults) throws
init(from userDefaults: UserDefaults) throws
}
enum LoadError: Error {
case fail
}
// Default implementations
extension UserDefaultStorable {
var key: String { return "key" }
func store(in userDefaults: UserDefaults) throws {
userDefaults.set(try JSONEncoder().encode(self), forKey: key)
}
init(from userDefaults: UserDefaults) throws {
guard let data = userDefaults.data(forKey: key) else { throw LoadError.fail }
self = try JSONDecoder().decode(Self.self, from: data)
}
}
然后让任何 Codable
类型符合 UserDefaultStorable
。这种方法非常有用,因为假设您有另一个结构:
struct User: Codable {
let name: String
let id: Int
}
无需在 UserDefaults
上定义单独的函数,您只需要一行代码:
extension User: UserDefaultStorable {}