Button in ForEach loop changes Bool for each item, instead of just one item

Hi there,

currently I try to practice on JSON data and I stumbled across following issue:

I have a JSON file which holds an item, which itself holds several items, which themselves hold several items. Something like

ItinaryITEM

  • HeaderITEM 1
  1. Item 1
  2. Item 2
  • HeaderITEM 2
  1. Item 1
  2. Item 2
class ItinaryItem: Codable, Identifiable {
    
var name: String
var id: Int
var headerItem: [HeaderItems]
}

class HeaderItems: Codable, Identifiable {
    var name: String
    var id: Int
    var complete: Bool
    var items: [Item]
}

class Item: Codable, Identifiable {
    var name: String
    var id: Int
    var amount: Int
    var complete: Bool
}

I want to show that data in a List view. For that, I created a
LIST
ForEach (itinaryItem)
ForEach(headerItem)
ForEach(item)

For the final item, I created a separate view.

  List {
            ForEach(itinaryItems) {  item in
                Section(header: Text(item.name)) {
                    ForEach(item.headerItem) { header in
                        VStack (alignment: .leading, spacing: 7) {
                            Text(header.name)
                                .font(.title)
                            ForEach(header.items) { finalItem in
                                FinalItemView(completed: finalItem.complete, amount: finalItem.amount, name: finalItem.name)
                                    
                            }
                        }
                    }
                }
                }
            }
struct FinalItemView: View {
    
    @State var completed: Bool
    var amount: Int
    var name: String
    
    var body: some View {
        
        
        HStack {
            Text(String(amount))
            Text(name)
            Spacer()
            Button {
               completed.toggle()
                
            } label: {
                completed == false ? Image(systemName: "circle").foregroundColor(.gray) : Image(systemName: "checkmark.circle.fill").foregroundColor(.blue)
            }
           
               
            
        }
            .font(.title2)
            .padding(.leading)
        
    }
}

when I run this code, all details are shown correctly, as written in the JSON file.
image

Note that the item.complete BOOL variable is correctly shown for the individual items. But when I click on one item, I was expecting to flip the BOOL only for that specific item. But in reality, the Bool is flipped for all 3 items.
For example, I click on the circle (which is the button) next to the “1 Mask”, but instead of just getting a blue checkmark on that line (which means all 3 items below the Primary Items should have blue checkmarks), the checkmarks on the other lines disappear.

Bildschirmfoto 2023-08-06 um 17.17.22

Why is the view correctly reading the initial Bool value of each item and displays it, but as soon as I toggle the Bool value, all Bool values of the ForEach loop are toggled?
What does the trick to just check one item, and not all three in that ForEach loop?

My solution:
instead of a Button in the forEach loop, which toggled all Bool in the complete forEach View, I attached an .onTapGesture { ...toggle() } to the HStack of the text and the image (the circle).

(Note: for testing I created more testData, so the naming is a little bit different to the code in above post)

        ForEach(testArray) { dataset in
                HStack {
                    Text(dataset.name)
                    dataset.isCompleted == false ? Image(systemName: "circle").foregroundColor(.gray) : Image(systemName: "checkmark.circle.fill").foregroundColor(.blue)
                }
                .onTapGesture {
                    dataset.isCompleted.toggle()
                    theID += 1
                }
                .id(theID)
            }

I still do not know, why an .onTapGesture changes the bool parameter of a single item in the forEach view, whereas the Button changes the bool parameters of all items in the forEach View.

Unfortunately, it seems that I don’t get any help here.

Can someone please explain to me, why this code

ForEach(testArray) { dataset in

Button {
               dataset.isCompleted.toggle()
            } label: {
               HStack {
                    Text(dataset.name)
                    dataset.isCompleted == false ? Image(systemName: "circle").foregroundColor(.gray) : Image(systemName: "checkmark.circle.fill").foregroundColor(.blue)
                }
            }
            }

behaves differently than this code?

