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