skip to Main Content

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.

2

Answers


  1. 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.

    Login or Signup to reply.
  2. 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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search