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()
}
}
}
}