swiftuibindingpickerstepper

SwiftUI: Stepper interfere with Binding variable


WHAT I NEED: I've a stepper to set a value. This value is completely different if user is using the app with different units.

WHAT'S THE PROBLEM: While I just play with the unit picker in the settings tab, the value (stored as @AppStorage in the settings tab) in the main tab updates correctly. When I touch the stepper and modify the value in the main tab, if I try to modify the unit in the settings tab, everything mess up! (While the value in the settings tab keeps updating correctly, the value displayed in the main tab doesn't).

SETTING TAB: class

 Defaults: ObservableObject {
    @AppStorage("appUnits") var appUnits: String = "πŸ‡ͺπŸ‡Ί"
    @AppStorage("memorizedDensity") var density: Double = 0.794
}

struct settings: View {
    @ObservedObject var defaults = Defaults()
    
    var body: some View {
        List {
            Section {
                HStack {
                    ViewThatFits {
                        Text("App default Units")
                    }
                    Spacer(minLength: 10)
                    Picker(selection: $defaults.appUnits, label: Text("Unit")) {
                        ForEach(["πŸ‡ͺπŸ‡Ί", "πŸ‡ΊπŸ‡Έ"], id: \.self) {riga in
                            Text(riga)
                        }
                    }
                    .onChange(of: defaults.appUnits) {newValue in
                        if newValue == "πŸ‡ͺπŸ‡Ί" {
                            defaults.density = 0.793
                        } else if newValue == "πŸ‡ΊπŸ‡Έ" {
                            defaults.density = 6.66
                        }
                        // I need to set a different value when the unit changes 
                        print(defaults.density)
                    }
                    .frame(width: 120)
                    .pickerStyle(.segmented)
                }
            }
        }
    }
}

MAIN TAB:

struct stepper: View {
    @ObservedObject var defaults = Defaults()
    
    var body: some View { fuelDatas }
    
    var fuelDatas: some View {
        VStack {
            Text("Fuel density")
            HStack {
                Text(defaults.density, format: .number.precision(.fractionLength(defaults.appUnits == "πŸ‡ͺπŸ‡Ί" ? 3 : 2)))
                Text(defaults.appUnits == "πŸ‡ͺπŸ‡Ί" ? "g/ml" : "lb/US gal")
            }
            
            Stepper("", value: defaults.$density, in: defaults.appUnits == "πŸ‡ͺπŸ‡Ί" ? 0.750...0.850 : 6...7, step: defaults.appUnits == "πŸ‡ͺπŸ‡Ί" ? 0.001 : 0.01)
                .frame(width: 50.0)
        }
    }
}

I know I'm messing something with the binding stuff and I'm probably creating 2 sources of truth, but I can't find what's wrong in the code! Here's the sample project-


Solution

  • Ideally, you should just declare the @AppStorages in your Views, instead of wrapping them in another Defaults class.

    struct stepper: View {
        @AppStorage("appUnits") var appUnits: String = "πŸ‡ͺπŸ‡Ί"
        @AppStorage("memorizedDensity") var density: Double = 0.794
    
        ...
    }
    

    Your current actually works if you take the two views out of the TabView, so it seems like this is another one of those peculiarities that TabView has.

    If you really want a Defaults object for some reason, one way to make this work is to make sure all your views use the same object.

    struct settings: View {
        // do not initialise it here
        @ObservedObject var defaults: Defaults
    
        ...
    }
    
    struct stepper: View {
        // do not initialise it here
        @ObservedObject var defaults: Defaults
    
        ...
    }
    

    In your tab view, declare a @StateObject and pass it down to both the stepper and settings.

    @StateObject var defaults = Defaults()
    
    var body: some View {
        TabView {
            NavigationStack {
                stepper(defaults: defaults)
            }
            .tabItem { Label("stepper", systemImage: "1.circle") }
            
            NavigationStack {
                settings(defaults: defaults)
            }
            .tabItem { Label("setting", systemImage: "2.circle") }
        }
    }
    

    You can also pass it as an @EnvironmentObject,

    NavigationStack {
        stepper()
    }
    .environmentObject(defaults)
    
    ...
    
    struct stepper: View {
        @EnvironmentObject var defaults: Defaults
    
        ...
    }