Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

View is not rerendered in Nested ForEach loop

Tags:

swiftui

I have the following component that renders a grid of semi transparent characters:

    var body: some View {
        VStack{
            Text("\(self.settings.numRows) x \(self.settings.numColumns)")
            ForEach(0..<self.settings.numRows){ i in
                Spacer()
                    HStack{
                        ForEach(0..<self.settings.numColumns){ j in
                            Spacer()
                            // why do I get an error when I try to multiply i * j
                            self.getSymbol(index:j)
                            Spacer()
                        }
                    }
                Spacer()
            }
        }
    }

settings is an EnvironmentObject

Whenever settings is updated the Text in the outermost VStack is correctly updated. However, the rest of the view is not updated (Grid has same dimenstions as before). Why is this?

Second question: Why is it not possible to access the i in the inner ForEach-loop and pass it as a argument to the function?

I get an error at the outer ForEach-loop:

Generic parameter 'Data' could not be inferred

like image 725
simibac Avatar asked Oct 22 '19 12:10

simibac


People also ask

Should only be used for * constant * Data instead conform data to?

should only be used for *constant* data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id! At this point, we might have actually solved the problem. There are no more warnings being emitted, and things might continue to work perfectly fine even as we mutate our Note array.

Can foreach loop be nested?

An important feature of foreach is the %:% operator. I call this the nesting operator because it is used to create nested foreach loops. Like the %do% and %dopar% operators, it is a binary operator, but it operates on two foreach objects.

What can foreach loops not be used for?

For-each cannot be used to initialize any array or Collection, because it loops over the current contents of the array or Collection, giving you each value one at a time. The variable in a for-each is not a proxy for an array or Collection reference.

What is foreach loop with example?

C# provides an easy to use and more readable alternative to for loop, the foreach loop when working with arrays and collections to iterate through the items of arrays/collections. The foreach loop iterates through each item, hence called foreach loop.


2 Answers

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`!"
like image 52
John M. Avatar answered Oct 16 '22 22:10

John M.


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)")
                        }
                    }
                }
            }
        }
    }
}

like image 1
Kristof Van Landschoot Avatar answered Oct 16 '22 20:10

Kristof Van Landschoot