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
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, theinit()
method onSecondView
also calls the init method, in itsContentView.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 withinContentView
. Assuming you change the namespacing, you’d have something likeOne 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
.First of all,
let sdk = mySdk(dbFactory: DbFactory())
should be@StateObject var sdk = mySdk(dbFactory: DbFactory())
.To continue,
SecondView
&ContentView
should have the sameViewModel
, hence they should be like this:Also use
@StateObject
instead of@ObservedObject