Reputation: 115
So, I'm going through the SwiftUI documentation to get familiar. I was working on a grid sample app. It has the following code:
ForEach(allColors, id: \.description) { color in
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
}
It didn't occur to me first that ForEach is actually a struct, I thought it's a variation of the for in loop at first so I'm quite new at this. Then I checked the documentation.
When I read the documentation and some google articles for the ForEach struct, I didn't understand two points in the code:
.\description
instead of .self?color in
. Since foreach is a struct and the paranthesis is the initializtion parameters this looks like the return type of a closure but why would we return individual colors to foreach? I thought the return is a collection of views or controls like button and label. This is like var anInteger: Int = 1
for example. What type does ForEach accept as a result of the closure? Or am I reading this all wrong?Upvotes: 0
Views: 637
Reputation: 35636
So we are initializing the foreach struct with an array of colors. For the the ID why did they use .\description instead of .self?
It depends on the type of allColors
. What you should have in mind that id
here is expected to be stable. The documentation states:
It’s important that the id of a data element doesn’t change unless you replace the data element with a new data element that has a new identity. If the id of a data element changes, the content view generated from that data element loses any current state and animations.
So for example if colors are reference types (which are identifiable) and you swap one object with an identical one (in terms of field values), the identity will change, whereas description
wouldn't (for the purposes of this example - just assuming intentions of code I have no access to).
Edit: Also note that in this specific example allColors
appears to be a list of Color
, which is not identifiable. So that's the reason behind the custom id
keyPath.
Regarding your second point, note that the trailing closure is also an initialization parameter. To see this clearly we could use the "non-sugared" version:
ForEach(allColors, id: \.description, content: { color in
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
})
where content
is a closure (an anonymous function) that gets passed an element of the collection and returns some View
.
So the idea is something like this: "Give me an collection of identifiable elements and I will call a function for each of these elements expecting from you to return me some View
".
I hope that this makes (some) sense.
Additional remarks regarding some of the comments:
It appears to me that the main source of confusion is the closure itself. So let's try something else. Let's write the same code without a closure:
ForEach
's init
has this signature:
init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)
Now, the content
translates to:
A function with one parameter of type Data.Element
, which in our case is inferred from the data
so it is a Color
. The function's return type is Content
which is a view builder that produces some View
so our final code, which is equivalent to the first one, could look like this:
struct MyView: View {
let allColors: [Color] = [.red, .green, .blue]
@State private var selectedColor: Color?
var body: some View {
List {
ForEach(allColors, id: \.description, content: colorView)
}
}
@ViewBuilder
func colorView(color: Color) -> some View {
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
}
}
I hope that this could help to clarify things a little bit better.
Upvotes: 2