如何使用@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
相关产品推荐
相关产品推荐

