1

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

user14119170
  • 1,191
  • 3
  • 8
  • 21
  • Why do you even need gamesBeingPlayed? Can’t you create the object when you create the link, `NavigationLink(destination: EmojiMemoryGameView(game: EmojiMemoryGame(theme: theme))`? – Joakim Danielson Nov 13 '21 at 12:19
  • I'd like to do it this way, because it says so in the [Assignment 6.pdf](https://cs193p.sites.stanford.edu/sites/g/files/sbiybj16636/files/media/file/assignment_6.pdf) Hints section: 7. Picking a good architecture for the data in an application is crucial to making your code clean and easy to understand. Normally we don’t propose any specific data architecture so you can experience this design aspect yourself (as you did in A3), but we will suggest something in this case because there’s a lot of UI to do in this assignment and laying a lot of data-structure-architecture work – user14119170 Nov 13 '21 at 12:26
  • on you might distract from that. Our suggestion? Put the source of truth for the games being played into your theme chooser’s View in an @State which is a Dictionary whose keys are theme identifiers and whose values are the EmojiMemoryGame ViewModels for the games you can navigate to. You can use this Dictionary each time you navigate to play the game (to get the ViewModel to use) and to update the theme in all of the games being played whenever the user edits any themes (i.e. on any change of the themes and probably on the first appearance of your theme chooser too). – user14119170 Nov 13 '21 at 12:26
  • 2
    Then use the dictionary (keys or values) for your ForEach loop instead of the themseStore.themes and use onAppear as you do now. This way you will not get a crash since the ForEach loop will not be executed while the dictionary is empty. – Joakim Danielson Nov 13 '21 at 12:47
  • `ForEach(gamesBeingPlayed.keys, id: \.self) { themeID in NavigationLink(destination: EmojiMemoryGameView(game: gamesBeingPlayed[themeID]!)) { VStack(alignment: .leading) { Text(gamesBeingPlayed[themeID]!.theme.name) .foregroundColor(gamesBeingPlayed[themeID]!.theme.color) .font(.title) Text(themeCardsDescription(theme: gamesBeingPlayed[themeID]!.theme)) } } } ` – user14119170 Nov 13 '21 at 13:42
  • Thank you, so I did it like this ⬆️above, but then I get this error: "Generic struct 'ForEach' requires that 'Dictionary.Keys' (aka 'Dictionary.Keys') conform to 'RandomAccessCollection'" do you know how to get rid of it? It seems like I have to make UUID conform to RandomAccessCollection, but I am new to Swift, and this seems to be beyond my knowledge. Could you help me solve this? – user14119170 Nov 13 '21 at 13:45
  • Have you studied SwiftUI's `PreferenceKey` in this class yet? – valeCocoa Nov 13 '21 at 13:49
  • No, I didn't. Reading it (the documentation of PreferenceKey) now, but hard to understand to me with my current knowledge. Is there other way to do it, I doubt they would require this, maybe I should use Int, not the UUID for my keys (and therefore for my theme id), would it solve this? – user14119170 Nov 13 '21 at 13:59
  • Use `values` instead? – Joakim Danielson Nov 13 '21 at 14:13
  • 1
    Well using a `PrefernceKey` here wold be the optimal solution, but since you ought use what you've already studied so far, then go with the solution Joakim Danielson provided, and don't use the `keys` of the dictionary as elements of the `ForEach` but an array of them: `ForEach(Array(gamesBeingPlayed.keys), id: \.self) {…}` or if `EmojiMemoryGame` values conform to `Identifiable` then use an array made out of the dictionary `values`. – valeCocoa Nov 13 '21 at 14:39

1 Answers1

2

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

    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690