Swift: Add property to a struct that gets encoded to JSON

Viewed 469

I have a Struct for the user's preferences and this object gets encoded and stored as a JSON file and stored for iCloud Document Sync (I found this to be more reliable than NSUbiquitousContainer, even though that's what you're supposed to use for preferences).

Let's say in v1 of the app the struct looks like this and all users have a JSON file with these 3 properties.

struct Preferences: Codable {
    var soundsDisabled: Bool = false
    var hapticsDisabled: Bool = false
    var badgeNumberEnabled: Bool = false
}

What I need to figure out is if there is a way to add a 4th property in v1.1 to this Struct without messing up the existing JSON. Right now if I do add a new property, everything gets reset to the default values and the user's preferences get lost.

UPDATE: this is how this struct gets encoded to JSON

extension Storage {
    
    static var preferences: Preferences {
        get {
            guard
                let data = try? Data(contentsOf: Storage.preferencesFile),
                let decoded = try? JSONDecoder().decode(Preferences.self, from: data)
            else { return Preferences() }
            return decoded
        }
        set {
            do { try JSONEncoder().encode(newValue).write(to: Storage.preferencesFile, options: .atomic) }
            catch let error {
                print("Failed to write Preferences: \(error.localizedDescription)")
                MSAnalytics.trackEvent("Failed to write preferences", withProperties: ["Error": error.localizedDescription])
            }
        }
    }

}

Thank you.

1 Answers

This probably happens because JSONDecoder cannot decode the stored data as a Preferences instance any more. (and in that case you are returning a new instance Preferences())

What you can do is to add your new property as an optional, like this:

struct Preferences: Codable {
    var soundsDisabled: Bool = false
    var hapticsDisabled: Bool = false
    var badgeNumberEnabled: Bool = false
    var newProperty: Bool? = false
}

Then after decoding the old data, newProperty will be nil.

Update: Another, maybe better, solution would be to implement decoding yourself and when there is no property newProperty set the default value:

struct Preferences: Codable {
    var soundsDisabled: Bool = false
    var hapticsDisabled: Bool = false
    var badgeNumberEnabled: Bool = false
    var newProperty: Bool = false
    
    enum CodingKeys: String, CodingKey {
        case soundsDisabled
        case hapticsDisabled
        case badgeNumberEnabled
        case newProperty
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        soundsDisabled = try container.decode(Bool.self, forKey: .soundsDisabled)
        hapticsDisabled = try container.decode(Bool.self, forKey: .hapticsDisabled)
        badgeNumberEnabled = try container.decode(Bool.self, forKey: .badgeNumberEnabled)

        // Decode newProperty and if not present set default value
        newProperty = try container.decodeIfPresent(Bool.self, forKey: .newProperty) ?? false
    }
}
Related