You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在单个View中创建多个ViewModel是否符合MVVM设计规范?

问题:卡牌游戏MVVM架构设计疑问

我有一个包含52张卡牌的Game View,相关代码如下:

卡牌模型定义

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

简化后的GameViewModel

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
}

对应的GameView代码

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

原本我把所有游戏逻辑都放在GameViewModel中,甚至通过self.cards[i]操作卡牌,但现在发现需要对每张卡牌进行更精细的控制,因此考虑为每张Card创建对应的ViewModel。

有两个核心疑问:

  1. 创建52个CardViewModel是否符合MVVM设计模式?是否应该遵循一个View对应一个ViewModel的原则?
  2. CardViewModel应该在哪里创建?是在View中,还是像以下代码一样由GameViewModel创建?
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: ""))
        }
    }
}

注:该问题可扩展至所有一对多关系场景,如CategoryVM->Items、GroupVM->Individuals、FamilyVM->People等。


回答

1. 创建多个CardViewModel完全符合MVVM设计模式

MVVM的核心是视图与数据逻辑解耦,并非严格遵循“一个View对应一个ViewModel”的刻板规则。当单个模型(Card)需要承载独立的交互逻辑、状态管理(比如卡牌的翻转状态、选中状态、动画触发逻辑等)时,为每个Card创建专属的CardViewModel是合理的:

  • 它能把原本堆砌在GameViewModel中的卡牌细节逻辑拆解出去,让GameViewModel专注于全局游戏逻辑(比如洗牌、胜负判定、回合管理)
  • 每张卡牌的状态变化可以独立通知视图,避免因单个卡牌变动导致整个卡牌数组的不必要刷新
  • 这种设计也符合单一职责原则,每个ViewModel只负责自己范围内的逻辑

所谓“一个View对应一个ViewModel”,更多是指页面级/容器级的对应关系,对于列表项、组件级的视图,完全可以为每个子项创建独立的ViewModel,这在MVVM架构中是常见实践。

2. CardViewModel应该由GameViewModel负责创建和管理

不建议在View中创建CardViewModel,原因如下:

  • GameViewModel作为游戏的全局管理者,应该掌控所有卡牌的生命周期,包括创建、初始化、销毁以及状态同步(比如洗牌时重置所有卡牌状态)
  • 如果在View中创建,卡牌的状态会分散在View层,难以在GameViewModel中统一控制全局逻辑(比如批量重置卡牌状态)
  • 保持数据逻辑的集中管理,能让代码更易维护和测试,比如你可以在GameViewModel中统一初始化卡牌的花色、数值,而不需要在View中处理这些数据逻辑

具体实现时,建议让CardViewModel持有Card模型的引用,而非重复存储suit和value:

class CardViewModel: ObservableObject {
    let card: Card
    @Published var isFlipped = false
    @Published var isSelected = false

    init(card: Card) {
        self.card = card
    }

    // 卡牌专属逻辑,比如翻转操作
    func flip() {
        isFlipped.toggle()
    }
}

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

    init(numberOfCards: Int) {
        self.numberOfCards = numberOfCards
        // 生成52张实际卡牌
        let allCards = generateAllCards()
        self.cardViewModels = allCards.map { CardViewModel(card: $0) }
    }

    private func generateAllCards() -> [Card] {
        let suits = ["♠️", "♥️", "♦️", "♣️"]
        let values = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
        return suits.flatMap { suit in
            values.map { value in
                Card(suit: suit, value: value)
            }
        }
    }

    // 全局游戏逻辑,比如洗牌
    func shuffle() {
        cardViewModels.shuffle()
    }
}

扩展到通用一对多场景

对于CategoryVM->Items、GroupVM->Individuals这类一对多关系,逻辑完全一致:

  • 父级ViewModel(CategoryVM/GroupVM)负责子项ViewModel的创建、管理和全局逻辑
  • 子项ViewModel负责单个子项的独立状态和交互逻辑
  • 视图层只需要通过父级ViewModel拿到子项ViewModel数组,遍历渲染即可

内容的提问来源于stack exchange,提问作者Stan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 15:50:29