4

When I use a ForEach loop over an array twice within a view, I get the following warning at runtime:
LazyVGridLayout: the ID 84308994-9D16-48D2-975E-DC40C5F9EFFF is used by multiple child views, this will give undefined results!

The reason for this is clear so far, but what is the smartest way to work around this problem?

The following sample code illustrates the problem:

import SwiftUI

// MARK: - Model

class Data: ObservableObject
{
    @Published var items: [Item] = [Item(), Item(), Item()]
}

struct Item: Identifiable
{
    let id = UUID()
    var name: String = ""
    var description: String = ""
}

// MARK: - View

struct MainView: View {
    @StateObject var data: Data

    private var gridItems: [GridItem] { Array(repeating: GridItem(), count: data.items.count) }
    
    var body: some View {
        LazyVGrid(columns: gridItems, alignment: .leading, spacing: 2) {
            ForEach(data.items) { item in
                Text(item.name)
            }
            ForEach(data.items) { item in
                Text(item.description)
            }
        }
    }
}


// MARK: - App

@main
struct SwiftUI_TestApp: App {
    
    var body: some Scene {
        WindowGroup {
            MainView(data: Data())
        }
    }
}

I could possibly divide the view into several SubViews.
Are there any other options?

Edit:
This is the body of the real app:

    var body: some View {
        VStack {
            LazyVGrid(columns: columns, alignment: .leading, spacing: 2) {
                Text("")
                ForEach($runde.players) { $player in
                    PlayerHeader(player: $player)
                }
                
                ForEach(Score.Index.allCases) { index in
                    Text(index.localizedName)
                    ForEach(runde.players) { player in
                        Cell(player: player, active: player == runde.activePlayer, index: index)
                    }
                }
                
                Text ("")
                ForEach(runde.players) { player in
                    PlaceView(player: player)
                }
            }
            .padding()
        }
    }
D. Mika
  • 2,577
  • 1
  • 13
  • 29
  • Use only one ForEach, and inside it use a VStack (or any other encapsulating view) and put your two Text views in that same stack. – Eric Aya Jun 26 '21 at 10:47
  • @EricAya: That doesn't work. In the example it might work, but in the actual program I use a LazyVGrid and so I can't combine that into one ForEach. – D. Mika Jun 26 '21 at 10:50
  • It should work, this is the way to do it. You should generate the subviews only once in the LazyVGrid or any superview (use one ForEach, not several). Please show your real code so that we can help otherwise we're navigating blind... – Eric Aya Jun 26 '21 at 10:53
  • Use 1 ForEach and instead create 2 items for each of your data. – mahan Jun 26 '21 at 11:40

2 Answers2

11

If you really need that kind of grid filling, then it is possible just to use different identifiers for those ForEach containers, like

    LazyVGrid(columns: gridItems, alignment: .leading, spacing: 2) {
        ForEach(data.items) { item in
            Text(item.name).id("\(item.id)-1")      // << here !!
        }
        ForEach(data.items) { item in
            Text(item.description).id("\(item.id)-2")    // << here !!
        }
    }

Tested with Xcode 13beta / iOS 15

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • This solucion works perfect, for those using an EmptyView() or Space() just use : Spacer().id(UUID().uuidString). – Joule87 Mar 02 '23 at 14:58
-1

While adding identifiers within the ForEach loop sometimes works, I found that accessing the indexes from the loop with indices worked in other cases:

ForEach(items.indices, id: \.self) { i in
    Text(items[i])
}