0

I try to accomplish having an observable object with a published value training. On every change it should save the custom struct to the user defaults. On every load (AppState init) it should load the data:

class AppState: ObservableObject {

    var trainings: [Training] {
        willSet {
            if let encoded = try? JSONEncoder().encode(trainings) {
                let defaults = UserDefaults.standard
                 defaults.set(encoded, forKey: "trainings")
            }
            objectWillChange.send()
        }
    }

    init() {
        self.trainings = []
        if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data {
            if let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
                self.trainings = loadedTraining
            }
        }
    }

}

I know if this is best practice, but I want to save the data locally. The code I wrote is not working and I can't figure out why.

I'm a beginner and I never stored data to a device.

T. Karter
  • 638
  • 7
  • 25

1 Answers1

4

Each time you call the init method the first line resets the value stored in UserDefaults and in-turn returns the empty array instead of the value that was previously stored. Try this modification to your init method to fix it:

init() {
    if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
        let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
        self.trainings = loadedTraining
    } else {
        self.trainings = []
    }
}

Better Approach: A much better approach would to modify your trainings property to have a get and set instead of the current setup. Here is an example:

var trainings: [Training] {
    set {
        if let encoded = try? JSONEncoder().encode(newValue) {
            let defaults = UserDefaults.standard
             defaults.set(encoded, forKey: "trainings")
        }
        objectWillChange.send()
    }
    get {
        if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
            let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
            return loadedTraining
        }
        return []
    }
}

Note: This can again be improved using Swift 5.1's @PropertyWrapper. Let me know in the comments if anyone wants me to include that as well in the answer.

Update: Here's the solution that makes it simpler to use UserDefaults using Swift's @PropertyWrapper as you have requested for:-

@propertyWrapper struct UserDefault<T: Codable> {
    var key: String
    var wrappedValue: T? {
        get {
            if let data = UserDefaults.standard.object(forKey: key) as? Data {
                return try? JSONDecoder().decode(T.self, from: data)
            }
            return nil
        }
        set {
            if let encoded = try? JSONEncoder().encode(newValue) {
                UserDefaults.standard.set(encoded, forKey: key)
            }
        }
    }
}
class AppState: ObservableObject {
    @UserDefault(key: "trainings") var trainings: [Training]?
    @UserDefault(key: "anotherProperty") var anotherPropertyInUserDefault: AnotherType?
}
Frankenstein
  • 15,732
  • 4
  • 22
  • 47
  • This ends in `Return from initializer without initializing all stored properties`. How do I need to modify the init() with the better approach? – T. Karter May 31 '20 at 19:40
  • 1
    Give the default `[]` when it's unsuccessful to set all the properties. I've updated check now. – Frankenstein May 31 '20 at 19:46
  • Would be great of you could include the `@PropertryWrapper` solution as I am trying to use the latest state of coding.n Thank you! – T. Karter May 31 '20 at 20:07
  • just want to bring you question back in mind - a PropertyWrapper solution would be very helpful – T. Karter Jun 02 '20 at 10:33
  • Was busy due to work, I'll be updated the PropertyWrapper solution soon. – Frankenstein Jun 02 '20 at 10:47
  • this is a very interesting solution with the @PropertyWraper. I am not 100% sure how to implement this. Lets say, I have the following: **class AppState: ObservableObject { @UserDefault(key: "achievements") var achievements: [String : Bool]? }** How can I read and write to it? In my test examples the value is always empty: e.g. **AppState().achievements?["Easy"] = true** ... is this because you cannot combine this type of dictionary? – Nathanael Tse Jan 17 '23 at 13:53