简体   繁体   中英

Updating EnvironmentObject from within the View Model

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?

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

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
    }

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

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, eg

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM