I am making a card game and I'm trying to fit cards into a ScrollView
. I'm doing this by iterating through an array of cards and subsequently building the view for each card with 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() }
}
}
}
Each card has a specific number of symbols (and several other characteristics, but those are not the issue right now.) which it should show. ->
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)
}
}
}
}
}
When I run this, the View builds correctly. The image shows the id and the n (number of symbols that should be on the card.)
However when I scroll down and back up, the View gets rebuilt incorrectly. The number of symbols seems to be completely random. This only happens when the ScrollView
is of a certain length, so when the number of cards is over 30 cards (depending on their size.) The cards aren't getting mixed up because the id remains the same and n is also unchanged.
Refreshed and incorrectly rebuilt View
Am I missing something? It must be something to do with the way ScrollView
and ForEach
interact. It seems to be a problem with the CardContentView
struct and the ForEach
statement within it. I just don't know what.
These are the errors I'm getting after refreshing;
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 toIdentifiable
or useForEach(_:id:content:)
and provide an explicitid
!
Below; the entire Model
which holds the CardContent
and Card
structs along with the init that creates the cards array;
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 }
}
}
Below; the entire ViewModel which is used in the ContentView. The app is extremely simple, yet I don't understand what's not working.
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 :)))")
}
}
Main:
import SwiftUI
@main
struct mreApp: App {
var body: some Scene {
let game = ViewModel()
WindowGroup {
ContentView(viewModel: game)
}
}
}
Model:
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 }
}
}
ViewModel:
import Foundation
class ViewModel: ObservableObject {
@Published private(set) var game: CardGameModel
init() {
game = CardGameModel()
}
var cards: Array<CardGameModel.Card> {
game.cards
}
}
View:
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()
}
}
}
}
}
In CardContentView
add in the ForEach
the id
identifier as the error message requests it:
ForEach(0..<content.numberOfShapes, id: \.self) { _ in
...
}
Tested and working.