Supposons qu'un dictionnaire est stocké dans UserDefaults selon le code suivant:

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

Le plist qui résulte de l'exécution de cette commande est:

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

Maintenant, considérez que ces données doivent être représentées par la structure suivante:

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

Je voudrais appeler le code suivant et obtenir les mêmes résultats que ci-dessus (données stockées dans le plist en utilisant exactement la même disposition):

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

Malheureusement, cela entraîne une erreur:

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

Comment puis-je y parvenir, en conservant la même disposition plist, et si possible de manière générique? (c'est-à-dire qui fonctionne pour toute structure composée de propriétés pouvant être encodées dans un plist, sans coder en dur les propriétés de la structure telles que name et value dans ce cas)

0
swineone 27 janv. 2019 à 19:12

3 réponses

Meilleure réponse

L'extension suivante à UserDefaults résout le problème, et je ne l'ai pas généralisée par manque de temps, mais cela peut être possible:

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

Cela peut également fonctionner sur les tableaux:

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

Bien que UserDefaults.standard.set(:forKey:) soit l'objet de la question, mon objectif était en fait de le faire fonctionner avec les liaisons Cocoa à utiliser avec NSArrayController. J'ai décidé de sous-classer NSArrayController comme suit (voir le commentaire de Hamish à mon autre question, qui était la dernière pièce manquante du puzzle pour rendre ce générique):

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)
    }
}
0
swineone 28 janv. 2019 à 11:46

Vous pouvez créer une méthode orientée protocole qui résoudra votre problème.

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

Rendez simplement n'importe quel type Codable conforme à UserDefaultStorable alors. Cette approche est très utile car disons que vous avez une autre structure:

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

Au lieu de définir des fonctions séparées sur UserDefaults, vous avez juste besoin de cette ligne unique:

extension User: UserDefaultStorable {}
1
Import Accelerate 30 janv. 2019 à 17:38

Une solution serait d'écrire une fonction sur le Struct qui le convertit en dictionnaire.

struct Preset {
     var name: String
     var value: Int

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

Ensuite, pour l'enregistrer dans UserDefaults, vous pouvez simplement faire ceci:

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

J'espère que cela aide!

0
Matthew Knippen 27 janv. 2019 à 16:58