Learn Courses My Dashboard

EnvironmentObject not updating view on change SwiftUI

Hi all,

I am having a problem when using a function to change data in an EnvironmentObject, this does not immediately update the view code, however the change does appear to have been made successfully as when the view is reloaded the change appears as expected.

After playing around I have noticed that when the ‘@Published’ modifier is on the var in the Model rather than on the new instance of the Class in the ViewModel the view updates immediately as expected. However, I am unable to put the ‘@Published’ modifier on the var’s in the Model as these need to conform the ‘Decodable’ protocol.

The view is a detail page at the bottom of a sequence of a navigation link lists. This view then also has other navigation links to be able to edit some of the details (change the variety and quantity of components etc). All of the functions are called from the ViewModel apart from the ‘Delete’ function which I have lazily left in the view for now, but the behaviour is the same (i.e needing to reload the view in order to see the effect of the function).

I hope that makes sense, code below. Please let me know if any more detail is required.

Thank you!

Model

import Foundation

class FoodandDrink: Identifiable, Decodable {

var id:UUID?

var catagory:String

var items:[Item]

}

class Item: Identifiable, Decodable {

var id:UUID?

var name: String

var totalCalories: Int

var isFavourite: Bool

var addedToday: Int

var components:[Component]

}

class Component: Identifiable, Decodable {

var id:UUID?

var name: String = “name”

var varietyName: String = “varirtyName”

var varietyCalories: Int = 0

var totalCalories: Int = 0

var quantity: Int = 0

var unit: String = “unit”

}

class Variety: Identifiable, Decodable {

var id:UUID?

var itemName: String

var varieties:[Varieties]

}

class Varieties: Identifiable, Decodable {

var id:UUID?

var name: String

var calories: Int

}

class DailyTotal: Identifiable, Decodable {

var id:UUID?

var todayTotal:Int = 0

var todayDate:Date?

var componentsAdded:[ComponentsAdded]

}

class ComponentsAdded: Identifiable, Decodable {

var id:UUID?

var componentCategory:String = “catagory”

var componentName:String = “component name”

var componentCalories:Int = 0

}

ViewModel

import Foundation

class DailyTotalModel: ObservableObject {

@Published var dailyTotal = [DailyTotal]()
@Published var componentsAdded = [ComponentsAdded]()

init() {
    
    self.dailyTotal = DataService.getLocalData3()
}

}

