Search code examples
swiftswiftuics193p

How can I use @EnvironmentObject to initialize @State, without using .onAppear?


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?

  • I can't do this in init(), because @EnvironmentObject is injected after view constructor call
  • I can't do this in .onAppear{ } as well, because it is run after view body.
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


Solution

  • 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
    
        }
    }