在单个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。
有两个核心疑问:
- 创建52个CardViewModel是否符合MVVM设计模式?是否应该遵循一个View对应一个ViewModel的原则?
- 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
相关产品推荐
相关产品推荐

