Swift UI ForEach 应该只用于 *constant* 数据

Swift UI ForEach should only be used for *constant* data

所以当 运行 我设置的游戏应用程序时出现此错误:

Image of the game screen when more of the same cards appear (All cards in the game should be unique)

ForEach<Range<Int>, Int, ModifiedContent<_ConditionalContent<_ConditionalContent<_ConditionalContent<_ConditionalContent
<ZStack<TupleView<(_ShapeView<Squiggle, ForegroundStyle>, 
_StrokedShape<Squiggle>)>>, ZStack<TupleView<(ModifiedContent<_StrokedShape<StripedRect>, 
_ClipEffect<Squiggle>>, _StrokedShape<Squiggle>)>>>, _StrokedShape<Squiggle>>, 
_ConditionalContent<_ConditionalContent<ZStack<TupleView<(_ShapeView<Capsule, ForegroundStyle>, _StrokedShape<Capsule>)>>, 
ZStack<TupleView<(ModifiedContent<_StrokedShape<StripedRect>, _ClipEffect<Capsule>>, 
_StrokedShape<Capsule>)>>>, _StrokedShape<Capsule>>>, _ConditionalContent<_ConditionalContent<ZStack<TupleView<(_ShapeView<Diamond, 
ForegroundStyle>, _StrokedShape<Diamond>)>>, ZStack<TupleView<(ModifiedContent<_StrokedShape<StripedRect>, _ClipEffect<Diamond>>, 
_StrokedShape<Diamond>)>>>, _StrokedShape<Diamond>>>,
 _FrameLayout>> count (2) != its initial count (1). `ForEach(_:content:)`
 should only be used for *constant* data. Instead conform data to `Identifiable` 
or use `ForEach(_:id:content:)` and provide an explicit `id`!

视图下方的卡片模型本身似乎没有变化。除了卡片上的 numberOfShapes 之外的所有内容都保持不变。因此,显示错误的卡片只有在基础模型正确匹配时才会匹配,而不会像视图中显示的那样匹配。

错误似乎只出现在我正确设置并按下“再显示三张牌”或者当我显示的牌比可见的多然后上下滚动屏幕时。

应用程序不会因错误而停止,但每张卡片上的 numberOfShapes 会动态变化(如上面的屏幕截图所示)。 (当我滚动到屏幕顶部然后滚动到底部并且某些卡片上的形状数量每次都在变化时,这是最清楚的)

所以我认为问题可能出在卡片视图中:

import SwiftUI

struct CardView: View {
    
    var card: Model.Card
    
    var body: some View {
        GeometryReader { geometry in
            ZStack{
                let shape = RoundedRectangle(cornerRadius: 10)
                if !card.isMatched {
                    shape.fill().foregroundColor(.white)
                    shape.strokeBorder(lineWidth: 4).foregroundColor(card.isSelected ? .red : .blue)
                }
            
                VStack {
                    Spacer(minLength: 0)
                    //TODO: Error here?
                    ForEach(0..<card.numberOfShapes.rawValue) { index in
                        cardShape().frame(height: geometry.size.height/6)
                    }
                    Spacer(minLength: 0)
                }.padding()
                .foregroundColor(setColor())
                .aspectRatio(CGFloat(6.0/8.0), contentMode: .fit)
                .frame(width: geometry.size.width, height: geometry.size.height)
            }
        }
    }
    
    @ViewBuilder private func cardShape() -> some View {
        switch card.shape {
        case .squiglle:          shapeFill(shape: Squiggle())
        case .roundedRectangle:  shapeFill(shape: Capsule())
        case .diamond:           shapeFill(shape: Diamond())
        }
    }
    
    private func setColor() -> Color {
        switch card.color {
        case .pink: return Color.pink
        case .orange: return Color.orange
        case .green: return Color.green
        }
    }
    
    @ViewBuilder private func shapeFill<setShape>(shape: setShape) -> some View
    //TODO: se løsning, brug rawvalues for cleanness
    where setShape: Shape {
        switch card.fill {
        case .fill:       shape.fillAndBorder()
        case .stripes:    shape.stripe()
        case .none:       shape.stroke(lineWidth: 2)
        }
    }
}

卡片视图以灵活的网格布局显示:

import SwiftUI

struct AspectVGrid<Item, ItemView>: View where ItemView: View, Item: Identifiable {
    var items: [Item]
    var aspectRatio: CGFloat
    var content: (Item) -> ItemView
    let maxColumnCount = 6
    
    init(items: [Item], aspectRatio: CGFloat, @ViewBuilder content: @escaping (Item) -> ItemView) {
        self.items = items
        self.aspectRatio = aspectRatio
        self.content = content
    }
    
    var body: some View {
            GeometryReader { geometry in
                VStack {
                    let width: CGFloat = widthThatFits(itemCount: items.count, in: geometry.size, itemAspectRatio: aspectRatio)
                    ScrollView{
                    LazyVGrid(columns: [adaptiveGridItem(width: width)], spacing: 0) {
                        ForEach(items) { item in
                            content(item).aspectRatio(aspectRatio, contentMode: .fit)
                    }
                    Spacer(minLength: 0)
                    }
                }
                }
            }
        }
    
    private func adaptiveGridItem(width: CGFloat) -> GridItem {
        var gridItem = GridItem(.adaptive(minimum: width))
        gridItem.spacing = 0
        return gridItem
    }

查看:

import SwiftUI

struct SetGameView: View {
    @StateObject var game: ViewModel
    
    var body: some View {
        VStack{
            HStack(alignment: .bottom){
                Text("Set Game")
                    .font(.largeTitle)
            }
            AspectVGrid(items: game.cards, aspectRatio: 2/3) { card in
                    CardView(card: card)
                        .padding(4)
                        .onTapGesture {
                            game.choose(card)
        
                }
            }.foregroundColor(.blue)
            .padding(.horizontal)
            Button("show 3 more cards"){
                game.showThreeMoreCardsFromDeck()
            }
        }
    }
}

非常感谢任何解决此问题的帮助。我已经在这上面花了很多时间了。

你的问题在这里:

ForEach(0..<card.numberOfShapes.rawValue) { index in
    cardShape().frame(height: geometry.size.height/6)
}

This ForEach initializer 的第一个参数是 Range<Int>。文档说(强调):

The instance only reads the initial value of the provided data and doesn’t need to identify views across updates. To compute views on demand over a dynamic range, use ForEach/init(_:id:content:).

因为它只读取“提供数据的初始值”,所以只有在 Range<Int> 是常量的情况下才能安全使用。如果更改范围,则效果不可预测。因此,从 Xcode 13 开始,如果您传递一个非常量范围,编译器会发出警告。

现在,也许您的范围 (0..<card.numberOfShapes.rawValue) 确实是恒定的。但是编译器不知道。 (并且根据您的错误,它不是一个恒定的范围。)

您可以通过切换到采用 id: 参数的初始化程序来避免警告:

ForEach(0..<card.numberOfShapes.rawValue, id: \.self) { index in
                                     // ^^^^^^^^^^^^^
                                     //    add this
    cardShape().frame(height: geometry.size.height/6)
}