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