Swiftui - How to Pass Environmentobject into View Model

How do I pass in an Environment Object to my ViewModel SwiftUI

EnvironmentObjects must be supplied by anscestor views!



 import SwiftUI

@main
struct pro2App: App { // <<: Here: your app name!
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(MealViewModel()) // <<: Here!
.environmentObject(UserInfoModel.shared) // <<: Here!
}
}

}


    class MealViewModel: ObservableObject {

@Published var nutrients: [RecipieAPI] = []

let person: UserInfoModel.shared // <<: Here


@State public var fat = 0
@State public var carbs = 0
@State public var protein = 0



init() {

fetchNutrients()


}

func fetchNutrients() {
NetworkServices.fetchNutrients(maxProtein: self.person.recipeNutrientsSearch.protein , maxFat: self.person.recipeNutrientsSearch.fat, maxCarbs: self.person.recipeNutrientsSearch.carb, number: 4) { (nutrients, error) in
if let error = error {
print(error)
} else {
if let nutrientList = nutrients as? [RecipieAPI] {
self.nutrients = nutrientList
}
}
}
}
}


class UserInfoModel: ObservableObject {

static let shared: UserInfoModel = UserInfoModel() // <<: Here

struct UserInfo: Identifiable {
var id = UUID()
var firstName: String
var height: Double
var weight: Double
var gender: String
var age: Double
var activityLevel: String
var BMR: Double

}

struct AddedFoods:Identifiable{
var name: String = ""
var totalCals: Double = 0
var totalProtein: Double = 0
var totalCarbs: Double = 0
var totalFat: Double = 0
var id = UUID().uuidString
//Your other properties
}

struct DailyCalorieGoals: Identifiable{
var id = UUID()
var calorieGoal: Double
var fatGoal: Double
var proteinGoal: Double
var carbGoal: Double

}

struct CurrentCalorieProgress: Identifiable{
var id = UUID()
var calorieProgress: Double
var fatProgress: Double
var carbProgress: Double
var proteinProgress: Double

}

struct SearchRecipeCalories: Identifiable{
var id = UUID()
var fat: Int
var carb: Int
var protein: Int

}

@Published var personUserInfo = UserInfo.init(firstName: "", height: 0, weight: 0, gender: "", age: 0, activityLevel: "", BMR: 0)
@Published var personDailyCalorieGoals = DailyCalorieGoals.init(calorieGoal: 2400, fatGoal: 40, proteinGoal: 40, carbGoal: 40)
@Published var personCurrentCalorieProgress = CurrentCalorieProgress.init(calorieProgress: 1200, fatProgress: 12, carbProgress: 5, proteinProgress: 30)

@Published var recipeNutrientsSearch = SearchRecipeCalories.init(fat: 0, carb: 0, protein: 0)


}

How to inject a Model from the Environment into a ViewModel in SwiftUI

There are a few different issues here and multiple strategies to handle them.

From the top, yes, you can create your data model at the App level:

@main
struct DIApp: App {

var dataModel = DataModel()

var body: some Scene {
WindowGroup {
ListView(dataModel: dataModel)
.environmentObject(dataModel)
}
}
}

Notice that I've passed dataModel explicitly to ListView and as an environmentObject. This is because if you want to use it in init, it has to be passed explicitly. But, perhaps subviews will want a reference to it as well, so environmentObject will get it sent down the hierarchy automatically.

The next issue is that your ListView won't update because you have nested ObservableObjects. If you change the child object (DataModel in this case), the parent doesn't know to update the view unless you explicitly call objectWillChange.send().

