Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating a saved Codable struct to add a new property

My app uses a codable struct like so:

public struct UserProfile: Codable {

     var name: String = ""
     var completedGame: Bool = false

     var levelScores: [LevelScore] = []
}

A JSONEncoder() has been used to save an encoded array of UserProfiles to user defaults.

In an upcoming update, I'd like to add a new property to this UserProfile struct. Is this possible in some way?

or would I need to create a new Codable struct that has the same properties plus one new property, and then copy all the values over to the new struct and then start using that new struct in place of anywhere that the UserProfile struct was previously used?

If I simply add a new property to the struct, then I won't be able to load the previous encoded array of UserProfiles as the struct will no longer have matching properties. When I get to this code for loading the saved users:

if let savedUsers = UserDefaults.standard.object(forKey: "SavedUsers") as? Data {
       let decoder = JSONDecoder()
       if let loadedUsers = try? decoder.decode([UserProfile].self, from: savedUsers) {

loadedUsers doesn't decode if the properties that UserProfile had when they were encoded and saved do not contain all the current properties of the UserProfile struct.

Any suggestions for updating the saved struct properties? Or must I go the long way around and re-create since I didn't already plan ahead for this property to be included previously?

Thanks for any help!

like image 851
RanLearns Avatar asked Jan 02 '20 04:01

RanLearns


1 Answers

As mentioned in the comments you can make the new property optional and then decoding will work for old data.

var newProperty: Int?

Another option is to apply a default value during the decoding if the property is missing.

This can be done by implementing init(from:) in your struct

public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    completedGame = try container.decode(Bool.self, forKey: .completedGame)
    do {
        newProperty = try container.decode(Int.self, forKey: .newProperty)
    } catch {
        newProperty = -1
    }
    levelScores = try container.decode([LevelScore].self, forKey: .levelScores)
}

This requires that you define a CodingKey enum

enum CodingKeys: String, CodingKey {
    case name, completedGame, newProperty, levelScores
}

A third option if you don't want it to be optional when used in the code is to wrap it in a computed non-optional property, again a default value is used. Here _newProperty will be used in the stored json but newProperty is used in the code

private var _newProperty: Int?
var newProperty: Int {
    get {
        _newProperty ?? -1
    }
    set {
        _newProperty = newValue
    }
}
like image 124
Joakim Danielson Avatar answered Oct 31 '22 16:10

Joakim Danielson