1

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.

10
  • Update: I was given this error - 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. Commented Feb 7, 2022 at 3:09
  • There are a few things going on here. @State can't be used outside of a view, so your model struct should not have any @State properties. Conversely, your let toppingsList will not be mutable -- it should be annotated with @State. Then, in your for each, you'll need binding. Probably a good opportunity to go through the Apple or Hacking With Swift SwiftUI tutorials and get familiar with some of the SwiftUI basics. Commented Feb 7, 2022 at 3:46
  • Watch this at about minute 20 Commented Feb 7, 2022 at 15:39
  • Also, it is minor, but since struct Topping is Identifiable, you do not have to specifically use .id. Just iterate it like: ForEach(toppingsList) { topping in. Commented Feb 7, 2022 at 15:44
  • @jnpdx I might be formatting my code incorrectly, but I have the toppingsList declared inside a ViewModel, which will not let me use State. The static let toppingsList is then passed to a ViewModel where Published newVar is initialized to be equal to the list. Commented Feb 7, 2022 at 22:02

1 Answer 1

0

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/

Sign up to request clarification or add additional context in comments.

8 Comments

Wow. You got it to work? Would you be able to explain it to me? The difference between my code and yours? (Except the @State in the struct definition, I understand that)
@jNiuk Did you read the inline comments -- I tried to explain all of the changes, and then I provided a link at the end that describes the element binding in more detail. Is there a part specifically that needs more explanation? Happy to provide it if I can.
This does work, one more question. What if I wanted to pass in a toppingsList for that view? In my program there are different lists for toppings (cheese, meats, veggies) and I want to reuse that view. That's why early I included the parameter in my old code. Thanks for all your help @jnpdx
Currently I have a really hacky solution of iterating through the entire state object list, and comparing string titles, and displaying based on that. It works, but if there is a better solution, im willing to learn :)
If you wanted a specific list for that single view, I'd think you'd want to use @StateObject private var vm = MenuViewModel() on the View itself. EnvironmentObject (which you used in your original code) implies that it's going to be used in more of a global state (or at least across multiple views).
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.