swiftstructforeachswiftuiuistepper

Increment Multiple Struct Variables using ForEach + Stepper SwiftUI


I have a list of toppings to include on a pizza. Each item in the list is a struct:

struct Topping: Identifiable {
    let name: String
    let price: Double
    @State var amount: Int
    let id = UUID()
}

The list is created in a Class in file MenuDataService.swift, is defined using static let, then initialized in a ViewModel file:

//FILE MenuDataService.swift
Class MenuDataService {
    static let toppingsList: [Topping] = [Topping(name: "Cheese", price: 3.00, amount: 0), Topping(name: "Sauce", price: 3.00, amount: 0)]
}

//FILE MenuViewModel.swift
Class MenuViewModel: ObservableObject {
    @Published var toppingsList: [Topping]
    init() {
        toppingsList = MenuDataService.toppingsList
        self.toppingsList = toppingsList
    }
}

Inside the view im accessing the object using .environmentobject in the view. I'm iterating over the list of toppings using a ForEach loop, displaying the name and amount of toppings, followed by a stepper which should increment or decrement the 'amount' variable.

@EnvironmentObject private var vm = MenuViewModel
let toppingsList: [Topping]
var body: some View {
    VStack {
        ForEach(toppingsList, id: \.id) { topping in
            Stepper("\(topping.name): \(topping.amount)", value: topping.$amount)
        }
    }
}

struct ExtraToppingsView_Previews: PreviewProvider {
    static var previews: some View {
        ExtraToppingsView(toppingsList: MenuViewModel.init().toppingsList)
        .environmentObject(MenuViewModel())
    }
}

I get this error in the Stepper() Line of code: Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.

When clicking the '+' in the 'Cheese' stepper, it should increment the variable, and change the title to 'Cheese: 1', but the title stays the same. I'm assuming this has something to do with '@State var amount' variable, or some binding to the '$amount' variable in the stepper. If someone can point me in the right direction, it would be much appreciated.


Solution

  • Most of your code is close -- there are just subtle differences required to make it work. See inline comments for my changes:

    struct Topping: Identifiable {
        let id = UUID()
        let name: String
        let price: Double
        var amount: Int //don't include `@State` in non-Views
    }
    
    //no changes
    class MenuDataService {
        static let toppingsList: [Topping] = [Topping(name: "Cheese", price: 3.00, amount: 0), Topping(name: "Sauce", price: 3.00, amount: 0)]
    }
    
    class MenuViewModel: ObservableObject {
        @Published var toppingsList: [Topping]
        init() {
            toppingsList = MenuDataService.toppingsList
            //remove unnecessary duplicate assignment
        }
    }
    
    //This is the parent view -- may be called something different in your code
    struct ContentView : View {
        @StateObject private var vm = MenuViewModel() //declare as @StateObject
        
        var body: some View {
            ExtraToppingsView()
                .environmentObject(vm) //pass the environment object
        }
    }
    
    struct ExtraToppingsView : View {
        @EnvironmentObject private var vm : MenuViewModel //no need to take the toppingsList parameter separately -- just use the environment object 
    
        var body: some View {
            VStack {
                ForEach($vm.toppingsList) { $topping in //use element binding to get a mutable binding to each item
                    Stepper("\(topping.name): \(topping.amount)", value: $topping.amount) //see change of the position of the $ character
                }
            }
        }
    }
    
    struct ExtraToppingsView_Previews: PreviewProvider {
        static var previews: some View {
            ExtraToppingsView()
                .environmentObject(MenuViewModel())
        }
    }
    

    For more info on element binding: https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/