class VarietyModel: ObservableObject {

@Published var variety = [Variety]()
@Published var varieties = [Varieties]()

init() {
    
    self.variety = DataService.getLocalData2()
}

}
class FoodandDrinkModel: ObservableObject {

@Published var foodandDrink = [FoodandDrink]()
@Published var item = [Item]()
@Published var component = [Component]()


init() {
    
    self.foodandDrink = DataService.getLocalData()
}

static func calculatedCalories(component:Component, computedQuantity:Int) -> Int {
    
    var computedTotal:Int = 0

// var computedQuantity:Int = component.quantity ?? 0
var computedCalories:Int = component.varietyCalories

    computedTotal = ((computedQuantity+1)*10) * computedCalories
    
    component.totalCalories = computedTotal
    
    return component.totalCalories
}

static func totalCal (item:Item) -> Int {
    var tryTotalCal: Int
    
    tryTotalCal =  item.components.reduce(0) { cals, currentItem in
            cals + currentItem.totalCalories
            
        }
    item.totalCalories = tryTotalCal
    return item.totalCalories
    }
    
static func getVarities (component:Component, varietyModel:VarietyModel) -> [Varieties] {
    
    var variety = varietyModel.variety
    var myVariety:[Varieties] = []
    
    for variety in variety {
        if variety.itemName == component.name
        {
    
         myVariety = variety.varieties
    }
    }
    
    print (myVariety)
    
    return myVariety
}
    
static func newComponent (item:Item, newVariety:Variety) -> [Component] {
    
    // get item.component
    var componentList = item.components
    

    var newComponent:Component = Component()
    // get selected component
    newComponent.id = UUID()
    newComponent.name = newVariety.itemName
    newComponent.varietyName = newVariety.varieties[0].name
    newComponent.varietyCalories = newVariety.varieties[0].calories
    newComponent.totalCalories = 0
    newComponent.quantity = 0
    newComponent.unit = "g"
    
    // appened selected component to item.component
    componentList.append(newComponent)

//
item.components = componentList
// go to new instance of ComponentListView

    print (componentList)
    return componentList
}

static func deleteComponent(at offsets: IndexSet, item:Item) {
    item.components.remove(atOffsets: offsets)
    }
    

func addToTotal (foodanddrink:FoodandDrink, item:Item, dailyTotal:DailyTotal) -> Int {
 
    //create addedComponent array
    var newComponentsAdded:ComponentsAdded = ComponentsAdded()
    //Assighn selcted component to newComponentAdded
    newComponentsAdded.id = UUID()
    newComponentsAdded.componentName = item.name
    newComponentsAdded.componentCategory = foodanddrink.catagory
    newComponentsAdded.componentCalories = item.totalCalories
    
    //append to today.addedcomponents
    var newComponentsAddedList = dailyTotal.componentsAdded
    
    newComponentsAddedList.append(newComponentsAdded)
    
    
    dailyTotal.componentsAdded = newComponentsAddedList
    
    print (dailyTotal.componentsAdded)
    
    //calculate daily total and assign date
    var calculatedDailyTotal = dailyTotal
    
    calculatedDailyTotal.todayTotal = dailyTotal.componentsAdded.reduce(0) {cals, currentItem in
        cals + currentItem.componentCalories}
    
    calculatedDailyTotal.todayDate = Date()
    
    dailyTotal.todayDate = calculatedDailyTotal.todayDate
    
    dailyTotal.todayTotal = calculatedDailyTotal.todayTotal
    
    return dailyTotal.todayTotal
    
}

static func minusFromTotal (item:Item, dailyTotal:DailyTotal) -> [ComponentsAdded] {
    

   
    var myItemsAdded = dailyTotal.componentsAdded
    
    if let i = myItemsAdded.firstIndex(where: { $0.componentName == item.name}) {myItemsAdded.remove(at: i)}
    
    dailyTotal.componentsAdded = myItemsAdded
    print (dailyTotal.componentsAdded)
    
    //calculate daily total and assign date
    var calculatedDailyTotal = dailyTotal
    
    calculatedDailyTotal.todayTotal = dailyTotal.componentsAdded.reduce(0) {cals, currentItem in
        cals + currentItem.componentCalories}
    
    calculatedDailyTotal.todayDate = Date()
    
    dailyTotal.todayDate = calculatedDailyTotal.todayDate
    
    dailyTotal.todayTotal = calculatedDailyTotal.todayTotal
    
    
    
    
    return dailyTotal.componentsAdded
    
 
}

static func countAdded (item:Item, dailyTotal:DailyTotal) -> Int {

    let myItemsAdded = dailyTotal.componentsAdded
    
    let myCount = myItemsAdded.filter { $0.componentName == item.name }.count
    
print (myCount)
    return myCount

}
}

View

import SwiftUI

