skip to Main Content

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.

2

Answers


  1. you need to observe the changes, for the moment you pass a copy of the AppSecurityStore not a refference.
    conform to ObservableObject and use the @Publish:

    final class AppSecurityStore: ObservableObject {
        static let shared = AppSecurityStore()
        
        private init() { }
    
        @Published var isAppLockEnabled: Bool {
            get {
                UserDefaults.standard.bool(forKey: "AppLock")
            }
            set {
                UserDefaults.standard.setValue(newValue, forKey: "AppLock")
            }
        }
    }
    
    struct ContentView: View {
        @ObservedObject private var appSecurityStore: AppSecurityStore
        
        init(appSecurityStore: AppSecurityStore = .shared) {
            self.appSecurityStore = appSecurityStore
        }
        
        var body: some View {
            Form {
                Section {
                    Toggle("App Lock", isOn: $appSecurityStore.isAppLockEnabled)
                }
            }
        }
    }
    

    update:
    initiate the IsAppLock in init and use the didSet to update the changes

    final class AppSecurityStore: ObservableObject {
        static let shared = AppSecurityStore()
        
        @Published var isAppLockEnabled: Bool {
            didSet {
                UserDefaults.standard.set(isAppLockEnabled, forKey: "AppLock")
            }
        }
        
        private init() {
            self.isAppLockEnabled = UserDefaults.standard.bool(forKey: "AppLock")
        }
    }
    
    Login or Signup to reply.
  2. 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
        ...
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search