SwiftUI切换视图后,ViewModel中@Published数组为何被清空?
问题:CategoryViewModel中myAllCategories数据丢失的原因
我在CategoryViewModel里定义了@Published var myAllCategories: [Category] = [],加载第一个视图BrowseByCategoryView时,调用API获取所有分类,再调用loadMyMajorCategories从myAllCategories筛选一级分类存入myMajorCategories,此时打印显示myAllCategories有数据。但切换到子分类视图BrowseBySubView后,调用loadMySubCategories想读取myAllCategories筛选二级分类时,数组为空,访问元素触发索引越界错误。
相关代码
CategoryViewModel代码
import Foundation import SwiftUI @MainActor class CategoryViewModel: ObservableObject { @Published var path = NavigationPath() @Published var myAllCategories: [Category] = [] @Published var myMajorCategories: [Category] = [] @Published var mySubCategories: [Category] = [] // Alert vars @Published var alertTitle = "" @Published var alertMessage = "" @Published var showApiAlert = false // MARK: - Categories func loadCategories() async { Service.sharedInstance.getCategoriesApi { categories in self.myAllCategories = categories.subcategories self.loadMyMajorCategories() } failure: { alertType, errorText in } } func loadMyMajorCategories() { for major in myAllCategories { if major.categoryLevel == 1 { myMajorCategories.append(major) } } print("🪵 loadMajorCategories allCategories first element: \(myAllCategories[0].majorCategory)-\(myAllCategories[0].description)") } func loadMySubCategories(majorCategory: Category) { print("🪵 filteredSubCategories() called") print("🪵 filteredSubCategories allCategories first element: \(myAllCategories[0].majorCategory)-\(myAllCategories[0].description)") self.mySubCategories = myAllCategories.filter({ $0.categoryLevel == 2 }) } }
一级分类视图BrowseByCategoryView代码
import SwiftUI struct BrowseByCategoryView: View { var backPressed: ()->() @State private var categoryName = "" @StateObject private var vm = CategoryViewModel() var body: some View { NavigationStack(path: $vm.path){ VStack { MyNavigationBar( title: "Major Category", backgroundColor: Color("browseByCatColor"), navHeight: 48, backText: "More", backPressed: { print("🪵 BrowseByCategoryView --- backPressed() line \(#line)") backPressed() }, showAddButton: false, addPressed: {} ) CustomerInfoBar(backgroundColor: Color("browseByCatAccent")) List(vm.myMajorCategories, id: \.self) { category in NavigationLink(destination: { BrowseBySubView(majorCat: category) }, label: { GeometryReader { geometry in Text(category.description) .frame( width: geometry.size.width * 0.90, height: geometry.size.height, alignment: .leading ) .onTapGesture(perform: { // Navigate to a list of items in this category print("🪵 Will display items in \(category.description) category") categoryName = category.description vm.path.append("BrowseItems") }) .listRowBackground(Color(.tertiarySystemBackground)) } }) } .navigationDestination(for: String.self) { view in if view == "BrowseItems" { BrowseItemsView( categoryName: categoryName, backPressed: { backPressed() }) } } .listStyle(.plain) .ignoresSafeArea() Spacer() } } .task { // Use .task to run an asynchronous task before the view appears await vm.loadCategories() } //MARK: API Alerts // This presents API errors for the entire Returns module, everything called under ReturnsListView .alert(vm.alertTitle, isPresented: $vm.showApiAlert) { Button("OK") {} } message: { Text(vm.alertMessage) } } }
子分类视图BrowseBySubView代码
import SwiftUI struct BrowseBySubView: View { var majorCat: Category @Environment(\.dismiss) var dismiss @State private var categoryName = "" @StateObject private var vm = CategoryViewModel() var body: some View { NavigationStack(path: $vm.path) { VStack { MyNavigationBar( title: "Sub Category", backgroundColor: Color("browseByCatColor"), navHeight: 48, backText: "Back", backPressed: { print("🪵 BrowseBySubView --- backPressed() line \(#line)") dismiss() }, showAddButton: false, addPressed: {} ) CustomerInfoBar(backgroundColor: Color("browseByCatAccent")) Text("Test Text") Text("\(majorCat.majorCategory) - \(majorCat.description)") List(vm.mySubCategories, id: \.self) { subCat in NavigationLink(destination: { BrowseByMinorView(subCat: subCat) }, label: { GeometryReader { geometry in Text(subCat.description) .frame( width: geometry.size.width * 0.90, height: geometry.size.height, alignment: .leading ) .onTapGesture(perform: { // Navigate to a list of items in this category print("🪵 Will display items in \(majorCat.description) category") categoryName = subCat.description vm.path.append("BrowseItems") }) .listRowBackground(Color(.tertiarySystemBackground)) } }) } .navigationBarBackButtonHidden(true) .navigationDestination(for: String.self) { view in if view == "BrowseItems" { BrowseItemsView( categoryName: categoryName, backPressed: { dismiss() } ) } } .listStyle(.plain) .ignoresSafeArea() Spacer() } } .onAppear { // Use .onAppear to perform an action before this view appears print("🪵 BrowseBySub.onAppear majorCatDescription: \(majorCat.description)") print("🪵 BrowseBySub.onAppear majorCategory: \(majorCat.majorCategory)") vm.loadMySubCategories(majorCategory: majorCat) } //MARK: API Alerts .alert(vm.alertTitle, isPresented: $vm.showApiAlert) { Button("OK") {} } message: { Text(vm.alertMessage) } } }
问题根源
你在BrowseByCategoryView和BrowseBySubView中都用@StateObject private var vm = CategoryViewModel()创建了独立的ViewModel实例。也就是说,两个视图持有完全不同的CategoryViewModel对象:
BrowseByCategoryView里的vm调用API加载了myAllCategories数据BrowseBySubView里的vm是全新初始化的,myAllCategories自然是空数组,所以调用loadMySubCategories时会触发索引越界
解决方案
方案1:父视图传递ViewModel给子视图
修改BrowseBySubView,不再创建新的ViewModel实例,而是接收父视图的vm:
// 修改BrowseBySubView的定义 struct BrowseBySubView: View { var majorCat: Category @Environment(\.dismiss) var dismiss @State private var categoryName = "" // 接收父视图传递的ViewModel,用@ObservedObject监听变化 @ObservedObject var vm: CategoryViewModel // ... 其余代码保持不变 }
在BrowseByCategoryView的NavigationLink中传递vm:
NavigationLink(destination: { BrowseBySubView(majorCat: category, vm: vm) }, label: { // ... 原有代码 })
方案2:使用单例模式(全局共享)
修改CategoryViewModel为单例,确保整个App中只有一个实例:
@MainActor class CategoryViewModel: ObservableObject { // 全局共享的单例实例 static let shared = CategoryViewModel() // 私有化初始化方法,禁止外部创建新实例 private init() {} // ... 其余属性和方法保持不变 }
在两个视图中使用单例:
// BrowseByCategoryView中 @StateObject private var vm = CategoryViewModel.shared // BrowseBySubView中 @StateObject private var vm = CategoryViewModel.shared
额外优化建议
- 访问数组元素前先判空,避免索引越界:
func loadMySubCategories(majorCategory: Category) { print("🪵 filteredSubCategories() called") guard !myAllCategories.isEmpty else { print("⚠️ myAllCategories为空,无法筛选子分类") mySubCategories = [] return } print("🪵 filteredSubCategories allCategories first element: \(myAllCategories[0].majorCategory)-\(myAllCategories[0].description)") // 可根据传入的majorCat筛选对应二级分类,而非所有二级分类 self.mySubCategories = myAllCategories.filter({ $0.categoryLevel == 2 && $0.majorCategory == majorCategory.majorCategory }) }
- 用filter简化
loadMyMajorCategories代码:
func loadMyMajorCategories() { myMajorCategories = myAllCategories.filter { $0.categoryLevel == 1 } guard !myAllCategories.isEmpty else { return } print("🪵 loadMajorCategories allCategories first element: \(myAllCategories[0].majorCategory)-\(myAllCategories[0].description)") }
内容的提问来源于stack exchange,提问作者jonathan3087
相关产品推荐
相关产品推荐

