IOS 14 将 ObservedObject 添加到 var 导致视图不出现在 iPhone

IOS 14 adding ObservedObject to a var causes views not to appear on iPhone

我正在研究斯坦福在线 CS139p 中的示例。抽取记忆游戏的卡片。我将 viewModel 设为 ObservableObject,发布代表记忆游戏的 var,以及 运行 应用程序。一切都如我所料发生。然后,我将 @ObservedObject 属性 处理器添加到视图控制器中的 viewModel var。当我 运行 应用程序时,我不再看到抽取卡片。

问题 1:发生了什么事?

问题 2:我该如何调试它?我尝试设置一个断点并遍历,但我无法确定出了什么问题。我在谷歌上搜索了一下,发现了一些可以尝试的东西,但最终无法让它发挥作用。这个例子在 1 年前对我有用,当时我第一次尝试它。

MemorizeApp.swift

import SwiftUI

@main
struct MemorizeApp: App {
    var body: some Scene {
        WindowGroup {
            let game = EmojiMemoryGame()
            EmojiMemoryGameView(viewModel: game)
        }
    }
}

EmojiMemoryGameView.swift

import SwiftUI

// uncomment the below line and then comment the line below it to see the condition that does not work. 


struct EmojiMemoryGameView: View {
//    @ObservedObject var viewModel: EmojiMemoryGame
    var viewModel:EmojiMemoryGame
    var body: some View {
        HStack {
            ForEach(viewModel.cards) { card in
                CardView(card: card).onTapGesture {
                    viewModel.choose(card: card)
                }
        }
        }
        .foregroundColor(Color.orange)
        .padding()
        .font(viewModel.cards.count == 10 ? Font.title : Font.largeTitle)
        
    }
}


struct CardView: View {
    var card: MemoryGame<String>.Card
    var body: some View {
        ZStack {
            
            if card.isFaceUp {
                RoundedRectangle(cornerRadius: 10.0).fill(Color.white)
                RoundedRectangle(cornerRadius: 10.0).stroke(lineWidth: 3.0)
                Text(card.content)
            } else {
                RoundedRectangle(cornerRadius: 10.0).fill()
            }
        }
        .aspectRatio(2/3, contentMode: .fit)
    }
}



struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        EmojiMemoryGameView(viewModel: EmojiMemoryGame())
    }
}

EmojiMemoryGame.swift


import SwiftUI

func createCardContent(pairIndex: Int) -> String {
    return ""
}
// This is the viewModel
class EmojiMemoryGame: ObservableObject {
    
    @Published private var model: MemoryGame<String> = EmojiMemoryGame.createMemoryGame()
    
    static func createMemoryGame() -> MemoryGame<String> {
        let emojies = ["", "", "", "", "", "", "", "", "", "", "", ""]
        return MemoryGame<String>(numberOfPairsOfCards: Int.random(in: 2...5)) { pairIndex in
            return emojies.randomElement()!
            //return emojies[pairIndex]
        }
    }
    
    
    // MARK: Access to the Model
    
    var cards: Array<MemoryGame<String>.Card> {
        model.cards.shuffle()
        return model.cards
    }
    
    // MARK: Intent(s)
    
    func choose(card: MemoryGame<String>.Card) {
        model.choose(card: card)
    }

}

MemoryGame.swift

import Foundation

struct MemoryGame<CardContent> {
    
    var cards: Array<Card>
    
    mutating func choose(card: Card) {
        print("Card Choosen: \(card)")
        let chosenIndex: Int = index(of: card)
        cards[chosenIndex].isFaceUp = !cards[chosenIndex].isFaceUp
        
    }
    
    func index(of card: Card) -> Int {
        for index in 0..<cards.count {
            if self.cards[index].id == card.id {
                return index
            }
        }
        return 0 // TODO: bogus!
    }
    
    init(numberOfPairsOfCards: Int, cardContentFactory: (Int) -> CardContent) {
        cards = Array<Card>()
        for pairIndex in 0..<numberOfPairsOfCards {
            let content = cardContentFactory(pairIndex)
            cards.append(Card(content: content, id: pairIndex*2))
            cards.append(Card(content: content, id: pairIndex*2+1))
        }
    }
    
    struct Card: Identifiable {
        
        
        var isFaceUp: Bool = true
        var isMatched: Bool = false
        var content: CardContent
        var id: Int
    }
}

创建一个对象 @ObservedObject 每当观察到的对象发生变化时,视图 re-render 本身就会创建,这是通过其 @Published 属性确定的(也可以直接通过 objectWilLChange,但它不适用于此处)。

您有一个 @Published 属性,即:

@Published private var model: MemoryGame<String> = ...

(重要的是它是私有的,对象仍然通知观察视图)

因此,每当 model 更新时,视图 re-renders.

而且,事实上,模型总是在计算 属性 cards 中更新,视图在其 ForEach:

中访问
var cards: Array<MemoryGame<String>.Card> {
    model.cards.shuffle() // this mutates the model
    return model.cards
}

所以,基本上,作为视图 re-computes 它的主体,它访问它的视图模型,导致它变异为 side-effect,状态已经失效,它需要另一个 re-computation - 在无限循环中。


将 side-effect 放在 getter 中通常是一种不好的做法。

解决方法是从 getter 中删除 model.cards.shuffle() 并将其放在其他有意义的地方。