struct ComponentListView: View {
@EnvironmentObject var model:FoodandDrinkModel
@EnvironmentObject var model2:VarietyModel
@EnvironmentObject var model3:DailyTotalModel

@State var item:Item
@State var foodandDrink:FoodandDrink



var body: some View {
   
    Spacer()
  
        VStack(alignment: .leading){
            
            HStack {
                Spacer()
            Text("Calories Today")
                ForEach (model3.dailyTotal) { r in
                    Text(String(r.todayTotal))}
                Spacer()
            }
            
        HStack {
            VStack(alignment: .leading) {
                Text("Name")
        Text(item.name)
            }
            Spacer()
            
            VStack(alignment: .leading) {
                Text("Calories")
                Text(String(FoodandDrinkModel.totalCal(item: item)))
                
            }
            Spacer()
            VStack(alignment: .leading) {
                Text("Added Today")
                HStack {
                    ForEach (model3.dailyTotal) { r in
                        Button(action: {model.addToTotal(foodanddrink: foodandDrink, item: item, dailyTotal: r)}) {
                        Image(systemName: "plus.circle")
                    }
                    Text(String(FoodandDrinkModel.countAdded(item: item, dailyTotal: r)))
                        Button(action: {FoodandDrinkModel.minusFromTotal(item: item, dailyTotal: r)}) {
                        Image(systemName: "minus.circle")
                    }
                    }
                }
            }
            
        }
        }
         

            VStack(alignment: .leading){
            HStack {
                Text("Components:")
            }
            .padding(.vertical)
                Form {
            ForEach (item.components) { r in

                HStack{
                    Text(r.name)
                    Text(String(r.totalCalories))
                    Spacer()
                    NavigationLink (  destination: VarietyListView(component:r),
                                      label: {Text("Edit")})
                }

            }
            .onDelete(perform: delete)
            
                    
                    
                   
                    
                }
                
                VStack {
                    HStack{
                        ForEach (model3.dailyTotal) { r in
                            
                            Button ("Add", action: {model.addToTotal(foodanddrink: foodandDrink, item: item, dailyTotal: r)

                            }
                            )
                        }
                        
                       
                    }
                    
                    Spacer()
                HStack (alignment: .top){
                    Spacer()
                    NavigationLink ( destination: NewComponentView(item: item),
                                     label: {Text("New Component")})
                
                    Spacer()
                }
                
                Spacer()
            }
            }
    
   
}

func delete(at offsets: IndexSet) {
    item.components.remove(atOffsets: offsets)
    }

}

Is this part the problem? Looks like you might be creating a new instance instead of updating your environment object.

Should it be this instead?..
Text(String(model.countAdded(item: item, dailyTotal: r)))
Button(action: {model.minusFromTotal(item: item, dailyTotal: r)})

Same thing in the VStack just above that code.

@Scott7975 thanks for the suggestion, I did try this previously changing the ‘addToTotal’ function to ‘model’ rather than a new instance of FoodandDrinkModel, that didn’t seem to make a difference. I have just tried amending all of the functions so that they are ‘model.’ but again it doesn’t seem to make a difference.

The ‘delete’ function which is defined within the view code is also still exhibiting the same behaviour which leads be to believe it is not the method of how the functions are being called which is causing the problem.

Simulator Screen Recording - iPhone 12 Pro - 2021-07-26 at 10.41.45

Hopefully this helps explain the problem; the delete button and add button do not change the view immediately, however when the view is reloaded the changes appear as expected.

Apologies for the very basic aesthetics! Just trying to get the functionality working before worrying about how it looks.

I’m a bit puzzled why you have item defined as a @State variable. Are you passing in the item from the previous View?

Note: I deleted my previous reply because .onDetete does in fact work with Form (surprised me when I tested that on a quickly put together project here). In any case I would suggest using List rather than Form since the layout created by using Form is a little bit restrictive when it come to customisation.

Just a suggestion for your code layout is to make use of the reindent tool available by selecting your code and pressing Control + I. Having code indented correctly can make a difference to you being able to identify issues.

@Chris_Parker Yes the item is being passed in from a previous view, there is a category list and an item list to navigate to this view. I am just using dummy data right now so the only item in the item list is Lasagna.

Do you think these layers maybe be stopping the EnvironmentObject from updating the view?

Thanks for the tips, I will re past the indented view code below.

import SwiftUI

