SwiftUI:ScrollView 无法与 ForEach 一起正常工作

SwiftUI: ScrollView not working correctly with ForEach

我正在制作纸牌游戏,我正在尝试将纸牌放入 ScrollView。我通过遍历卡片数组然后使用 CardView.

为每张卡片构建视图来做到这一点
struct ContentView: View {
    @ObservedObject var viewModel = SetGameViewModel()
    
    var body: some View {
        VStack {
            ScrollView {
                LazyVGrid(columns: [GridItem(),GridItem(),GridItem(),GridItem()]) {
                    ForEach(viewModel.cards) { card in
                        CardView(card: card)
                            //Individual card gets built depending on its data.
                            .aspectRatio(2/3, contentMode: .fill)
                        }
                }
            }
            DealThreeCardsButton()
                .onTapGesture { viewModel.dealThreeCards() }
        }
    }
}

每张卡片都有特定数量的符号(以及其他几个特征,但这些不是现在的问题。)它应该显示。 ->

struct CardView: View {
    let card: SetGame.Card
    private var color: Color {
        switch card.content.color {
        case .green:
            return Color(.green)
        case .purple:
            return Color(.purple)
        case .red:
            return Color(.red)
        }
    }
    
    
    var body: some View {
        if card.isFaceUp {
            ZStack{
                RoundedRectangle(cornerRadius: 20)
                    .foregroundColor(.clear)
                RoundedRectangle(cornerRadius: 20)
                    .stroke(lineWidth: 2.0)
                    .foregroundColor(/*@START_MENU_TOKEN@*/.blue/*@END_MENU_TOKEN@*/)
                VStack {
                    CardContentView(content: card.content)
                        //The contents of the cards are created
                        .foregroundColor(color)
                    Text("\(card.id), n: \(card.content.number)")
                }
            }
        } else {
            RoundedRectangle(cornerRadius: 20)
                .foregroundColor(.blue)
        }
    }
}


private struct CardContentView: View {
    let content: SetGame.CardContent
    var body: some View {
        ForEach(1..<content.number+1) { _ in
            //This ForEach should create the number of symbols held by the card struct
            //However this seems to not work.
            ZStack {
                switch content.symbol {
                case .oval:
                    DrawCircle(fillType: content.fillType.rawValue)
                case .diamond:
                    DrawDiamond(fillType: content.fillType.rawValue)
                case .squiggle:
                    DrawSquiggle(fillType: content.fillType.rawValue)
                }
            }
        }
    }
}

当我 运行 这样做时,视图构建正确。该图像显示了 id 和 n(卡上应包含的符号数)。

Correctly built view

但是,当我向下滚动并返回时,视图重建不正确。符号的数量似乎是完全随机的。这仅在 ScrollView 具有一定长度时发生,因此当卡片数量超过 30 张卡片时(取决于它们的大小)。卡片不会混淆,因为 id 保持不变并且 n 是也没有变化。

Refreshed and incorrectly rebuilt View

我错过了什么吗?这一定与 ScrollViewForEach 交互的方式有关。 CardContentView 结构和其中的 ForEach 语句似乎有问题。我只是不知道。

这些是我在刷新后遇到的错误;

ForEach<Range, Int, ZStack<_ConditionalContent<_ConditionalContent<DrawCircle, DrawDiamond>, DrawSquiggle>>> count (3) != its initial count (2). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!

以下;包含 CardContentCard 结构的整个 Model 以及创建卡片数组的初始化;

struct SetGame {
    private(set) var cards: Array<Card>
    private(set) var deck: Array<Card>
    
    init() {
        deck = []
        cards = []
        var id = 1
        var color = CardContent.Color.red
        var fillType = CardContent.FillType.hollow
        var symbol = CardContent.Symbol.oval
        while deck.count < 81 {
            if deck.count % 27 == 0 {
                switch color {
                case .red:
                    color = .green
                case .green:
                    color = .purple
                case .purple:
                    color = .red
                }
            }
            if deck.count % 9 == 0 {
                switch symbol {
                case .oval:
                    symbol = .diamond
                case .diamond:
                    symbol = .squiggle
                case .squiggle:
                    symbol = .oval
                }
            }
            if deck.count % 3 == 0 {
                switch fillType {
                case .hollow:
                    fillType = .shaded
                case .shaded:
                    fillType = .filled
                case .filled:
                    fillType = .hollow
                }
            }
            deck.append(Card(id: id, content: CardContent(number: 1, color: color, fillType: fillType, symbol: symbol)))
            deck.append(Card(id: id+1, content: CardContent(number: 2, color: color, fillType: fillType, symbol: symbol)))
            deck.append(Card(id: id+2, content: CardContent(number: 3, color: color, fillType: fillType, symbol: symbol)))
            id += 3
        }
        //deck.shuffle()
        while cards.count < 81 {
            //When cards.count > 28; the view starts bugging
            cards.append(deck.popLast()!)
        }
    }
    
    mutating func dealThreeCards() {
        for _ in 0...2 {
            if deck.isEmpty {
                break
            } else {
                cards.append(deck.popLast()!)
            }
        }
        print("I was called :)")
    }
    
