Home > Software engineering >  How can I prevent a View model from being recreated on multiple views
How can I prevent a View model from being recreated on multiple views

Time:06-06

DISCLAIMER: I'm a newbie in swift

I'm trying to set an MVVM app in such a way that multiple screens can access a single View Model but for some reason, everytime I navigate away from the home page, the ViewModel get re-created. The ViewModel is set up this way:

extension ContentView {
//view model
class MyViewModel: ObservableObject {
  let sdk: mySdk

@Published  var allProducts = [ProductItem]()
@Published var itemsArray = [Item]() //This gets updated with content later on
  ...
  init(sdk: mySdk) {
    self.sdk = sdk
    self.loadProds(forceReload: false)

...
func loadProds(forceReload: Bool){
    
    sdk.getProducts(forceReload: forceReload) { products, error in
        if let products = products {
            
            self.allProducts = products
            
        } else {
            self.products = .error (error?.localizedDescription ?? "error")
            print(error?.localizedDescription)
        }
        
    }

...

//itemsArray gets values appended to it as follows:
itemsArray.append(Item(productUid: key, quantity: Int32(value)))
        
    }

        } 
    }
}

The rest of the code is set up like:

struct ContentView: View { // Home Screen content
@ObservedObject var viewmodel: MyViewModel

 
var body: some View {

...
   }
}

The SecondView that should get updated based on the state of the itemsArray is set up like so:

 struct SecondView: View {
       
    @ObservedObject var viewModel: ContentView.MyViewModel //I have also tried using @StateObject

    init(sdk: mySdk) {
        _viewModel = ObservedObject(wrappedValue: ContentView.MyViewModel(sdk: sdk))
      }
      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")

                  ...
               }
           }
      }
  }
}

The Main View that holds the custom TabView and handles Navigation is set up like this:

struct MainView: View {
    let sdk = mySdk(dbFactory: DbFactory())
    
    @State private var selectedIndex = 0
    
    let icons = [
        "house",
        "cart.fill",
        "list.dash"
    ]
    
    var body: some View{
        VStack {
            //Content
            ZStack {
                switch selectedIndex {
                case 0:
                    NavigationView {
                        ContentView(viewmodel: .init(sdk: sdk))
                            .navigationBarTitle("Home")
                    }
                case 1:
                    NavigationView {
                        SecondView(sdk:  sdk)
                            .navigationBarTitle("Cart")
                    }       

           ...
           ... 
            }
          }
         }
        }
     }

Everytime I navigate away from the ContentView screen, any updated content of the viewmodel gets reset. For example, on navigating the SecondView screen itemsArray.count shows 0 but allProducts Array shows the correct value as it was preloaded. The entire content of ContentView gets recreated on navigating back as well.

I would love to have the data in the ViewModel persist on multiple views unless explicitly asked to refresh. How can I go about doing that please? I can't seem to figure out where I'm doing something wrong. Any help will be appreciated.

CodePudding user response:

Your call to ContentView calls .init on your view model, so every time SwiftUI's rendering system needs to redraw itself, you'll get a new instance of the view model created. Similarly, the init() method on SecondView also calls the init method, in its ContentView.MyViewModel(sdk: sdk) form.

A better approach would be to create a single instance further up the hierarchy, and store it as a @StateObject so that SwiftUI knows to respond to changes to its published properties. Using @StateObject once also shows which view "owns" the object; that instance will stick around for as long as that view is in the hierarchy.

In your case, I'd create your view model in MainView – which probably means the view model definition shouldn't be namespaced within ContentView. Assuming you change the namespacing, you'd have something like

struct MainView: View {
  @StateObject private var viewModel: ViewModel

  init() {
    let sdk = mySdk(dbFactory: DbFactory())
    let viewModel = ViewModel(sdk: sdk)
    _viewModel = StateObject(wrappedValue: viewModel)
  }

  var body: some View{
    VStack {
      //Content
      ZStack {
        switch selectedIndex {
        case 0:
          NavigationView {
            ContentView(viewModel: viewModel)
              .navigationBarTitle("Home")
          }
        case 1:
          NavigationView {
            SecondView(viewModel: viewModel)
              .navigationBarTitle("Cart")
          }       
          ...
          ... 
        }
      }
    }
  }
}

struct ContentView: View {
  @ObservedObject var viewModel: ViewModel
  
  var body: some View {
    // etc
  }
}

struct SecondView: View {
  @ObservedObject var viewModel: ViewModel
  
  var body: some View {
    // etc
  }
}

One of the key things is that ObservedObject is designed to watch for changes on an object that a view itself doesn't own, so you should never be creating objects and assigning them directly to an @ObservedObject property. Instead they should receive references to objects owned by a view higher up, such as those that have been declared with a @StateObject.

CodePudding user response:

First of all, let sdk = mySdk(dbFactory: DbFactory()) should be @StateObject var sdk = mySdk(dbFactory: DbFactory()).

To continue, SecondView & ContentView should have the same ViewModel, hence they should be like this:

ContentView(viewmodel: sdk)
SecondView(sdk: sdk)

Also use @StateObject instead of @ObservedObject

  • Related