【问题标题】: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 属性和视图之间没有绑定;你所看到的正是我所期望的

标签: ios swift mvvm swiftui


【解决方案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)
            }
        }
    }
} 

【讨论】:

    猜你喜欢
    • 2012-07-25
    • 2016-12-26
    • 2014-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-30
    • 2020-02-19
    • 1970-01-01
    相关资源
    最近更新 更多