View is not rerendered in Nested ForEach loop

ForEach should only be used for constant data. So it is only evaluated once by definition. Try wrapping it in a List and you will see errors being logged like:

ForEach, Int, TupleView<(Spacer, HStack, Int, TupleView<(Spacer, Text, Spacer)>>>, Spacer)>> count (7) != its initial count (0). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!

I was surprised by this as well, and unable to find any official documentation about this limitation.

As for why it is not possible for you to access the i in the inner ForEach-loop, I think you probably have a misleading compiler error on your hands, related to something else in the code that is missing in your snippets. It did compile for me after completing the missing parts with a best guess (Xcode 11.1, Mac OS 10.14.4).

Here is what I came up with to make your ForEach go over something Identifiable:



struct SettingsElement: Identifiable {
    var id: Int { value }

    let value: Int

    init(_ i: Int) { value = i }
}

class Settings: ObservableObject {
    @Published var rows = [SettingsElement(0),SettingsElement(1),SettingsElement(2)]
    @Published var columns = [SettingsElement(0),SettingsElement(1),SettingsElement(2)]
}

struct ContentView: View {
    @EnvironmentObject var settings: Settings

    func getSymbol(index: Int) -> Text { Text("\(index)") }

    var body: some View {
        VStack{
            Text("\(self.settings.rows.count) x \(self.settings.columns.count)")
            ForEach(self.settings.rows) { i in
                VStack {
                    HStack {
                        ForEach(self.settings.columns) { j in
                            Text("\(i.value) \(j.value)")
                        }
                    }
                }
            }
        }
    }
}


TL;DR

Your ForEach needs id: \.self added after your range.

Explanation

ForEach has several initializers. You are using

init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)

where data must be a constant.

If your range may change (e.g. you are adding or removing items from an array, which will change the upper bound), then you need to use

init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)

You supply a keypath to the id parameter, which uniquely identifies each element that ForEach loops over. In the case of a Range<Int>, the element you are looping over is an Int specifying the array index, which is unique. Therefore you can simply use the \.self keypath to have the ForEach identify each index element by its own value.

Here is what it looks like in practice:

struct ContentView: View {
    @State var array = [1, 2, 3]

    var body: some View {
        VStack {
            Button("Add") {
                self.array.append(self.array.last! + 1)
            }

            // this is the key part  v--------v
            ForEach(0..<array.count, id: \.self) { index in
                Text("\(index): \(self.array[index])")
                //Note: If you want more than one views here, you need a VStack or some container, or will throw errors
            }
        }
    }
}

If you run that, you'll see that as you press the button to add items to the array, they will appear in the VStack automatically. If you remove "id: \.self", you'll see your original error:

`ForEach(_:content:)` should only be used for *constant* data. 
Instead conform data to `Identifiable` or use `ForEach(_:id:content:)`
and provide an explicit `id`!"

Tags:

Swiftui