Home > Software engineering >  SwiftUI: UserDefaults Binding
SwiftUI: UserDefaults Binding

Time:08-29

I have a settings view that has a button which toggles a binding that's stored with UserDefaults.

struct Settings: View {
    @ObservedObject var settingsVM = SetttingsViewModel()

    var body: some View {
        if settingsVM.settingActivated {
            Text("Setting activated")
        } else {
            Text("Setting deactivated")
        }
        
        Button("Activate") {
            settingsVM.settingActivated.toggle()
        }
        

    }
    
}

SettingsViewModel

class SetttingsViewModel: ObservableObject {
    
    @Published  var settingActivated: Bool = UserDefaults.standard.bool(forKey: "settingActivated") {
    didSet {
        UserDefaults.standard.set(self.settingActivated, forKey: "settingActivated")
      }
    }
    
}

The text("Setting activated/ Setting deactivated")in the Settings view update instantly when i press the button but the text in ContentView doesn't change unless i restart the app & i have no idea why.

struct ContentView: View {
    @ObservedObject var settingsVM = SetttingsViewModel()
    @State private var showsettings = false
    var body: some View {
        
        if settingsVM.settingActivated {
            
            Text("Setting Activated")
                .padding(.top)
        } else {
            Text("Setting Deactivated")
                .padding(.top)
        }
            Button("Show Settings") {
                showsettings.toggle()
                
            }
            .sheet(isPresented: $showsettings) {
                Settings()
                   }
            .frame(width: 300, height: 300)
       }
    }

This is for a macOS 10.15 app so i can't use @AppStorage

CodePudding user response:

Right now, you don't have any code in you view model to react to a change in UserDefaults. Meaning, if UserDefaults gets a new value set, it won't know about it. And, since you're using a different instance of SettingsViewModel in your two different views, they can easily become out-of-sync.

The easiest change would be to pass the same instance of SettingsViewModel to Settings:

struct Settings: View {
    @ObservedObject var settingsVM: SettingsViewModel //<-- Here

    var body: some View {
        if settingsVM.settingActivated {
            Text("Setting activated")
        } else {
            Text("Setting deactivated")
        }
        
        Button("Activate") {
            settingsVM.settingActivated.toggle()
        }
    }
}

struct ContentView: View {
    @ObservedObject var settingsVM = SetttingsViewModel()
    @State private var showsettings = false
    var body: some View {
        
        if settingsVM.settingActivated {
            
            Text("Setting Activated")
                .padding(.top)
        } else {
            Text("Setting Deactivated")
                .padding(.top)
        }
            Button("Show Settings") {
                showsettings.toggle()
                
            }
            .sheet(isPresented: $showsettings) {
                Settings(settingsVM: settingsVM) //<-- Here
            }
            .frame(width: 300, height: 300)
       }
    }

Another option would be to use a custom property wrapper (like AppStorage, but available to earlier targets): https://xavierlowmiller.github.io/blog/2020/09/04/iOS-13-AppStorage


Also, @vadian's comment is important -- if you had access to it, you'd want to use @StateObject. But, since you don't, it's important to store your ObservableObject at the top level so it doesn't get recreated.

  • Related