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

SwiftUI MVVM中如何避免ViewModel在多视图切换时被重复创建

问题根因

数据在页面切换后重置,核心原因是所有页面从未共享过同一个ViewModel实例,每个页面都在独立创建、持有自己的ViewModel对象,具体错误点如下:

  • MainView的Tab切换逻辑中,每次切到首页都会执行ContentView(viewmodel: .init(sdk: sdk)),重新生成一个全新的ViewModel实例,旧实例随视图移除被直接释放
  • SecondView的初始化方法中,自行调用ContentView.MyViewModel(sdk: sdk)创建了第二个独立的ViewModel实例,和首页持有的实例内存地址完全不同,数据自然不互通
  • 属性包装器使用场景错误:@ObservedObject不会持有传入的对象实例,仅负责订阅更新;只有@StateObject会主动持有对象,将对象生命周期与当前视图绑定。你在子视图中临时创建实例再传给属性包装器,会随着视图重建反复生成新对象。

补充:你观察到allProducts数据正常,是因为每次新建ViewModel都会调用loadProds(forceReload: false),如果sdk层做了本地缓存,新实例能直接拉到缓存的商品列表;但itemsArray是存储在ViewModel内存中的临时数据,新实例初始化时为空数组,所以切换页面后计数为0。

修复方案

核心原则:在生命周期最长、不会随Tab切换销毁的顶层父视图(即MainView)创建唯一的ViewModel实例,所有子视图仅接收传入的共享实例,绝不自行初始化ViewModel。

步骤1:在MainView中创建全局唯一的ViewModel实例

用@StateObject在MainView中持有ViewModel,保证整个App运行过程中只要MainView不销毁,ViewModel实例就不会被释放重建:

struct MainView: View {
    private let sdk: mySdk
    // 顶层持有唯一共享ViewModel实例,生命周期与MainView绑定
    @StateObject private var sharedViewModel: ContentView.MyViewModel
    
    @State private var selectedIndex = 0
    
    let icons = [
        "house",
        "cart.fill",
        "list.dash"
    ]
    
    init() {
        // 仅初始化一次sdk与ViewModel
        let sdkInstance = mySdk(dbFactory: DbFactory())
        self.sdk = sdkInstance
        _sharedViewModel = StateObject(wrappedValue: ContentView.MyViewModel(sdk: sdkInstance))
    }
    
    var body: some View{
        VStack {
            ZStack {
                switch selectedIndex {
                case 0:
                    NavigationView {
                        // 传入已创建的共享实例,禁止在此处调用.init新建
                        ContentView(viewmodel: sharedViewModel)
                            .navigationBarTitle("Home")
                    }
                case 1:
                    NavigationView {
                        // 传入同一个共享实例
                        SecondView(viewModel: sharedViewModel)
                            .navigationBarTitle("Cart")
                    }
                default:
                    EmptyView()
                }
            }
            // 原有自定义TabBar逻辑
            // ...
        }
    }
}

步骤2:修改SecondView,删除自行创建ViewModel的逻辑

SecondView只负责接收外部传入的共享实例,不要在自身init中新建ViewModel:

struct SecondView: View {
    // 仅接收传入的实例,用@ObservedObject订阅更新即可
    @ObservedObject var viewModel: ContentView.MyViewModel
    
    // 删除原有自行初始化ViewModel的自定义init,使用默认构造器接收参数
    var body: some View {
        ScrollView {
            LazyVStack {
                Text("Items array count is \(viewModel.itemsArray.count)")
                Text("All prods array count is \(viewModel.allProducts.count)")
                                   
                if viewModel.itemsArray.isEmpty{
                    Text ("Items array is empty")
                } else {
                    Text ("Items array is not empty")
                    // 原有业务逻辑
                    // ...
                }
            }
        }
    }
}

步骤3:ContentView无需大改

原有@ObservedObject的声明方式符合子视图接收实例的规则,只要保证传入的是顶层创建的共享实例即可:

struct ContentView: View {
    @ObservedObject var viewmodel: MyViewModel
    
    var body: some View {
        // 原有首页业务逻辑
        // ...
    }
}
优化建议(可选)

如果你的页面层级较深,不想一层一层传递ViewModel实例,可以在MainView中用.environmentObject(sharedViewModel)把实例注入视图环境:

// 在MainView的body最外层加修饰符
VStack {
    // 原有Tab切换逻辑
}
.environmentObject(sharedViewModel)

之后任意层级的子视图都可以直接通过@EnvironmentObject var viewModel: ContentView.MyViewModel拿到共享实例,不需要手动传参。

属性包装器使用规则记两点即可:

  • 哪个视图创建ViewModel实例,哪个视图就用@StateObject持有
  • 所有接收实例传参的子视图,一律用@ObservedObject(从环境取值的话用@EnvironmentObject)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.31 12:54:18