可观察对象模型在模型更新时不会更改视图值

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)
            
        
    
 

【讨论】:

以上是关于可观察对象模型在模型更新时不会更改视图值的主要内容,如果未能解决你的问题,请参考以下文章

模型更改时视图中的列表不会更新

[DataGridCheckBoxColumn在属性更改时不会在MVVM中更新

python设计模式之观察者模式

将更改发布到可观察对象的集合

使用视图控制器淘汰嵌套的可观察对象

在 Core Data 中更改托管对象模型的属性值时崩溃