Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
932 views
in Technique[技术] by (71.8m points)

swiftui - View is not rerendered in Nested ForEach loop

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

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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`!"

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...