I am following Stanfords' CS193p Developing Apps for iOS online course. I'm trying to do the Assignment 6 (Memorize Themes.pdf).
When I run my app in simulator, I get the following fatal error: Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
I think I probably understand why it gives this error - because gamesBeingPlayed is initialized to empty, and it is assigned proper value in onAppear, which runs AFTER var body.
So my question is: How can I initialize @State private var gamesBeingPlayed?
import SwiftUI
struct ThemeChooserView: View {
@EnvironmentObject var themeStore: ThemeStore
@State private var gamesBeingPlayed: Dictionary<Theme.ID, EmojiMemoryGame> = [:]
var body: some View {
NavigationView {
List {
ForEach(themeStore.themes) { theme in
NavigationLink(destination: EmojiMemoryGameView(game: gamesBeingPlayed[theme.id]!)) {
// Here I get error: "Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value"
VStack(alignment: .leading) {
Text(theme.name)
.foregroundColor(theme.color)
.font(.title)
Text(themeCardsDescription(theme: theme))
}
}
}
}
.navigationTitle("Memorize")
}
.onAppear {
var games: Dictionary<Theme.ID, EmojiMemoryGame> = [:]
for theme in themeStore.themes {
games[theme.id] = EmojiMemoryGame(theme: theme)
}
gamesBeingPlayed = games
}
}
private func themeCardsDescription(theme: Theme) -> String {
let numberOrAll = theme.numberOfPairsOfCards == theme.emojis.count ? "All" : "\(theme.numberOfPairsOfCards)"
return numberOrAll + " pairs from \(theme.emojis.joined(separator: ""))"
}
}
If I use nil coalescing operator, like this however:
NavigationLink(destination: EmojiMemoryGameView(game: gamesBeingPlayed[theme.id] ?? EmojiMemoryGame(theme: themeStore.themes[0]))) {
... then when I tap to navigate to the chosen game theme, it always is this first one: themeStore.themes[0]
. I have no idea why to be honest. I thought onAppear should set gamesBeingPlayed by the time I tap on a View in the List to navigate to.
Please help me
If to answer question as it is postulated
How can I use @EnvironmentObject to initialize @State, without using .onAppear?
then here is a possible approach - move all content into internal view which accepts environment object as input argument (because it is already present in body), like
struct ThemeChooserView: View {
@EnvironmentObject var themeStore: ThemeStore
var body: some View {
ThemeChooserViewImp(themeStore) // << here !!
}
private struct ThemeChooserViewImp: View {
@State private var gamesBeingPlayed: Dictionary<Theme.ID, EmojiMemoryGame> // << declare only
init(_ themeStore: ThemeStore) {
_gamesBeingPlayed = State(initialValue: ...) // << use themeStore
}
// ... other content here
}
}