3

if I have something like this:

struct ContentView: View {
    var results = [Result(score: 8), Result(score: 5), Result(score: 10)]

    var body: some View {
        VStack {
            ForEach(results, id: \.id) { result in
                Text("Result: \(result.score)")
            }
        }
    }
}

And then I have a button that appends sometihng to the results array, the entire ForEach loop will reload. This makes sense, but I'm wondering if there is some way to prevent this. The reason I'm asking is because I have a ForEach loop with a few items, each of which plays an animation. If another item is appended to the array, however, the new item appears at the top of the ForEach, but, since the entire view is reload, the other animations playing in the items stop.

Is there any way to prevent this? Like to add an item to a ForEach array, and have it appear, but not reload the entire ForEach loop?

I assume not, but I would wonder how to get around such an issue.

2 Answers 2

2

Create separate view for iterating ForEach content, then SwiftUI rendering engine will update container (by adding new item), but not refresh existed rows

ForEach(results, id: \.id) {
    ResultCellView(result: $0)
}

and

struct ResultCellView: View {
   let result: Result
   var body: some View {
      Text("Result: \(result.score)")
   }
}

Note: I don't see your model, so there might be needed to confirm it to Hashable, Equatable.

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

1 Comment

hmm, is this true? I have a similar setup and it doesn't seem to work. I there some resource you used to find this information?
1

In general not providing an id makes it impossible for the ForEach to know what changed (as it has no track of the items) and therefore does not re-render the view.

E.g.

struct ContentView: View {
    
    @State var myData: Array<String> = ["first", "second"]
    
    var body: some View {
        VStack() {
            
            ForEach(0..<self.myData.count) { item in
                Text(self.myData[item])
            }
            
            Button(action: {
                self.myData.append("third")
            }){
                Text("Add third")
            }
        }
    }
}

This throws an console output (that you can ignore) where it tells you about what I just wrote above:

ForEach<Range<Int>, Int, Text> count (3) != its initial count (2). 
`ForEach(_:content:)` should only be used for *constant* data. 
Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` 
and provide an explicit `id`!

For your code try this: Tested on iOS 13.5.

struct ContentView: View {
    
    @State var results = [Result(score: 8), Result(score: 5), Result(score: 10)]

    var body: some View {
        VStack {
            ForEach(0..<self.results.count) { item in
                // please use some index test on production
                Text("Result: \(self.results[item].score)")
            }
            
            Button(action: {
                self.results.append(Result(score: 11))
            }) {
                Text("Add 11")
            }
        }
    }
}

class Result {
    
    var score: Int
    
    init(score: Int) {
        self.score = score
    }

}

Please note that this is a "hacky" solution and ForEach was not intended to be used for such cases. (See the console output)

1 Comment

Oh wow thank you, this is very interesting - so, is there some way in which I could explicitly specify the IDs and then, say, only update certain elements of the array (whose ID's I specified). So, like, maybe add one more item on to the array, but don't reload the ENTIRE array, just add that one item?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.