0

I am trying to code the board game Splendor as a simple coding project in SwiftUI. I'm a bit of a hack so please point me in the right direction if I've got this all wrong. I'm also attempting to use the MVVM paradigm.

The game board has two stacks of tokens on it, one for unclaimed game tokens on and one for the tokens of the active player. There is a button on the board which allows the player to claim a single token at a time.

The tokens are drawn in a custom view - TokenView() - which draws a simple Circle() offset by a small amount to make a stack. The number of circles matches the number of tokens. Underneath each stack of tokens is a Text() which prints the number of tokens.

When the button is pressed, only the Text() updates correctly, the number of tokens drawn remains constant.

I know my problem something to do with the fact that I'm mixing an @ObservedObject and a static Int. I can't work out how to not use the Int, as TokenView doesn't know whether it's drawing the board's token collection or the active players token collection. How do I pass the count of tokens as an @ObservedObject? And why does Text() update correctly?

Model:

struct TheGame {
    var tokenCollection: TokenCollection
    var players: [Player]
    
    init() {
        self.tokenCollection = TokenCollection()
        self.players = [Player()]
    }
}

struct Player {
    var tokenCollection: TokenCollection
    
    init() {
        self.tokenCollection = TokenCollection()
    }
}

struct TokenCollection {
    var count: Int
    
    init() {
        self.count = 5
    }
}

ViewModel:

class MyGame: ObservableObject {
    @Published private (set) var theGame = TheGame()
    
    func collectToken() {
        theGame.tokenCollection.count -= 1
        theGame.players[0].tokenCollection.count += 1
    }
}

GameBoardView:

struct GameBoardView: View {
    @StateObject var myGame = MyGame()

    var body: some View {
        VStack{
            TokenStackView(myGame: myGame, tokenCount: myGame.theGame.tokenCollection.count)
                .frame(width: 100, height: 200, alignment: .center)
            Button {
                myGame.collectToken()
            } label: {
                Text ("Collect Token")
            }
            TokenStackView(myGame: myGame, tokenCount: myGame.theGame.players[0].tokenCollection.count)                .frame(width: 100, height: 200, alignment: .center)
        }
    }
}

TokenStackView:

struct TokenStackView: View {
    @ObservedObject var myGame: MyGame
    
    var tokenCount: Int
    
    var body: some View {
        VStack {
            ZStack {
                ForEach (0..<tokenCount) { index in
                    Circle()
                        .stroke(lineWidth: 5)
                        .offset(x: CGFloat(index * 10), y: CGFloat(index * 10))
                }
            }
            Spacer()
            Text("\(tokenCount)")
        }
    }
}
user1357607
  • 214
  • 4
  • 13
  • 1
    If you wanna your question to be answered, you wanna make time for an expert to get working sample with reproducible problem as fast as possible. Perfectly I should just paste your code into my sample project and see the problem as fast as I run it. In your case `GemType` and other classes are unknown. Also description is too long. And I'm not asking to add these classes, I'm asking for a [minimal reproducible example][https://stackoverflow.com/help/minimal-reproducible-example], so you need to remove all issue unrelated code, locallize your problem both in code and in question description – Phil Dukhov Aug 15 '21 at 05:21
  • Thanks Philip. I've attempted to do just that. You can copy and paste my snippets of code and it should work. – user1357607 Aug 15 '21 at 11:01
  • 1
    Thanks, that's much better – Phil Dukhov Aug 15 '21 at 11:27

1 Answers1

1

If you take a look at your console you'll see the error:

ForEach<Range<Int>, Int, OffsetShape<_StrokedShape<Circle>>> 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`!

The fix is pretty easy in this case, just add id, like this:

ForEach(0..<tokenCount, id: \.self) {
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • Thanks!! I spent a good three hours of precious baby-sleeping time on this and it was that simple! Can you tell me why tokenCount works as one of the "published" variables, when its a static Int? From my (clearly wrong) understanding that MyGame, including all it's properties, is the @StateObject. When I call TokenStackView and pass it the tokenCount, I'm actuating passing an integer value not the published object. I do understand that all properties of a struct/class are published on update, but I *thought* I was copying a value into tokenCount, not actually passing a reference. – user1357607 Aug 15 '21 at 21:22
  • 1
    @user1357607 You're welcome! `tokenCount` is nut published. Your `@ObservedObject var myGame: MyGame` triggers `GameBoardView` recalculation and passing new values to `TokenStackView`, that's it – Phil Dukhov Aug 16 '21 at 05:49