Process
Process

Reputation: 115

ForEach: ID parameter and closure return type

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:

Upvotes: 0

Views: 637

Answers (1)

Alladinian
Alladinian

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

Related Questions