struct ListView: View {
@StateObject private var vm: ViewModel

init(dataModel: DataModel) {
_vm = StateObject(wrappedValue: ViewModel(dataModel: dataModel))
}

var body: some View {
NavigationView {
List {
ForEach(vm.dataModel.items) { item in
Text(item.title)
}
}
.navigationTitle("List")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
Button(action: {
addItem()
}) {
Image(systemName: "plus.circle")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())

}

func addItem() {
vm.addRandomItem()
}
}

extension ListView {
class ViewModel: ObservableObject {
let dataModel: DataModel

init(dataModel: DataModel) {
self.dataModel = dataModel
}

func addRandomItem() {
let newID = Int.random(in: 100..<999)
let newItem = Item(id: newID, title: "New Item \(newID)")

dataModel.addItem(newItem)
self.objectWillChange.send()
}
}
}

An alternate approach would be including DataModel on your ListView as an @ObservedObject. That way, when it changes, the view will update, even if ViewModel doesn't have any @Published properties:


struct ListView: View {
@StateObject private var vm: ViewModel
@ObservedObject private var dataModel: DataModel

init(dataModel: DataModel) {
_dataModel = ObservedObject(wrappedValue: dataModel)
_vm = StateObject(wrappedValue: ViewModel(dataModel: dataModel))
}

var body: some View {
NavigationView {
List {
ForEach(vm.dataModel.items) { item in
Text(item.title)
}
}
.navigationTitle("List")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
Button(action: {
addItem()
}) {
Image(systemName: "plus.circle")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())

}

func addItem() {
vm.addRandomItem()
}
}

extension ListView {
class ViewModel: ObservableObject {
let dataModel: DataModel

init(dataModel: DataModel) {
self.dataModel = dataModel
}

func addRandomItem() {
let newID = Int.random(in: 100..<999)
let newItem = Item(id: newID, title: "New Item \(newID)")

dataModel.addItem(newItem)
}
}
}

Yet another object would be using Combine to automatically send objectWilLChange updates when items is updated:

struct ListView: View {
@StateObject private var vm: ViewModel

init(dataModel: DataModel) {
_vm = StateObject(wrappedValue: ViewModel(dataModel: dataModel))
}

var body: some View {
NavigationView {
List {
ForEach(vm.dataModel.items) { item in
Text(item.title)
}
}
.navigationTitle("List")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
Button(action: {
addItem()
}) {
Image(systemName: "plus.circle")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())

}

func addItem() {
vm.addRandomItem()
}
}

import Combine

extension ListView {
class ViewModel: ObservableObject {
let dataModel: DataModel

private var cancellable : AnyCancellable?

init(dataModel: DataModel) {
self.dataModel = dataModel
cancellable = dataModel.$items.sink { [weak self] _ in
self?.objectWillChange.send()
}
}

func addRandomItem() {
let newID = Int.random(in: 100..<999)
let newItem = Item(id: newID, title: "New Item \(newID)")

dataModel.addItem(newItem)
}
}
}

As you can see, there are a few options (these, and others). You can pick the design pattern that works best for you.

Access EnvironmentObject from ViewModel in SwiftUI

Since your token is somewhat dynamic, I would suggest that you shouldn't pass it directly to your view models. Rather, pass the AppState object and retrieve the token when needed.

If you detect an expired token you can call a function on the AppState that obtains a refresh token and updates its token property.

SwiftUI : I get lost when I try to pass EnvironmentObject to another View when using MVVM

You have to inject your dataModel from the root of you app, that’s from Main root struct.

import SwiftUI

struct Payer: Identifiable {
var id = UUID().uuidString
var name : String = "ABCD"

var offset : CGFloat = 0
}

class BaseViewModel: ObservableObject {

//* The problem * //
@Published var payer: Payer
//* The problem * //

@Published var payers = [
Payer( name: "AA"),
Payer( name: "BaB"),
Payer( name: "CC"),
]

init(payer: Payer) {

self.payer = payer
}
}

struct ContentView: View {
@EnvironmentObject var vm : BaseViewModel
var body: some View {
Text(vm.payer.name)
}
}

@Main-:

import SwiftUI

@main
struct WaveViewApp: App {

@StateObject var dataController:BaseViewModel

init() {
let payer = Payer()
let model = BaseViewModel(payer:payer)
_dataController = StateObject(wrappedValue: model)
}

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


Related Topics



Leave a reply



Submit