struct ComponentListView: View {
@EnvironmentObject var model:FoodandDrinkModel
@EnvironmentObject var model2:VarietyModel
@EnvironmentObject var model3:DailyTotalModel

@State var item:Item
@State var foodandDrink:FoodandDrink



var body: some View {
    
    Spacer()
    
    VStack(alignment: .leading){
        
        HStack {
            Spacer()
            Text("Calories Today")
            ForEach (model3.dailyTotal) { r in
                Text(String(r.todayTotal))}
            Spacer()
        }
        
        HStack {
            VStack(alignment: .leading) {
                Text("Name")
                Text(item.name)
            }
            Spacer()
            
            VStack(alignment: .leading) {
                Text("Calories")
                Text(String(model.totalCal(item: item)))
                
            }
            Spacer()
            VStack(alignment: .leading) {
                Text("Added Today")
                HStack {
                    ForEach (model3.dailyTotal) { r in
                        Button(action: {model.addToTotal(foodanddrink: foodandDrink, item: item, dailyTotal: r)}) {
                            Image(systemName: "plus.circle")
                        }
                        Text(String(model.countAdded(item: item, dailyTotal: r)))
                        Button(action: {model.minusFromTotal(item: item, dailyTotal: r)}) {
                            Image(systemName: "minus.circle")
                        }
                    }
                }
            }
            
        }
    }
    
    
    VStack(alignment: .leading){
        HStack {
            Text("Components:")
        }
        .padding(.vertical)
        List {
            ForEach (item.components) { r in
                
                HStack{
                    Text(r.name)
                    Text(String(r.totalCalories))
                    Spacer()
                    NavigationLink (  destination: VarietyListView(component:r),
                                      label: {Text("Edit")})
                }
                
            }
            .onDelete(perform: delete)
                        
        }
        
        VStack {
            HStack{
                ForEach (model3.dailyTotal) { r in
                    
                    Button ("Add", action: {model.addToTotal(foodanddrink: foodandDrink, item: item, dailyTotal: r)
                        
                    }
                    )
                }
                
            }
            
            Spacer()
            HStack (alignment: .top){
                Spacer()
                NavigationLink ( destination: NewComponentView(item: item),
                                 label: {Text("New Component")})
                
                Spacer()
            }
            
            Spacer()
        }
    }
    
    
}

func delete(at offsets: IndexSet) {
    item.components.remove(atOffsets: offsets)
}

}

Is there a reason that you opted to create 3 ViewModels? Unless you are doing 3 discrete separate functions in your App and the data is not interrelated then it would be better to have only one ViewModel and therefore only one ObservableObject in which you have all of your necessary @Published properties.

At a glance you appear to be over complicating the App.

have you tried changing your environmentobject to an observedobject instead?

If the data is required in multiple views then it is better to inject the ViewModels at the parent level using

.enviromentObject(ViewModel())

otherwise what you are suggesting will work too. It just depends on what you need access to and in which View.

Then you should be using @Binding instead of @State.

@State should be used when you want to mutate one of a View's own properties; @Binding is for when you want to work with a property passed in from a parent View.

@Chris_Parker, the three models represent three different JSON files that input data in to the app, it felt like the tidiest way to do it but I guess it would be possible to combine into one file/one model.

@roosterboy, thanks for that. I have changed to @Binding but the problem still persists unfortunately.

Can you post your DataService.swift file.

Just a tip when you post your code in a reply.

Place 3 back-ticks ``` on the line above your code and 3 back-ticks ``` on the line below your code so that it is formatted nicely. The back-tick character is located on the same keyboard key as the tilde character ~ (below the Esc key).

Thanks Chris, here is the DataService code;

import Foundation

class DataService {
    
    static func getLocalData3() -> [DailyTotal] {
        
        let pathString = Bundle.main.path(forResource: "DailyTotal", ofType: "json")
        
        guard pathString != nil else {
            return [DailyTotal]()
        }
        
        let url = URL(fileURLWithPath: pathString!)
        
        do {
            
            let data = try Data(contentsOf: url)
            
            
            let decoder = JSONDecoder()
            
            do {
                
                let dailyData = try decoder.decode([DailyTotal].self, from: data)
                
                for d in dailyData {
                    d.id = UUID()
                    
                    for c in d.componentsAdded {
                        c.id = UUID()
                        
                    }
                }
                
                return dailyData
            }
            catch {
                print(error)
            }
        }
        
        
        
        catch {
            print(error)
        }
        
        return [DailyTotal]()
    }
    
    static func getLocalData2() -> [Variety] {
        
        let pathString = Bundle.main.path(forResource: "VarietyData", ofType: "json")
        
        guard pathString != nil else {
            return [Variety]()
        }
        
        let url = URL(fileURLWithPath: pathString!)
        
        do {
            
            let data = try Data(contentsOf: url)
            
            
            let decoder = JSONDecoder()
            
            do {
                
                let varietyData = try decoder.decode([Variety].self, from: data)
                
                for v in varietyData {
                    v.id = UUID()
                    
                    for i in v.varieties {
                        i.id = UUID()
                        
                    }
                }
                
                return varietyData
            }
            catch {
                print(error)
            }
        }
        
        
        
        catch {
            print(error)
        }
        
        return [Variety]()
    }
    
