SwiftUI Add to ForEach array without causing the entire view to reload

Viewed 3067

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

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.

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)

Related