Search code examples
swiftuiuserdefaultsappstorage

Global Updates With UserDefaults In SwiftUI


I've been using @AppStorage and UserDefaults for updates in SwiftUI. If I make a change to the vie that has the @AppStorage wrapper all works well. I'm confused with how to make this work globally.

I'm using a struct that has computed properties and formatters associated. The idea is to check user defaults and convert items to lbs or kg. The issue is that the views using the computed properties do not update when UserDefaults is updated. Is there a way to create a global change that would update weightFormatted in SecondaryView below?

// Weight Struct

struct Weight {
var weight: Double
var weightFormatted: String {
return weightDecimalLbsOrKgFormatted2(weight)
}

// Formatting Method

func weightDecimalLbsOrKgFormatted2(_ lbs: Double) -> String {
    if (!UserDefaults.standard.bool(forKey: "weightInKilograms")) {
        let weightString = decimalFormatterDecimal2(lbs)
        return weightString + "lbs"
        
    } else {
        let kg = toKg(lbs)
        let weightString = decimalFormatterDecimal2(kg)
        return weightString + "kg"
    }
}

// Where weightInKilograms Is Set

struct AccountView: View {
    @AppStorage("weightInKilograms") var weightInKilograms = false

    let weight = Weight(weight: 9.0))
    
    var body: some View {
        VStack {
            Text(weight.weightFormatted)
            Toggle(isOn: $weightInKilograms) {
                        Text("Kilograms")
            }
        }
}
}

// Secondary View Not Updating

struct SecondaryView: View {

    let weight = Weight(weight: 9.0))
    
    var body: some View {
         Text(weight.weightFormatted)
}
}

Solution

  • Your problem is that weight isn't wrapped by any state.

    In your AccountView, give weight a @State wrapper:

    struct AccountView: View {
      
      @AppStorage("weightInKilograms") var weightInKilograms = false
      
      @State var weight = Weight(weight: 9.0))
      
      var body: some View {
        //...
      }
    }
    

    In SecondaryView, ensure that weight is wrapped with @Binding:

    struct SecondaryView: View {
      
      @Binding var weight: Weight
      
      var body: some View {
        // ...
      }
    }
    

    Then, pass weight as a Binding<Weight> variable to SecondaryView within your first View:

    SecondaryView(weight: $weight)
    

    Is there a way to create a global change that would update weightFormatted in SecondaryView below?

    If you're looking to make a global change, you should consider setting up a global EnvironmentObject:

    class MyGlobalClass: ObservableObject {
    
      // Published variables will update view states when changed.
      @Published var weightInKilograms: Bool
      { get {
        // Get UserDefaults here
      } set {
        // Set UserDefaults here
      }}
    
      @Published var weight: Weight
    }
    

    If you pass an instance of MyGlobalClass as an EnvironmentObject to your main view, then to your secondary view, any changes made to properties in the global instance will update the views' state via the @Published wrapper:

    let global = MyGlobalClass()
    
    /* ... */
    
    // In your app's lifecycle, or where AccountView is instantiated
    AccountView().environmentObject(global)
    
    struct AccountView: View {
      @EnvironmentObject var global: MyGlobalClass
    
      var body: some View {
        // ...
        Text(global.weight.weightFormatted)
        // ...
        SecondaryView().environmentObject(global)
      }
    }