Is it appropriate to create multiple ViewModels in one View?

Viewed 77

This is more of a code architecture question.

I have a Game View with a deck of 52 cards. The model is:

struct Card {
    let suit: String
    let value: String
}

The simplified ViewModel is:

class GameViewModel: ObservableObject {
    let numberOfCards: Int
    @Published var cards: [Card] = []

    init(numberOfCards: Int) {
        self.numberOfCards = numberOfCards
        for _ in 0..<numberOfCards {
            cards.append(Card(suit: "", value: ""))
        }
    }
//Other logic methods
}

And the View:

struct GameView: View {
    @StateObject var gameVM = GameViewModel(numberOfCards: 52)
...
}

I've kept all game logic in GameViewModel, even manipulated cards via its self.cards[i], but now I've realized that I will need more control over each Card and thus probably a ViewModel for each Card.

Question: is creating 52 more ViewModels a good MVVM design pattern? Or one View = one ViewModel?

And where should I create Cards ViewModels? In the View? Or GameViewModel should create them like:

class GameViewModel: ObservableObject {
    let numberOfCards: Int    
    @Published var cards: [CardViewModel] = []

    init(numberOfCards: Int) {
        self.numberOfCards = numberOfCards
        for _ in 0..<numberOfCards {
            cards.append(CardViewModel(suit: "", value: ""))
        }
    }
}

P.S. I feel like this question can be expanded to any example where we have One-Many relations like CategoryVM->Items, or GroupVM->Individuals, or FamilyVM->People.

1 Answers

You can add funcs to the Card, e.g.

struct Card {
    let suit: String
    let value: String
    var somethingYouWantToChange
 
    mutating func someMethod() {

    }
}

In swiftUI we usually use the names "Model" or "Store" for the single object that stores the model structs, e.g.

class GameModel: ObservableObject {
    @Published var cards: [Card] = []

When you want to pass the card along and require write access, we use @Binding which converts the View struct into a View model struct, e.g.

struct CardView: View {
    @Binding var card: Card

    var body: some View {
        Text(card.value, format: .number) // Text automatically reformats the UILabel when region settings change.
        Button("Some action") {
            card.someMutatingFunc()
        }
    }

It's passed in like:

CardView(card: $card)

ForEach is usually the view model that creates and manages the bindings, e.g.

ForEach($gameModel.cards) $card in {

To make this word you have to make Card conform to Identifiable and give it an id var, usually let id = UUID() but in your case it might be var id: String { suit + value }.

Related