NSUserDefaults: storing struct/class with only property list compliant types, in a readable format

╄→гoц情女王★ 提交于 2019-12-11 15:36:08

问题


Suppose a dictionary is stored in UserDefaults according to the following code:

UserDefaults.standard.set(["name": "A preset", "value": 1], forKey: "preset")

The plist that results from running this command is:

<?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>

Now, consider this data should be represented by the following struct:

struct Preset: Codable {
    var name: String
    var value: Int
}

I'd like to call the following code and get the same results as above (data stored in the plist using exactly the same layout):

UserDefaults.standard.set(Preset(name: "A preset", value: 1), forKey: "preset")

Unfortunately this results in an error:

Attempt to set a non-property-list object
TableViewToUserDefaults.Preset(name: "A preset", value: 1)
as an NSUserDefaults/CFPreferences value for key preset

How can I achieve this, keeping the same plist layout, and if possible in a generic way? (i.e. one which works for any struct consisting of properties that can be encoded in a plist, without hardcoding the struct's properties such as name and value in this case)


回答1:


You can make a protocol-oriented way that will solve your problem.

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)
  }
}

Just make any Codable type conform to UserDefaultStorable then. This approach is very useful because let's say you have another struct:

struct User: Codable {
  let name: String
  let id: Int
}

Instead of defining separate functions on UserDefaults, you just need this one-liner:

extension User: UserDefaultStorable {}



回答2:


One solution would be to write a function on the Struct that converts it to a dictionary.

struct Preset {
     var name: String
     var value: Int

     func toDictionary() -> [String:Any] {
         return ["name": self.name, "value": self.value]
     }
}

Then, to save it to UserDefaults, you can simply do this:

let p = Preset(name: "A preset", value: 1)
UserDefaults.standard.set(p.toDictionary(), forKey: "preset")

Hope it helps!




回答3:


The following extension to UserDefaults solves the problem, and I didn't generalize it for lack of time, but it may be possible:

extension UserDefaults {
    func set(_ preset: Preset, forKey key: String) {
        set(["name": preset.name, "value": preset.value], forKey: key)
    }
}

This can work on arrays as well:

extension UserDefaults {
    func set(_ presets: [Preset], forKey key: String) {
        let result = presets.map { ["name":$0.name, "value":$0.value] }
        set(result, forKey: key)
    }
}

While UserDefaults.standard.set(:forKey:) is what the question was about, my goal was actually to get it working with Cocoa bindings for use with NSArrayController. I decided to subclass NSArrayController as follows (see comment by Hamish to my other question, which was the last missing piece of the puzzle to make this generic):

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)
    }
}


来源:https://stackoverflow.com/questions/54390160/nsuserdefaults-storing-struct-class-with-only-property-list-compliant-types-in

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!