ForEach(testArray) { dataset in
                HStack {
                    Text(dataset.name)
                    dataset.isCompleted == false ? Image(systemName: "circle").foregroundColor(.gray) : Image(systemName: "checkmark.circle.fill").foregroundColor(.blue)
                }
                .onTapGesture {
                    dataset.isCompleted.toggle()
                }
            }

Both times I loop over an array of 3 items with a ForEach loop.
In the first example, I use a button to display the data. In the second example, I use the same code but instead of a button, I use the .onTapGesture modifier.

In the first example, if I press on any button, the “isCompleted Bool” is toggled for all three buttons (which is neither the result I want, nor the result I expect with this code).
In the second example, if I press on any item, the “isCompleted Bool” is toggled only for that item.

The “Button” and the “Item” is basically the same - it is just a different approach to get to the same visual and functional result = Display some tappable area and if this area is tapped, change something in this area (but the Button version changes something outside that tappable area). Both are in the same ForEach loop. Nothing is different (i.e. the modifier is not outside the loop or anything like that).

Any explanation would be highly appreciated :slight_smile: Thank you in advance.

When other people post, other posts get pushed down when viewed chronologically. Posting saying you’d still like help is best. But sometimes not every question gets answered because some people don’t know the answer, but also because it can get lost sometimes.


Going back to the beginning first your models should be structs not classes.
Also I’m guessing the problem (from your 1st post) is because, when you have a @State property, you should not be passing values into it. @State properties are meant to be private to that view, and can be passed as bindings to its children.

You shouldn’t have a @State property that is an input parameter.

In your 2nd post, I’m confused what theID is supposed to do and where it comes from. However you’re using it as .id(theID) which means you’re specifically saying what the id of the view is for SwiftUI which is actually very important, and why that solution probably works.

See “Demystifying SwiftUI” the WWDC session, id values of views are super important cause that’s how SwiftUI knows what to update or not based on state changes.

For FinalItemView you shouldn’t pass all 3 variables, it’s easier to just pass the item itself, as a @Binding


Okay so I actually haven’t figured this out, and I can’t spend more time on it, but I started by googling “changing a value in a list swiftui”

It definitely has to do with view identity and not treating each item as separate. Below is the code, for a slightly better way to write it than what you originally had, but it still has the same issue as before.

I’d also google how to make a todo list, cause that’s pretty much what this is, and it may show why this is happening

This post might be helpful

struct FinalItemView: View {
    
    @Binding var item: Item
    
    var body: some View {
        HStack {
            Text(String(item.amount))
            Text(item.name)
            Spacer()
            Button {
//                $item.complete.wrappedValue.toggle()
                print("this item: \(item)") // prints 3 times for all the items
            } label: {
                item.complete ? Image(systemName: "checkmark.circle.fill").foregroundColor(.blue) : Image(systemName: "circle").foregroundColor(.gray)
            }
        }
        .font(.title2)
        .padding(.leading)
    }
}

// Content View's body
List {
    ForEach($itinaryItems) { $itinaryItem in
        Section {
            ForEach($itinaryItem.headerItem) { $headerItem in
                VStack(alignment: .leading, spacing: 7) {
                    Text(headerItem.name)
                        .font(.title)
                    ForEach($headerItem.items) { $item in
                        FinalItemView(item: $item)
                    }
                }
            }
        } header: {
            Text(itinaryItem.name)
        }
    }
}

Thank you so much, Mikaela. Your answer was very helpful to me, especially the link at the end. I really appreciate it. I re-wrote my code and it looks better. As soon as I have an explanation (or working solution) for the button behavior, I’ll answer in this thread. Probably at the weekend, as I won’t have too much spare time before.

To answer your question about the .id … I learned this “trick” especially for ForEach loops, when the view is not updated automatically, although some properties are changing. Probably defining some get/set is the more accurate way to go.
Attaching the .id to a view, this view gets updated when the id is changing (thus displaying the new value of the property). the id might be the value of the property, which is changing but not causing the view to update, or you just change the id value as I did (just adding +1).
Works very well for ForEach loops in my experience.

1 Like

Yes! This has to do with views needing to know their identity to know when to be updated. See the WWDC session Demystify SwiftUI

Talking about Identity

1 Like