Save to UserDefaults Getting Called 19 Times

205 Views Asked by At

Hey is it normal for some SwiftUI code to get called multiple times as a result of a state change or save operation? This code is a picker with a choice of 4 options. The selected option is then saved to UserDefaults by the method saveBase. I put a print statement in the method to confirm my value was arriving correctly and discovered that the method is getting called 19 times every time a change is made to the picker. The code is working fine saving and restoring baseCurr. As a Firmware Engineer with experience in assembly and C I would think this is pretty buggy code, but I'm not so sure. Any ideas?

class UserData: ObservableObject {
    
    @Published var baseCurr: Int = 0

    func saveBase() -> () {
        let defaults = UserDefaults.standard
        defaults.set(self.baseCurr, forKey: "base")
        
        print("  base Curr = \(self.baseCurr)")
    }
}
struct aboutView: View {
    
    @EnvironmentObject var userData: UserData
    
    let baseCurrs = ["block A", "block B","block C","block D"]
    
    var body: some View {
        
        Form {
            VStack (alignment: .leading) {
                Text("Select Base")
                
                Picker(selection: $userData.baseCurr, label: Text("Curr >")) {
                    ForEach(0 ..< baseCurrs.count) {
                        Text( baseCurrs[$0])
                    }
                    .onChange(of: userData.baseCurr) { newValue in
                        userData.saveBase()
                    }
                }
            }
            .padding()
        }
        .navigationBarTitle("About", displayMode: .inline) 
    }
}
2

There are 2 best solutions below

4
On

As NewDev pointed out in the comments, you need to attach the onChange modifier to a single View (like Picker or VStack) instead of ForEach.

However, you can make your code a little bit cleaner by completely removing both the .onChange modifier and the saveBase() function.

You can add didSet to the baseCurr property to save the value whenever it's set:

class UserData: ObservableObject {
    @Published var baseCurr: Int = UserDefaults.standard.integer(forKey: "base") {
        didSet {
            UserDefaults.standard.set(baseCurr, forKey: "base")
            print("  base Curr = \(baseCurr)")
        }
    }
}

Now you don't need onChange:

Picker(selection: $userData.baseCurr, label: Text("Curr >")) {
    ForEach(0 ..< baseCurrs.count) {
        Text(baseCurrs[$0])
    }
}
0
On

Your .onChange is called baseCurrs.count times because you attached it to dynamic ForEach container, so .onChange is actually attached to every Text(baseCurrs[$0]), so to solve this, keeping your code, it needs just move .onChange out of dynamic container, like

VStack (alignment: .leading) {
    Text("Select Base")
    
    Picker(selection: $userData.baseCurr, label: Text("Curr >")) {
        ForEach(0 ..< baseCurrs.count) {
            Text(baseCurrs[$0])
        }
    }
}
.onChange(of: userData.baseCurr) { newValue in   // for eg. here !!
    userData.saveBase()
}
.padding()