BlackWolf
BlackWolf

Reputation: 5599

How to encode a dictionary of Codables in Swift?

I have a [String: Codable] dictionary in Swift that I want to save into user defaults, but I'm struggeling to do so.

I've tried converting it to Data using

try! JSONSerialization.data(withJSONObject: dictionary, options: .init(rawValue: 0))

But this crashes ("Invalid type in JSON write (_SwiftValue)")

I've tried using JSONEncoder:

JSONEncoder().encode(dictionary)

but this will not compile ("Generic parameter T could not be inferred").

Of course I could manually transform all my Codables into [String: Any] and then write it to user defaults, but since the whole point of Codable is to make Decoding and Encoding easy, I'm not quite sure why the two solutions above are not possible (particularly the second one)?

Example:

For reproducibility you can use this code in a Playground:

import Foundation

struct A: Codable {}
struct B: Codable {}

let dict = [ "a": A(), "b": B() ] as [String : Codable]
let data = try JSONEncoder().encode(dict)

Upvotes: 4

Views: 6842

Answers (2)

MikeElmwood
MikeElmwood

Reputation: 330

UserDefaults has a way to save [String: Any] dictionaries:

let myDictionary: [String: Any] = ["a": "one", "b": 2]
UserDefaults.standard.set(myDictionary, forKey: "key")
let retrievedDictionary: [String: Any] = UserDefaults.standard.dictionary(forKey: "key")!
print(retrievedDictionary)      // prints ["a": one, "b": 2]

However, if your dictionary is a property of an object that you want to save to UserDefaults you need to implement the Codable protocol for your object. The easiest way I know is to convert the dictionary to a Data object using JSONSerialization. The following code works for me:

class MyObject: Codable {

    let dictionary: [String: Any]

    init(dictionary: [String: Any]) {
        self.dictionary = dictionary
    }

    enum CodingKeys: String, CodingKey {
        case dictionary
    }

    public required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        if values.contains(.dictionary), let jsonData = try? values.decode(Data.self, forKey: .dictionary) {
            dictionary = (try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]) ??  [String: Any]()
        } else {
            dictionary = [String: Any]()
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        if !dictionary.isEmpty, let jsonData = try? JSONSerialization.data(withJSONObject: dictionary) {
            try container.encode(jsonData, forKey: .dictionary)
        }
    }
}

To save and retrieve MyObject from UserDefaults you can then do this:

extension UserDefaults {

    func set(_ value: MyObject, forKey defaultName: String) {
        guard let data = try? PropertyListEncoder().encode(value) else { return }
        set(data, forKey: defaultName)
    }

    func myObject(forKey defaultName: String) -> MyObject? {
        guard let data = data(forKey: defaultName) else { return nil }
        return try? PropertyListDecoder().decode(MyObject.self, from: data)
    }
}

Upvotes: 2

ielyamani
ielyamani

Reputation: 18581

Codable, as a generic constraint, and Any are not encodable. Use a struct instead of a dictionary:

struct A: Codable {
    let a = 0
}
struct B: Codable {
    let b = "hi"
}
struct C: Codable {
    let a: A
    let b: B
}

let d = C(a: A(), b: B())
let data = try JSONEncoder().encode(d)

Upvotes: 1

Related Questions