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

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

额外优化建议
  1. 访问数组元素前先判空,避免索引越界:
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
    })
}
  1. 用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 11:37:32