Home > Back-end >  Updating EnvironmentObject from within the View Model
Updating EnvironmentObject from within the View Model

Time:07-18

In SwiftUI, I want to pass an environment object to a view model so I can change/update it. The EnvironmentObject is a simple AppState which consists of a single property counter.

class AppState: ObservableObject {
    @Published var counter: Int = 0 
}

The view model "CounterViewModel" updates the environment object as shown below:

class CounterViewModel: ObservableObject {
    
    var appState: AppState
    
    init(appState: AppState) {
        self.appState = appState
    }
    
    var counter: Int {
        appState.counter 
    }
    
    func increment() {
        appState.counter  = 1
    }
    
}

The ContentView displays the value:

struct ContentView: View {
    
    @ObservedObject var counterVM: CounterViewModel
    
    init(counterVM: CounterViewModel) {
        self.counterVM = counterVM
    }
    
    var body: some View {
        VStack {
            Text("\(counterVM.counter)")
            Button("Increment") {
                counterVM.increment()
            }
        }
        
    }
}

I am also injecting the state as shown below:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationStack {
            
            let appState = AppState()
            
            ContentView(counterVM: CounterViewModel(appState: appState))
                .environmentObject(appState)
        }
    }
}

The problem is that when I click the increment button, the counterVM.counter never returns the updated value. What am I missing?

CodePudding user response:

Your class CounterViewModel is an ObservableObject, but it has no @Published properties – so no changes will be published automatically to the views.

But you can manually publish changes by using objectWillChange.send():

    func increment() {
        objectWillChange.send()
        appState.counter  = 1
    }

CodePudding user response:

I'm not sure why both the CounterViewModel and the AppState need to be observable objects, since you are using a view model to format the content of your models. I would consider AppState to be a model and I could therefore define it as a struct. The CounterViewModel will then be the ObservableObject and it published the AppState. In this way your code is clean and works.

Code for AppState:

import Foundation

struct AppState {
    var counter: Int = 0
}

Code for CounterViewModel:

import SwiftUI

class CounterViewModel: ObservableObject {
    
    @Published var appState: AppState
    
    init(appState: AppState) {
        self.appState = appState
    }
    
    var counter: Int {
        appState.counter
    }
    
    func increment() {
        appState.counter  = 1
    }
}

Code for the ContentView: import SwiftUI

struct ContentView: View {

@StateObject var counterVM = CounterViewModel(appState: AppState())

var body: some View {
    VStack {
        Text("\(counterVM.counter)")
        Button("Increment") {
            counterVM.increment()
        }
    }
}

}

Do remind, that in the View where you first define an ObservableObject, you define it with @StateObject. In all the views that will also use that object, you use @ObservedObject.

This code will work.

Kind regards, MacUserT

CodePudding user response:

Did you check your xxxxApp.swift (used to be the AppDelegate) file ? Sometimes Xcode would do it for you automatically, sometimes won't you have to add it manually and add your environment object. * It has to be the view that contains all the view you want to share the object to.

var body: some Scene {
    WindowGroup {
        VStack {
            ContentView()
                .environmentObject(YourViewModel())
        }
    }
}

CodePudding user response:

We actually don't use view model objects in SwiftUI for view data. We use an @State struct and if we need to mutate it in a subview we pass in a binding, e.g.

struct Counter {
    var counter: Int = 0
    
    mutating func increment() {
       counter  = 1
    }
}

struct ContentView: View {
    @State var counter = Counter()
    
    var body: some View {
        ContentView2(counter: $counter)
    }
}

struct ContentView2: View {
    @Binding var counter: Counter // if we don't need to mutate it then just use let and body will still be called when the value changes.

    var body: some View {
        VStack {
            Text(counter.counter, format: .number) // the formatting must be done in body so that SwiftUI will update the label automatically if the region settings change.
            Button("Increment") {
                counter.increment()
            }
        }
    }
}
  • Related