You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用@EnvironmentObject初始化@State且无需使用.onAppear

问题原因分析

强制解包崩溃原因

你的判断完全正确:@State 初始值为空字典,onAppear 执行时机晚于 body 的第一次渲染。第一次渲染列表时,gamesBeingPlayed[theme.id] 取值为 nil,强制解包直接触发崩溃。

空合运算符写法始终打开第一个主题的原因

iOS 16 之前版本的 SwiftUI 中,NavigationLink 的 destination 参数会在 body 渲染时立即求值,而非点击跳转时才求值。第一次渲染时gamesBeingPlayed 还未被 onAppear 赋值,所有 NavigationLink 的 destination 都直接使用了空合运算符后的默认值themeStore.themes[0],后续gamesBeingPlayed 更新也不会刷新已经生成的 destination,因此所有跳转都打开第一个主题。

可行解决方案

方案1:增加加载状态,初始化完成后再渲染列表

在gamesBeingPlayed未完成初始化时展示占位加载视图,初始化完成后再渲染列表,完全规避空值问题:

import SwiftUI

struct ThemeChooserView: View {
    @EnvironmentObject var themeStore: ThemeStore
    @State private var gamesBeingPlayed: Dictionary<Theme.ID, EmojiMemoryGame> = [:]
        
    var body: some View {
        NavigationView {
            // 增加加载状态判断
            if gamesBeingPlayed.isEmpty {
                ProgressView()
                    .onAppear {
                        var games: Dictionary<Theme.ID, EmojiMemoryGame> = [:]
                        for theme in themeStore.themes {
                            games[theme.id] = EmojiMemoryGame(theme: theme)
                        }
                        gamesBeingPlayed = games
                    }
            } else {
                List {
                    ForEach(themeStore.themes) { theme in
                        NavigationLink(destination: EmojiMemoryGameView(game: gamesBeingPlayed[theme.id]!)) {
                            VStack(alignment: .leading) {
                                Text(theme.name)
                                    .foregroundColor(theme.color)
                                    .font(.title)
                                Text(themeCardsDescription(theme: theme))
                            }
                        }
                    }
                }
                .navigationTitle("Memorize")
            }
        }
        // 原onAppear可以移除,已经移到加载视图的onAppear中
    }
    
    private func themeCardsDescription(theme: Theme) -> String {
        let numberOrAll = theme.numberOfPairsOfCards == theme.emojis.count ? "All" : "\(theme.numberOfPairsOfCards)"
        return numberOrAll + " pairs from \(theme.emojis.joined(separator: ""))"
    }
}

该方案逻辑简单无额外依赖,适合不需要动态增删主题的场景。

方案2:使用LazyView实现destination懒加载,按需创建游戏实例

先定义通用的懒加载容器LazyView,让destination只有在跳转时才求值,同时实现游戏实例的按需创建存储,不需要提前初始化全部游戏:
首先定义LazyView:

struct LazyView<Content: View>: View {
    let build: () -> Content
    init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }
    var body: Content {
        build()
    }
}

再修改ThemeChooserView的代码:

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
                    // destination用LazyView包裹,点击跳转时才会执行内部代码
                    NavigationLink(destination: LazyView(EmojiMemoryGameView(game: getOrCreateGame(for: theme)))) {
                        VStack(alignment: .leading) {
                            Text(theme.name)
                                .foregroundColor(theme.color)
                                .font(.title)
                            Text(themeCardsDescription(theme: theme))
                        }
                    }
                }
            }
            .navigationTitle("Memorize")
        }
        // 不需要onAppear提前初始化游戏
    }
    
    // 按需获取/创建游戏实例
    private func getOrCreateGame(for theme: Theme) -> EmojiMemoryGame {
        if let existGame = gamesBeingPlayed[theme.id] {
            return existGame
        }
        let newGame = EmojiMemoryGame(theme: theme)
        gamesBeingPlayed[theme.id] = newGame
        return newGame
    }
    
    private func themeCardsDescription(theme: Theme) -> String {
        let numberOrAll = theme.numberOfPairsOfCards == theme.emojis.count ? "All" : "\(theme.numberOfPairsOfCards)"
        return numberOrAll + " pairs from \(theme.emojis.joined(separator: ""))"
    }
}

该方案支持动态增删主题的场景,也不需要额外的加载占位视图。

内容的提问来源于stack exchange,提问作者user14119170

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 02:45:01