iosswiftswiftuistateuserdefaults

View not reflecting data coming from User Defaults


I have created a singleton class called AppSecurityStore which currently only holds a boolean property to store a setting value.

final class AppSecurityStore {
    static let shared = AppSecurityStore()
    
    private init() { }
    
    var isAppLockEnabled: Bool {
        get {
            UserDefaults.standard.bool(forKey: "AppLock")
        }
        set {
            UserDefaults.standard.setValue(newValue, forKey: "AppLock")
        }
    }
}

I have a view where I toggle the value for this property.

struct ContentView: View {
    @State private var appSecurityStore: AppSecurityStore
    
    init(appSecurityStore: AppSecurityStore) {
        self.appSecurityStore = appSecurityStore
    }
    
    var body: some View {
        Form {
            Section {
                Toggle("App Lock", isOn: $appSecurityStore.isAppLockEnabled)
            }
        }
    }
}

If I change the toggle value, it does get saved in User Defaults properly. However if I background the app and bring it back to the foreground, it doesn't reflect the updated state. It still shows the previous value!

Gif demonstrating the issue

The appSecurityStore variable inside the view is a @State variable so I'm not sure why it doesn't persist the updated value.

Any help to resolve this is appreciated.

I have uploaded a demo project here.

Important: I cannot use environment objects in this app due to decisions being made to not use them since they don't play well with MVVM.


Solution

  • There is a new property wrapper called @AppStorage for this use case, you may want to check it out here:

    struct ContentView: View {
      @AppStorage("AppLock") private var appLock = false
      var body: some View {
        ...
        Toggle("App Lock", isOn: $appLock)
      }
    }
    

    Updated: In case you want to make it a class for passing-around purposes, you can try this approach. Notice objectWillChange.send() means that whenever isAppLockEnabled changes, it will notify any subscribed views that need to be refreshed.

    final class AppSecurityStore: ObservableObject {
        static let shared = AppSecurityStore()
    
        private init() { }
    
        var isAppLockEnabled: Bool {
            get {
                UserDefaults.standard.bool(forKey: "AppLock")
            }
            set {
                UserDefaults.standard.setValue(newValue, forKey: "AppLock")
                objectWillChange.send()
            }
        }
    }
    

    AppSecurityStore need to be a @StateObject too, check this document.

    struct ContentView: View {
        @StateObject private var appSecurityStore: AppSecurityStore = .shared
        ...
    }