    static func getLocalData() -> [FoodandDrink] {
        
        let pathString = Bundle.main.path(forResource: "CalorieData", ofType: "json")
        
        guard pathString != nil else {
            return [FoodandDrink]()
        }
        
        let url = URL(fileURLWithPath: pathString!)
        
        do {
            
            let data = try Data(contentsOf: url)
            
            
            let decoder = JSONDecoder()
            
            do {
                
                let foodandDrinkData = try decoder.decode([FoodandDrink].self, from: data)
                
                for f in foodandDrinkData {
                    f.id = UUID()
                    
                    for i in f.items {
                        i.id = UUID()
                        
                        for c in i.components {
                            c.id = UUID()
                            
                            
                        }
                    }
                }
                
                return foodandDrinkData
            }
            catch {
                print(error)
            }
        }
        
        
        
        catch {
            print(error)
        }
        
        return [FoodandDrink]()
    }
}

I’ve still had no luck resolving this. Interestingly the EnvironmentObject is updating instantly on the “VarityListVIew” where the user selects the different quantity and variety of each component. However when the back button is pressed returning to the “ComponentListView” the updated EnvironmentObject is not carried over, when a new version of “ComponentListView” is loaded (by going back and clicking the link again) the EnvironmentObject is updated as expected. Any ideas greatly received!

I have put the code for the “VarietyListView” and a GIF of the above scenario below.

Simulator Screen Recording - iPhone 12 - 2021-08-10 at 14.36.51



struct VarietyListView: View {
    @EnvironmentObject var model:FoodandDrinkModel
    @EnvironmentObject var model2:VarietyModel
    
    @State var component:Component
    @State var selectedVariety:Int = 0
    @State var selectedQuantity:Int = 0
    
    var thisVariey:[Varieties]  { model.getVarities(component: component, varietyModel: model2)
    }
    
    var body: some View {
        
        
        HStack{
            Text (String(component.name))
        }
        .onAppear {selectedQuantity = component.quantity}
        .onChange(of: selectedQuantity, perform: { value in
            component.quantity = selectedQuantity
            print (selectedQuantity)
        })
        .onAppear {selectedVariety = component.varietyCalories}
        .onChange(of: selectedVariety, perform: { value in
            component.varietyCalories = selectedVariety
            print (selectedVariety)
        })
        
        
        
        
        Spacer()
        Text("Choose quantity:")
        Picker(selection: $selectedQuantity, label: Text("Choose quantity")) {
            ForEach(1..<30) { index in Text(" \(index)0g").tag(index)
            }
            
        }
        .pickerStyle(WheelPickerStyle())
        
        
        Text("Choose variety:")
        Picker(selection: $selectedVariety, label: Text("Choose variety")) {
            ForEach (thisVariey) { r in
                
                
                Text(r.name).tag(r.calories)
                
            }
            
        }.pickerStyle(WheelPickerStyle())
        
        Spacer()
        
        VStack {
            HStack{
                Text("Calories:")
                
                Text(String(model.calculatedCalories(component: component, computedQuantity: selectedQuantity, computedCalories: selectedVariety)))
                
            }
            
            
        }
        
    }
    
}


UPDATE: After some further messing around and googling, it may well be the the problem lies with the way I have used ForEach. There seems to be a lot of people out there struggling with views in ForEach not updating. There are also a few different suggestions for how to fix this, none of the ones I have tried so far have worked however so I will keep plugging away!

Usinf ForEach like this:

will not update anything. It’s for use with constant data only.

Using ForEach like this:

should work fine.

thanks @roosterboy, I also noticed that I have this function outside of a ForEach which is suffering from the same issue.

 VStack(alignment: .center) {
                    Text("Calories:")
                    Text(String(model.totalCal(item: item)))
                    
                }

Back to the drawing board!