可观察对象模型在模型更新时不会更改视图值
Posted
技术标签:
【中文标题】可观察对象模型在模型更新时不会更改视图值【英文标题】:Observable object model not changing View values when model is updated 【发布时间】:2021-08-16 04:12:32 【问题描述】:我正在失去理智,请帮忙
我正在关注斯坦福的 ios 教程,我正在尝试完成创建纸牌游戏的任务,我有 3 个模型,游戏、纸牌、主题和主题:
Game和Card负责主要的游戏逻辑
import Foundation
struct Game
var cards: [Card]
var score = 0
var isGameOver = false
var theme: Theme
var choosenCardIndex: Int?
init(theme: Theme)
cards = []
self.theme = theme
startTheme()
mutating func startTheme()
cards = []
var contentItems: [String] = []
while contentItems.count != theme.numberOfPairs
let randomElement = theme.emojis.randomElement()!
if !contentItems.contains(randomElement)
contentItems.append(randomElement)
let secondContentItems: [String] = contentItems.shuffled()
for index in 0..<theme.numberOfPairs
cards.append(Card(id: index*2, content: contentItems[index]))
cards.append(Card(id: index*2+1, content: secondContentItems[index]))
mutating func chooseCard(_ card: Card)
print(card)
if let foundIndex = cards.firstIndex(where: $0.id == card.id),
!cards[foundIndex].isFaceUp,
!cards[foundIndex].isMatchedUp
if let potentialMatchIndex = choosenCardIndex
if cards[foundIndex].content == cards[potentialMatchIndex].content
cards[foundIndex].isMatchedUp = true
cards[potentialMatchIndex].isMatchedUp = true
choosenCardIndex = nil
else
for index in cards.indices
cards[index].isFaceUp = false
cards[foundIndex].isFaceUp.toggle()
print(card)
mutating func endGame()
isGameOver = true
mutating func penalizePoints()
score -= 1
mutating func awardPoints ()
score += 2
struct Card: Identifiable, Equatable
static func == (lhs: Game.Card, rhs: Game.Card) -> Bool
return lhs.content == rhs.content
var id: Int
var isFaceUp: Bool = false
var content: String
var isMatchedUp: Bool = false
var isPreviouslySeen = false
主题用于建模不同类型的内容,主题用于跟踪当前正在使用的内容并获取新内容
import Foundation
import SwiftUI
struct Theme: Equatable
static func == (lhs: Theme, rhs: Theme) -> Bool
return lhs.name == rhs.name
internal init(name: String, emojis: [String], numberOfPairs: Int, cardsColor: Color)
self.name = name
self.emojis = Array(Set(emojis))
if(numberOfPairs > emojis.count || numberOfPairs < 1)
self.numberOfPairs = emojis.count
else
self.numberOfPairs = numberOfPairs
self.cardsColor = cardsColor
var name: String
var emojis: [String]
var numberOfPairs: Int
var cardsColor: Color
import Foundation
struct Themes
private let themes: [Theme]
public var currentTheme: Theme?
init(_ themes: [Theme])
self.themes = themes
self.currentTheme = getNewTheme()
private func getNewTheme() -> Theme
let themesIndexes: [Int] = Array(0..<themes.count)
var visitedIndexes: [Int] = []
while(visitedIndexes.count < themesIndexes.count)
let randomIndex = Int.random(in: 0..<themes.count)
let newTheme = themes[randomIndex]
if newTheme == currentTheme
visitedIndexes.append(randomIndex)
else
return newTheme
return themes.randomElement()!
mutating func changeCurrentTheme() -> Theme
self.currentTheme = getNewTheme()
return self.currentTheme!
这是我的虚拟机:
class GameViewModel: ObservableObject
static let numbersTheme = Theme(name: "WeirdNumbers", emojis: ["1", "2", "4", "9", "20", "30"], numberOfPairs: 6, cardsColor: .pink)
static let emojisTheme = Theme(name: "Faces", emojis: ["????", "????", "????", "????", "????", "????", "????", "????"], numberOfPairs: 8, cardsColor: .blue)
static let carsTheme = Theme(name: "Cars", emojis: ["????", "????️", "????", "????", "????", "????", "????", "????"], numberOfPairs: 20, cardsColor: .yellow)
static let activitiesTheme = Theme(name: "Activities", emojis: ["????", "????️", "????♂️", "????", "????♂️", "????️", "????♂️"], numberOfPairs: -10, cardsColor: .green)
static let fruitsTheme = Theme(name: "Fruits", emojis: ["????", "????", "????", "????", "????", "????", "????", "????"], numberOfPairs: 5, cardsColor: .purple)
static var themes = Themes([numbersTheme, emojisTheme, carsTheme, fruitsTheme])
static func createMemoryGame() -> Game
Game(theme: themes.currentTheme!)
@Published private var gameController: Game = Game(theme: themes.currentTheme!)
func createNewGame()
gameController.theme = GameViewModel.themes.changeCurrentTheme()
gameController.startTheme()
func choose(_ card: Game.Card)
objectWillChange.send()
gameController.chooseCard(card)
var cards: [Game.Card]
return gameController.cards
var title: String
return gameController.theme.name
var color: Color
return gameController.theme.cardsColor
这是我的观点:
struct ContentView: View
var columns: [GridItem] = [GridItem(.adaptive(minimum: 90, maximum: 400))]
@ObservedObject var ViewModel: GameViewModel
var body: some View
VStack
HStack
Spacer()
Button(action:
ViewModel.createNewGame()
, label:
VStack
Image(systemName: "plus")
Text("New game")
.font(/*@START_MENU_TOKEN@*/.caption/*@END_MENU_TOKEN@*/)
)
.font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
.padding(.trailing)
Section
VStack
Text(ViewModel.title)
.foregroundColor(/*@START_MENU_TOKEN@*/.blue/*@END_MENU_TOKEN@*/)
.font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
ScrollView
LazyVGrid(columns: columns )
ForEach(ViewModel.cards, id: \.id) card in
Card(card: card, color: ViewModel.color)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture
ViewModel.choose(card)
.font(.largeTitle)
.padding()
Text("Score")
.frame(maxWidth: .infinity, minHeight: 30)
.background(Color.blue)
.foregroundColor(/*@START_MENU_TOKEN@*/.white/*@END_MENU_TOKEN@*/)
Spacer()
HStack
Spacer()
Text("0")
.font(.title2)
.bold()
Spacer()
struct Card: View
let card: Game.Card
let color: Color
var body: some View
ZStack
let shape = RoundedRectangle(cornerRadius: 10)
if card.isFaceUp
Text(card.content)
shape
.strokeBorder()
.accentColor(color)
.foregroundColor(color)
else
shape
.fill(color)
基本上问题出在
.onTapGesture
ViewModel.choose(card)
在 View 中,当有人点击卡片时,卡片的 isFaceUp 属性会更改为 true,但这不会反映在 UI 中。 如果我通过更改主题和添加新卡片来生成新视图,则可以。
Button(action:
ViewModel.createNewGame()
, label:
VStack
Image(systemName: "plus")
Text("New game")
.font(/*@START_MENU_TOKEN@*/.caption/*@END_MENU_TOKEN@*/)
)
但是当我尝试翻转卡片时它不起作用,游戏模型中的值发生了变化,但视图上没有更新
点击后 ViewModel 调用选择方法
func choose(_ card: Game.Card)
gameController.chooseCard(card)
这通过调用chooseCard方法改变了Game.swift文件中Model的值
mutating func chooseCard(_ card: Card)
print(card)
if let foundIndex = cards.firstIndex(where: $0.id == card.id),
!cards[foundIndex].isFaceUp,
!cards[foundIndex].isMatchedUp
if let potentialMatchIndex = choosenCardIndex
if cards[foundIndex].content == cards[potentialMatchIndex].content
cards[foundIndex].isMatchedUp = true
cards[potentialMatchIndex].isMatchedUp = true
choosenCardIndex = nil
else
for index in cards.indices
cards[index].isFaceUp = false
cards[foundIndex].isFaceUp.toggle()
print(card)
值改变但视图不变,GameViewModel的gameController变量有@Published状态,它指向一个Game模型结构体的实例
@Published private var gameController: Game = Game(theme: themes.currentTheme!)
它使用 @ObservedObject 属性访问此 GameViewModel 的视图
@ObservedObject var ViewModel: GameViewModel
我以为我做的一切都是对的,但我想不是,哈哈,我到底做错了什么?如果我在 ViewModel 上使用已发布和可观察的对象,为什么无法更新我的视图?哈哈
【问题讨论】:
您在换卡之前先发送更改 @Paulw11 我不明白,你是什么意思? 你打电话给objectWillChange.send()
然后然后你换卡。尝试颠倒choose(_ card:)
中这两行的顺序
我这样做了,但它不起作用,实际上我认为甚至不需要 objectWillChange.send() 因为我的控制器具有在更改时应该更新的数据数组视图有@Published 关键字,类有ObservableObject 协议,所以idk,还有什么问题?
我看过 Stanford 的视频,看到他的 app 可以工作,但是我不知道怎么做,因为 @Published 属性和视图之间没有绑定;你所看到的正是我所期望的
【参考方案1】:
卡片视图没有看到更改的主要原因是因为在您的卡片视图中,您确实放置了一个相等的一致性协议,您在其中指定了一个相等检查 ==
函数,该函数仅检查内容而不是其他变量更改
static func ==(lhs: Game.Card, rhs: Game.Card) -> Bool
lhs.content == rhs.content
// && lhs.isFaceUp && rhs.isFaceUp //<- you can still add this
如果您删除 equatable 协议并让 swift 检查是否相等,则它应该是您的基本解决方案的最小更改。
我仍然会使用更改类卡状态的解决方案,以便视图可以以ObservableObject
对更改做出反应,@Published
用于视图需要跟踪的更改,如下所示:
class Card: Identifiable, Equatable, ObservableObject
var id: Int
@Published var isFaceUp: Bool = false
var content: String
@Published var isMatchedUp: Bool = false
var isPreviouslySeen = false
internal init(id: Int, content: String)
self.id = id
self.content = content
static func ==(lhs: Game.Card, rhs: Game.Card) -> Bool
lhs.content == rhs.content
而在Card
视图中卡片变量将变为
struct Card: View
@ObservedObject var card: Game.Card
...
顺便说一句,您不需要通知视图更改
objectWillChange.send()
如果您已经在使用 @Published
表示法。对变量的每个设置都会触发更新。
【讨论】:
谢谢,这行得通,但我不知道为什么当斯坦福教练不需要将卡片类型从结构更改为类以查看视图时,我为什么要这样做检测更改:/,视图检测 Game.Theme 何时更改,但不检测 Game.Cards[index].content 何时更改,但它也可以检测 Game.Cards 何时更改,因为当我更改卡片时,视图会重建,所以idkkkkk 这是怎么回事大声笑 我编辑了答案,因此它对基本解决方案的影响最小。基本上,您将需要删除 equatable 协议并让 swift 处理其余部分。通常第二部分是我习惯于推理的,所以我可以只更新最小的组件,而不是在单个字段更改时重绘整个堆栈 哇,谢谢,你是对的,问题在于 Card 结构符合 equatable 协议,这意味着当结构符合该协议时,视图只会检查字段在协议 func 中指定的那些? 当您添加static func ==
时,如果它符合“Equatable”协议,您基本上会覆盖该结构的基本相等检查。对于 swiftui 来说,即使不符合“Equatable”协议,它也是一个结构就足够了,它会使用每个属性来检查两个对象之间的差异,但在某些情况下,我认为最好指定两个结构何时相等,否则您可能会遇到在这里遇到的同样问题。【参考方案2】:
你可以试试这个,而不是声明 Card 一个类:
Card(card: card, color: ViewModel.color, isFaceUp: card.isFaceUp)
并将其添加到卡片视图中:
let isFaceUp: Bool
我的理解是卡片视图看不到卡片的任何变化(不知道为什么,可能是因为它在 if 中),
但是如果你给它一些真正改变的东西,那么它就会重新渲染。如前所述,不需要objectWillChange.send()
编辑1:
您也可以在“ContentView”中执行此操作:
Card(viewModel: ViewModel, card: card)
然后
struct Card: View
@ObservedObject var viewModel: GameViewModel
let card: Game.Card
var body: some View
ZStack
let shape = RoundedRectangle(cornerRadius: 10)
if card.isFaceUp
Text(card.content)
shape
.strokeBorder()
.accentColor(viewModel.color)
.foregroundColor(viewModel.color)
else
shape.fill(viewModel.color)
【讨论】:
以上是关于可观察对象模型在模型更新时不会更改视图值的主要内容,如果未能解决你的问题,请参考以下文章