SwiftUI使用EnvironmentObject时找不到DetailPageViewModel的致命错误
问题描述
使用SwiftUI构建App Store模拟应用,从Firebase拉取数据并以卡片列表展示,点击卡片跳转详情页。最初用@ObservedObject包装ViewModel时页面转场逻辑异常,改为@EnvironmentObject并在顶层视图配置.environmentObject(detailObject)后,出现如下致命错误:
Thread 1: Fatal error: No ObservableObject of type DetailPageViewModel found.
A View.environmentObject(_:) for DetailPageViewModel may be missing as an ancestor of this view.
错误触发点在HomePage的初始化方法中调用detailPageViewModel.getData()处,相关代码如下:
TabBar视图代码
struct TabBar: View { @Namespace var animation @StateObject var detailObject = DetailPageViewModel() var body: some View { ZStack { TabView { //Home Page HomePage(animation: animation) .environmentObject(detailObject) .tabItem({ Image(systemName: "house") Text("Home") }) //Search Page SearchPage() .tabItem({ Image(systemName: "magnifyingglass") Text("Search") }) } if detailObject.show { DetailPage(detailPageViewModel: detailObject, animation: animation) } } } }
HomePage视图代码
struct HomePage: View { var animation: Namespace.ID @EnvironmentObject var detailPageViewModel : DetailPageViewModel var body: some View { ScrollView(.vertical, showsIndicators: true){ Spacer() LazyVStack (alignment: .leading) { ForEach (detailPageViewModel.HomepageList){ recipeDets in HomePageCard(recipeDetail: recipeDets, animation: animation) .onTapGesture { print(detailPageViewModel.show) withAnimation(.interactiveSpring(response: 0.5, dampingFraction: 0.8, blendDuration: 0.8)){ detailPageViewModel.selectedRecipe = recipeDets detailPageViewModel.show.toggle() } print(detailPageViewModel.show) } } } } } init(animation: Namespace.ID) { self.animation = animation detailPageViewModel.getData() // 错误触发点 } }
DetailPage视图代码
struct DetailPage: View { @ObservedObject var detailPageViewModel : DetailPageViewModel var animation: Namespace.ID var body: some View { ScrollView { ZStack { LazyImage(url: URL(string: detailPageViewModel.selectedRecipe.fullImagePath), resizingMode: .aspectFill) .aspectRatio(contentMode: .fit) .matchedGeometryEffect(id: detailPageViewModel.selectedRecipe.fullImagePath, in: animation) .ignoresSafeArea() } }.ignoresSafeArea(.all, edges: .top) .statusBar(hidden: true) } }
DetailPageViewModel代码
class DetailPageViewModel: ObservableObject { @Published var selectedRecipe = RecipeDetail(...) @Published var show = false @Published var HomepageList = [RecipeDetail]() func getData() { let db = Firestore.firestore() db.collection("Recipe").getDocuments { snapshot, error in if error == nil { print("no error") if let snapshot = snapshot { DispatchQueue.main.async { self.HomepageList = snapshot.documents.map { recipeInfo in return RecipeDetail(....) } } } } else { // 错误处理 } } } }
问题原因
@EnvironmentObject是在视图被SwiftUI渲染环境完成注入后才可用的,而视图的init方法执行时,环境注入还未完成,此时访问detailPageViewModel会找不到对应的ObservableObject实例,触发致命错误。
解决方法
方法1:通过.onAppear触发数据加载
将getData()的调用从init移到视图的.onAppear修饰器中,确保视图完成环境注入后再执行数据拉取:
struct HomePage: View { var animation: Namespace.ID @EnvironmentObject var detailPageViewModel : DetailPageViewModel var body: some View { ScrollView(.vertical, showsIndicators: true){ // 原body内容保持不变 } .onAppear { // 仅在列表为空时加载数据,避免重复拉取 if detailPageViewModel.HomepageList.isEmpty { detailPageViewModel.getData() } } } init(animation: Namespace.ID) { self.animation = animation // 移除init中的getData调用 } }
方法2:在ViewModel初始化时自动加载数据
直接在DetailPageViewModel的初始化方法中调用getData(),这样ViewModel创建时就自动拉取数据,无需在HomePage中手动调用:
class DetailPageViewModel: ObservableObject { @Published var selectedRecipe = RecipeDetail(...) @Published var show = false @Published var HomepageList = [RecipeDetail]() init() { getData() // 初始化时自动执行数据拉取 } func getData() { // 原getData逻辑保持不变 } }
额外优化建议
- 拆分ViewModel职责:当前
DetailPageViewModel同时负责首页数据加载和详情页状态管理,后续可拆分为HomeViewModel和DetailViewModel,让逻辑更清晰。 - 添加加载状态管理:在ViewModel中新增
@Published var isLoading = false,在getData开始和结束时更新状态,页面上可显示加载指示器提升用户体验。
内容的提问来源于stack exchange,提问作者GeekNomore

