Kamilpro
Kamilpro

Reputation: 55

Userdefaults with published enum

try to save user setting, but UserDefaults is not working, Xcode 12.3, swiftui 2.0, when I am reload my app, my setting not updating for new value)

 class PrayerTimeViewModel: ObservableObject {
@Published var lm = LocationManager()
@Published var method: CalculationMethod = .dubai {
    didSet {
        UserDefaults.standard.set(method.params, forKey: "method")
        self.getPrayerTime()
    }
}
  
func getPrayerTime() {
    let cal = Calendar(identifier: Calendar.Identifier.gregorian)
    let date = cal.dateComponents([.year, .month, .day], from: Date())
    let coordinates = Coordinates(latitude: lm.location?.latitude ?? 0.0, longitude: lm.location?.longitude ?? 0.0)
    var par = method.params
    par.madhab = mashab
    self.times = PrayerTimes(coordinates: coordinates, date: date, calculationParameters: par)
}

and view.. update with AppStorage

 struct MethodView: View {

@ObservedObject var model: PrayerTimeViewModel
@Environment(\.presentationMode) var presentationMode
@AppStorage("method", store: UserDefaults(suiteName: "method")) var method: CalculationMethod = .dubai
var body: some View {
    List(CalculationMethod.allCases, id: \.self) { item in
        Button(action: {
            self.model.objectWillChange.send()
            self.presentationMode.wrappedValue.dismiss()
            self.model.method = item
            method = item
        }) {
            HStack {
                Text("\(item.rawValue)")
                if model.method == item {
                    Image(systemName: "checkmark")
                        .foregroundColor(.black)
                }
            }
        }
    }
}

}

Upvotes: 1

Views: 627

Answers (2)

Andrew
Andrew

Reputation: 28539

You have two issues.

First, as I mentioned in my comment above that you are using two different suites for UserDefaults. This means that you are storing and retrieving from two different locations. Either use UserDefaults.standard or use the one with your chosen suite UserDefaults(suitName: "method") - you don't have to use a suite unless you plan on sharing your defaults with other extensions then it would be prudent to do so.

Secondly you are storing the wrong item in UserDefaults. You are storing a computed property params rather than the actual enum value. When you try to retrieve the value it fails as it is not getting what it expects and uses the default value that you have set.

Here is a simple example that shows what you could do. There is a simple enum that has a raw value (String) and conforms to Codable, it also has a computed property. This matches your enum.

I have added an initialiser to my ObservableObject. This serves the purpose to populate my published Place from UserDefaults when the Race object is constructed.

Then in my ContentView I update the place depending on a button press. This updates the UI and it updates the value in UserDefaults.

This should be enough for you to understand how it works.

enum Place: String, Codable {
    case first
    case second
    case third
    case notPlaced

    var someComputedProperty: String {
        "Value stored: \(self.rawValue)"
    }
}

class Race: ObservableObject {
    @Published var place: Place = .notPlaced {
        didSet {
            // Store the rawValue of the enum into UserDefaults
            // We can store the actual enum but that requires more code
            UserDefaults.standard.setValue(place.rawValue, forKey: "method")

            // Using a custom suite
            // UserDefaults(suiteName: "method").setValue(place.rawValue, forKey: "method")
        }
    }

    init() {
        // Load the value from UserDefaults if it exists
        if let rawValue = UserDefaults.standard.string(forKey: "method") {
            // We need to nil-coalesce here as this is a failable initializer
            self.place = Place(rawValue: rawValue) ?? .notPlaced
        }

        // Using a custom suite
        // if let rawValue = UserDefaults(suiteName: "method")?.string(forKey: "method") {
        //    self.place = Place(rawValue: rawValue) ?? .notPlaced
        // }
    }
}

struct ContentView: View {
    @StateObject var race: Race = Race()

    var body: some View {
        VStack(spacing: 20) {
            Text(race.place.someComputedProperty)
                .padding(.bottom, 20)
            Button("Set First") {
                race.place = .first
            }
            Button("Set Second") {
                race.place = .second
            }
            Button("Set Third") {
                race.place = .third
            }
        }
    }
}

Addendum:

Because the enum conforms to Codable it would be possible to use AppStorage to read and write the property. However, that won't update the value in your ObservableObject so they could easily get out of sync. It is best to have one place where you control a value. In this case your ObservableObject should be the source of truth, and all updates (reading and writing to UserDefaults) should take place through there.

Upvotes: 2

Asperi
Asperi

Reputation: 257937

You write in one UserDefaults domain but read from the different. Assuming your intention is to use suite only UserDefaults, you should change one in model, like

@Published var method: CalculationMethod = .dubai {
    didSet {
        UserDefaults(suiteName: "method").set(method.params, forKey: "method")
        self.getPrayerTime()
    }
}

or if you want to use standard then just use AppStorage with default constructor, like

// use UserDefaults.standard by default
@AppStorage("method") var method: CalculationMethod = .dubai

Upvotes: 1

Related Questions