Olivia Kumar
Olivia Kumar

Reputation: 21

Swift UI - LazyVGrid and ForEach - cannot use duplicate items

New to SwiftUI and trying to display these emojis on separate CardViews inside of a LazyVGrid. When I use a ForEach loop to set one emoji per CardView, I cannot use duplicate emojis - even though I used id: \.self (actually, not entirely sure that matters for what I'm trying to achieve but thought it was worth mentioning).

Basically, I can show 5 cards but, when I try to show 6 (which is when the duplicate items begin), the preview crashes. I can do it with an HStack but, for some reason, cannot with a LazyVStack. I reviewed LazyVStack documentation but couldn't find an answer to this.

Additionally, tried using .indicies in ForEach but didn't seem to fix it either.

import SwiftUI

struct ContentView: View {
    var emojis: [String] = ["😊", "😇", "😄", "😎",
                            "😝", "😊", "😇", "😄",
                            "😎", "😝", "😊", "😇",
                            "😎", "😝", "😊", "😇",
                            "😄", "😎", "😝", "😊",
                            "😇", "😄", "😎", "😝"]
    @State var emojiCount = 5 //6
    
    var gridLayout = [GridItem(), GridItem(), GridItem()]
    
    var body: some View {
        VStack {
            LazyVGrid(columns: gridLayout) {
//            HStack {
                ForEach((emojis[0..<emojiCount]), id: \.self) { emoji in
                    CardView(content: emoji)
                }
            }
            .foregroundColor(.red)

            Spacer()
            
            HStack {
                remove
                Spacer()
                add
            }
        }
        .padding(.horizontal)
    }
    
    var add: some View {
        Button(action: {
            emojiCount += 1
        }, label: {
            Text("⨁")
        })
    }

    var remove: some View {
        Button(action: {
            if emojiCount > 0 {
                emojiCount -= 1
            }
        }, label: {
            Text("⊖")
        })
    }
}

struct CardView: View {
    var content: String
    @State var isFaceUp: Bool = true
    
    var body: some View {
        ZStack {
            let shape = RoundedRectangle(cornerRadius: 20.0)
            if isFaceUp {
                shape.fill().foregroundColor(.white)
                shape.stroke(lineWidth: 3.0)
                Text(content)
            } else {
                shape.fill()
            }
        }
        .onTapGesture {
            isFaceUp = !isFaceUp
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .preferredColorScheme(.dark)
        ContentView()
            .preferredColorScheme(.light)
    }
}

struct ContentView_Previews_2: PreviewProvider {
    static var previews: some View {
        /*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/
    }
}

Upvotes: 2

Views: 2264

Answers (2)

George
George

Reputation: 30451

You can't have duplicate ids for views within LazyVGrid (and others such as List). Since you have duplicate emojis and you are identifying each CardView by \.self, you have views with duplicate IDs.

Solution #1 (worse)

Only use this if you have constant data, otherwise you may get the problems mentioned below.

Use the index. This will always be unique. However, this may cause problems when the emojis changes, because views will then be swapping IDs (which breaks animations, etc). The ids here are not constant.

See solution #2 for a better way.

ForEach(0 ..< emojiCount, id: \.self) { emojiIndex in
    CardView(content: emojis[emojiIndex])
}

Solution #2 (better)

You can use this on constant or mutating data.

Make each emoji uniquely identifiable. This is the best solution. The ids here are constant.

struct MyEmoji: Identifiable {
    let id = UUID()
    let string: String

    init(_ string: String) {
        self.string = string
    }
}
var emojis: [MyEmoji] = [MyEmoji("😊"), MyEmoji("😇"), MyEmoji("😄"), MyEmoji("😎"),
                         MyEmoji("😝"), MyEmoji("😊"), MyEmoji("😇"), MyEmoji("😄"),
                         MyEmoji("😎"), MyEmoji("😝"), MyEmoji("😊"), MyEmoji("😇"),
                         MyEmoji("😎"), MyEmoji("😝"), MyEmoji("😊"), MyEmoji("😇"),
                         MyEmoji("😄"), MyEmoji("😎"), MyEmoji("😝"), MyEmoji("😊"),
                         MyEmoji("😇"), MyEmoji("😄"), MyEmoji("😎"), MyEmoji("😝")]
ForEach(emojis[0 ..< emojiCount]) { emoji in
    CardView(content: emoji.string)
}

Upvotes: 5

Eric Shieh
Eric Shieh

Reputation: 817

You didn't say how you used indices, but the following worked in my own tests:

ForEach(Array((emojis[0..<emojiCount]).enumerated()),id:\.offset) { index,emoji in {
  ....
}

I'm not sure why HStack works, but in general, SwiftUI uses ids to track changes so it's a best practice to keep them as close to real identifiers as possible. In the case where you're using \.self, it uses the emoji string itself which ends up with duplicate ids. There's a WWDC video from this year (2021) explaining the importance of ids and how misuse can cause odd SwiftUI behavior: https://developer.apple.com/videos/play/wwdc2021/10022/

Upvotes: 0

Related Questions