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

