View won't update in ForEach loop

I'm trying make it so that when the plus button is pushed in ListView the price will increment by one and be reflected on the screen. I'm not getting any errors, and I'm getting the expected result from the print statement, but the view will not update. I've tried just incrementing the price directly in the button as opposed to creating a variable for the same thing. but I get an error saying the left side is immutable because it's a let constant. Any help would be appreciated, I'm getting really frustrated. Thank you!

struct Product: Identifiable { let id = UUID() var name: String var price: Double var aisle: Int var location: Int }

class Inventory { var inventory: [Product] = [ Product(name: "Coke", price: 2.99, aisle: 1, location: 10), Product(name: "Pepsi", price: 3.99, aisle: 1, location: 6), Product(name: "Dr. Pepper", price: 1.99, aisle: 2, location: 8), Product(name: "Pibb", price: 1.50, aisle: 2, location: 1) ] }

struct ListView: View { @State var base = Inventory().inventory

@State var userList: [Product] = []

var body: some View {
    List {
        ForEach(base) { product in
            var thing = product.price
            HStack {
                Text(product.name)
                Spacer()
                Button {
                    thing += 1
                    print(thing)
                } label: {
                    Image(systemName: "plus")
                }

                Text("\(thing, specifier: "%.2f")")
            }
        }
    }
}
}

struct ListView_Previews: PreviewProvider { static var previews: some View { ListView() } }

Answered by Claude31 in 763029022

Welcome to the forum.

You should first format code with code formatter tool. More readable.

struct Product: Identifiable {
    let id = UUID()
    var name: String
    var price: Double
    var aisle: Int
    var location: Int
}

class Inventory {
    var inventory: [Product] = [ Product(name: "Coke", price: 2.99, aisle: 1, location: 10), Product(name: "Pepsi", price: 3.99, aisle: 1, location: 6), Product(name: "Dr. Pepper", price: 1.99, aisle: 2, location: 8), Product(name: "Pibb", price: 1.50, aisle: 2, location: 1) ]
}

struct ListView: View {
    @State var base = Inventory().inventory
    @State var userList: [Product] = []
    
    var body: some View {
        List {
            ForEach(base) { product in
                var thing = product.price
                HStack {
                    Text(product.name)
                    Spacer()
                    Button {
                        thing += 1
                        print(thing)
                    } label: {
                        Image(systemName: "plus")
                    }
                    
                    Text("\(thing, specifier: "%.2f")")
                }
            }
        }
    }
}

struct ListView_Previews: PreviewProvider {
    static var previews: some View {
         ListView()
    }
}

You are missing essential elements of SwiftUI.

  • you create a local var (thing), so the change remains local, it is not propagated to update Text().
  • you have to use a State var.
  • Your tried using base, but base in ForEach is a local constant, it cannot be updated
  • You have to update directly the base state var, at the right index
  • To do so, a simple way is to use enumerated.

Here is a code that works:

struct Product: Identifiable, Hashable {    // Need to make it Hashable
    let id = UUID()
    var name: String
    var price: Double
    var aisle: Int
    var location: Int
}

class Inventory {
    var inventory: [Product] = [ Product(name: "Coke", price: 2.99, aisle: 1, location: 10), Product(name: "Pepsi", price: 3.99, aisle: 1, location: 6), Product(name: "Dr. Pepper", price: 1.99, aisle: 2, location: 8), Product(name: "Pibb", price: 1.50, aisle: 2, location: 1) ]
}

struct ListView: View {
    @State var base = Inventory().inventory
    @State var userList: [Product] = []
    
    var body: some View {
        List {
            ForEach(Array(base.enumerated()), id: \.element) { index, product in    // index to access directly base[index] ; it is equal to product, but this is modifiable
            // ForEach(base) { product in
                // var thing = product.price
                HStack {
                    Text(product.name)
                    Spacer()
                    Button {
                        base[index].price += 1      // thing += 1
                    } label: {
                        Image(systemName: "plus")
                    }
                    Text("\(product.price, specifier: "%.2f")")      // Text("\(thing, specifier: "%.2f")")
                }
            }
        }
    }
}

If that works, don't forget to close the thread by marking this answer as correct. Otherwise, explain what is the remaining issue.

Note: if you find enumerated too complex, you could add an index to Product, which will be its rank in the array (starting at 0). And use it to access the right element in base

struct Product: Identifiable {
    let id = UUID()
    var name: String
    var price: Double
    var aisle: Int
    var location: Int
    var index: Int = 0
}