    struct Card: Identifiable {
        var isFaceUp = true
        var isMatched = false
        let id: Int
        let content: CardContent
    }
    
    struct CardContent: Equatable {
        let number: Int
        let color: Color
        let fillType: FillType
        let symbol: Symbol
        enum Color { case red, green, purple }
        enum FillType: Double {
            case hollow = 0.0
            case shaded = 0.2
            case filled = 1.0
        }
        enum Symbol { case oval, diamond, squiggle }
    }
}

以下; ContentView 中使用的整个 ViewModel。该应用程序非常简单,但我不明白有什么问题。

class SetGameViewModel: ObservableObject {
    @Published private(set) var game: SetGame
    
    
    init() {
        game = SetGame()
    }
    
    var cards: Array<SetGame.Card> {
        game.cards
    }
    
    func dealThreeCards() {
        game.dealThreeCards()
        print("I was called :)))")
    }
}

最小可重现示例

主要:

import SwiftUI

@main
struct mreApp: App {
    var body: some Scene {
        let game = ViewModel()
        WindowGroup {
            ContentView(viewModel: game)
        }
    }
}

型号:

import Foundation

struct CardGameModel {
    private(set) var cards: Array<Card>
    private(set) var deck: Array<Card>
    
    
    init() {
        //the init only takes care of creating the two arrays
        //deck and cards. It seems to be working correctly and nothing
        //is wrong here, I believe.
        deck = []
        cards = []
        var id = 1
        var color = CardContent.Color.red
        var fillType = CardContent.FillType.hollow
        var symbol = CardContent.Symbol.oval
        while deck.count < 81 {
            if deck.count % 27 == 0 {
                switch color {
                case .red:
                    color = .green
                case .green:
                    color = .purple
                case .purple:
                    color = .red
                }
            }
            if deck.count % 9 == 0 {
                switch symbol {
                case .oval:
                    symbol = .diamond
                case .diamond:
                    symbol = .squiggle
                case .squiggle:
                    symbol = .oval
                }
            }
            if deck.count % 3 == 0 {
                switch fillType {
                case .hollow:
                    fillType = .shaded
                case .shaded:
                    fillType = .filled
                case .filled:
                    fillType = .hollow
                }
            }
            deck.append(Card(id: id, content: CardContent(numberOfShapes: 1, color: color, fillType: fillType, symbol: symbol)))
            deck.append(Card(id: id+1, content: CardContent(numberOfShapes: 2, color: color, fillType: fillType, symbol: symbol)))
            deck.append(Card(id: id+2, content: CardContent(numberOfShapes: 3, color: color, fillType: fillType, symbol: symbol)))
            id += 3
        }
        //deck.shuffle()
        while cards.count < 81 {
            //When cards.count > 28; the view starts bugging.
            //However it also depends on the amount of columns in the
            //LazyVGrid. If more columns are included, the number of cards
            //displayable before bugs is greater.
            
            //Optional
            cards.append(deck.popLast()!)
        }
    }
    
    struct Card: Identifiable {
        let id: Int
        let content: CardContent
    }
    
    struct CardContent: Equatable {
        let numberOfShapes: Int
        let color: Color
        let fillType: FillType
        let symbol: Symbol
        enum Color { case red, green, purple }
        enum FillType: Double {
            case hollow = 0.0
            case shaded = 0.2
            case filled = 1.0
        }
        enum Symbol { case oval, diamond, squiggle }
    }
}

视图模型:

import Foundation

class ViewModel: ObservableObject {
    @Published private(set) var game: CardGameModel
    
    init() {
        game = CardGameModel()
    }
    
    var cards: Array<CardGameModel.Card> {
        game.cards
    }
}

查看:

import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel: ViewModel
    
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(),GridItem(),GridItem(),GridItem()]) {
                ForEach(viewModel.cards) { card in
                    CardView(card: card)
                        .aspectRatio(2/3,contentMode: .fill)
                }
            }
        }
    }
}


struct CardView: View {
    let card: CardGameModel.Card
    private var color: Color {
        switch card.content.color {
        case .green:
            return Color(.green)
        case .purple:
            return Color(.purple)
        case .red:
            return Color(.red)
        }
    }
    
    var body: some View {
        ZStack{
            RoundedRectangle(cornerRadius: 20)
                .stroke(lineWidth: 2.0)
                .foregroundColor(/*@START_MENU_TOKEN@*/.blue/*@END_MENU_TOKEN@*/)
            VStack {
                CardContentView(content: card.content)
                    //The contents of the cards are created
                    .foregroundColor(color)
                Text("\(card.id), n: \(card.content.numberOfShapes)")
            }
        }
    }
}

struct CardContentView: View {
    let content: CardGameModel.CardContent
    var body: some View {
        VStack {
            ForEach(0..<content.numberOfShapes) { _ in
                switch content.symbol {
                case .oval:
                    Circle()
                case .squiggle:
                    RoundedRectangle(cornerRadius: 35.0)
                case .diamond:
                    Rectangle()
                }
            }
        }
    }
    
}

CardContentView 中添加 ForEach 错误消息要求的 id 标识符:

ForEach(0..<content.numberOfShapes, id: \.self) { _ in
       ...
}

已测试并正常工作。