class Inventory {
    var inventory: [Product] = [
        Product(name: "Coke", price: 2.99, aisle: 1, location: 10, index: 0),
        Product(name: "Pepsi", price: 3.99, aisle: 1, location: 6, index: 1),
        Product(name: "Dr. Pepper", price: 1.99, aisle: 2, location: 8, index: 2),
        Product(name: "Pibb", price: 1.50, aisle: 2, location: 1, index: 3)
    ]
}
        List {
             ForEach(base) { product in
                HStack {
                    Text(product.name)
                    Spacer()
                    Button {
                        base[product.index].price += 1
                    } label: {
                        Image(systemName: "plus")
                    }
                    Text("\(product.price, specifier: "%.2f")")
                }
            }

Good continuation.

Accepted Answer

Welcome to the forum.

You should first format code with code formatter tool. More readable.

struct Product: Identifiable {
    let id = UUID()
    var name: String
    var price: Double
    var aisle: Int
    var location: Int
}

class Inventory {
    var inventory: [Product] = [ Product(name: "Coke", price: 2.99, aisle: 1, location: 10), Product(name: "Pepsi", price: 3.99, aisle: 1, location: 6), Product(name: "Dr. Pepper", price: 1.99, aisle: 2, location: 8), Product(name: "Pibb", price: 1.50, aisle: 2, location: 1) ]
}

struct ListView: View {
    @State var base = Inventory().inventory
    @State var userList: [Product] = []
    
    var body: some View {
        List {
            ForEach(base) { product in
                var thing = product.price
                HStack {
                    Text(product.name)
                    Spacer()
                    Button {
                        thing += 1
                        print(thing)
                    } label: {
                        Image(systemName: "plus")
                    }
                    
                    Text("\(thing, specifier: "%.2f")")
                }
            }
        }
    }
}

struct ListView_Previews: PreviewProvider {
    static var previews: some View {
         ListView()
    }
}

You are missing essential elements of SwiftUI.

  • you create a local var (thing), so the change remains local, it is not propagated to update Text().
  • you have to use a State var.
  • Your tried using base, but base in ForEach is a local constant, it cannot be updated
  • You have to update directly the base state var, at the right index
  • To do so, a simple way is to use enumerated.

Here is a code that works:

struct Product: Identifiable, Hashable {    // Need to make it Hashable
    let id = UUID()
    var name: String
    var price: Double
    var aisle: Int
    var location: Int
}

class Inventory {
    var inventory: [Product] = [ Product(name: "Coke", price: 2.99, aisle: 1, location: 10), Product(name: "Pepsi", price: 3.99, aisle: 1, location: 6), Product(name: "Dr. Pepper", price: 1.99, aisle: 2, location: 8), Product(name: "Pibb", price: 1.50, aisle: 2, location: 1) ]
}

struct ListView: View {
    @State var base = Inventory().inventory
    @State var userList: [Product] = []
    
    var body: some View {
        List {
            ForEach(Array(base.enumerated()), id: \.element) { index, product in    // index to access directly base[index] ; it is equal to product, but this is modifiable
            // ForEach(base) { product in
                // var thing = product.price
                HStack {
                    Text(product.name)
                    Spacer()
                    Button {
                        base[index].price += 1      // thing += 1
                    } label: {
                        Image(systemName: "plus")
                    }
                    Text("\(product.price, specifier: "%.2f")")      // Text("\(thing, specifier: "%.2f")")
                }
            }
        }
    }
}

If that works, don't forget to close the thread by marking this answer as correct. Otherwise, explain what is the remaining issue.

Note: if you find enumerated too complex, you could add an index to Product, which will be its rank in the array (starting at 0). And use it to access the right element in base

struct Product: Identifiable {
    let id = UUID()
    var name: String
    var price: Double
    var aisle: Int
    var location: Int
    var index: Int = 0
}

class Inventory {
    var inventory: [Product] = [
        Product(name: "Coke", price: 2.99, aisle: 1, location: 10, index: 0),
        Product(name: "Pepsi", price: 3.99, aisle: 1, location: 6, index: 1),
        Product(name: "Dr. Pepper", price: 1.99, aisle: 2, location: 8, index: 2),
        Product(name: "Pibb", price: 1.50, aisle: 2, location: 1, index: 3)
    ]
}
        List {
             ForEach(base) { product in
                HStack {
                    Text(product.name)
                    Spacer()
                    Button {
                        base[product.index].price += 1
                    } label: {
                        Image(systemName: "plus")
                    }
                    Text("\(product.price, specifier: "%.2f")")
                }
            }

Good continuation.

The problem you have is you're updating the local thing variable when the increment button is tapped. The thing variable is a copy of product.price and so changes to thing will not be reflected elsewhere. This is what the @State property wrapper is used for: to update the view's body when it's underlying value changes.

As you already have a state property, base, you can extract read/write values that can be updated from within the view body and will have the behaviour you are expecting. You can use the ForEach initialiser that takes a binding to a collection and gives you a binding to each of its elements to work with.

Here is an example that works:

ForEach($base) { $product in // `product` is a variable
    HStack {
        Text(product.name)
        Spacer()
        Button {
            product.price += 1
        } label: {
            Image(systemName: "plus")
        }

        Text("\(product.price, specifier: "%.2f")")
    }
}


Just a note on this code:

$product is of type Binding<Product>. It provides a way of reading and writing to another value, in this case an element of the base collection. It can be useful if, for example, you have a Stepper control which requires a binding to update its value to and from.

product is of type Product. This is a variable and can be read and written to. It also updates the view body when it changes which is why it is useful here.

View won't update in ForEach loop
 
 
Q