SwiftUI: Array Not Updating In All Views When Referencing The Same Observed Object

657 Views Asked by At

I am a fairly novice developer. I wrote an app for tracking indoor bouldering climbs all in 1 really large file. It was working fine, just hard to maintain. Now I am working on splitting it into multiple files and having some issues.

I created this view model to maintain all variables that are to be updated and then used by any view. This was the first piece I split out, and it worked just fine when all of my views were still in the same file.

I started by trying to get my titles to update in one file when a button was clicks in another file, with the title value maintained in what I called "VariableSetupModel". I got help here and now that is working. I tried to extend what I did for the text variable to an array I am maintaining in the same file. Here is the relevant part of this to my issue now.

File: VariableSetupModel

final class VariableSetupModel: ObservableObject {
    //static let shared = VariableSetupModel()
    
    //Climbs List
    @Published var climbs:[NSManagedObject] = []
}

Everything loads correctly when the app first builds. It pulls in the full list from CoreData and writes it to a local variable climbs for me to manipulate. But when I save a new climb to the array, it write to core data correctly, but my graphs all stay exactly the same. When I close the app and build again, the new climb I added then loads. I am using the same functions to load the graph on page load as I am when I hit save to reload, so I am stuck on how it's not working. I've spent a few days on this and made no progress.

Here is the view that adds a new climb.

File: CurrentProjectView

@EnvironmentObject var viewModel: VariableSetupModel
    
let contentView = ContentView()
    
var body: some View {

Button(action:{
                    contentView.addNewClimb(grade: viewModel.selectedGrade, attempts: viewModel.attemptsCount, status: viewModel.completeStatus)
                    
                    contentView.updateGraphArrays()
                    
                    print("submit button pressed: \(viewModel.climbs.first!)")
                }){
                    Text("Save")
                }
}

That print only shows the last item in the array when the page loaded. Does not include the new value added by contentView.addNewClimb(grade: viewModel.selectedGrade, attempts: viewModel.attemptsCount, status: viewModel.completeStatus)

I am printing after I write the new climb to contentView.addNewClimb so I went there to see what viewModel.climbs looks like there

File:ContentView

contentView.addNewClimb()

loadClimbs()

@ObservedObject var viewModel = VariableSetupModel()

var body: some View {
    VStack (alignment: .leading) {
        TodaysSessionView()

        CurrentProjectView().onAppear{
                    self.loadClimbs()
                    self.updateGraphArrays()
                }
    }.environmentObject(viewModel)
}


func addNewClimb(grade: Int, attempts: Int, status: String){
        guard let appDelegate =
            UIApplication.shared.delegate as? AppDelegate else {
                return
        }
        
        //where we are saving
        let managedContext =
            appDelegate.persistentContainer.viewContext
        
        // type of entity we are creating
        let entity =
            NSEntityDescription.entity(forEntityName: "Climb",
                                       in: managedContext)!
        
        //creating one new instance
        let newClimb = NSManagedObject(entity: entity,
                                       insertInto: managedContext)
        
        
        //setting the value of each property
        newClimb.setValue(grade, forKeyPath: "grade")
        newClimb.setValue(attempts, forKeyPath: "attempts")
        newClimb.setValue(status, forKeyPath: "passfail")
        newClimb.setValue(Date(), forKeyPath: "climbdate")
    
        do {
            //try to save
            try managedContext.save()
            print("saved successfully!!!!")
            print(newClimb)
            
            self.loadClimbs()
        } catch let error as NSError {
            //if cannot save then print error
            print("Could not save. \(error), \(error.userInfo)")
        }
        
    }

func loadClimbs(){
        guard let appDelegate =
            UIApplication.shared.delegate as? AppDelegate else {
                return
        }
        
        let managedContext =
            appDelegate.persistentContainer.viewContext
        
        let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Climb")
        
        //sort
        let sort = NSSortDescriptor(key: "climbdate", ascending: false)
        fetchRequest.sortDescriptors = [sort]
        
        do {
            //set the climbs variable to all values
            viewModel.climbs = try managedContext.fetch(fetchRequest)
            
            print("View model updated:", viewModel.climbs.first!)

        } catch let error as NSError {
            print("Could not fetch. \(error), \(error.userInfo)")
        }
        
        self.updateGraphArrays()
    }

In my addNewClimb functionprint(newClimb) prints the correct latest climb. So it is written to CoreData Correctly. Then I call loadClimbs() to write to VariableSetupModel().climbs.

In my loadClimbs function, print("View model updated:", viewModel.climbs.first!) also reflects the correct value.

So in order: I hit submit, writes the new climb to core data, I then write the new core data values to viewModel.climbs in my loadClimbs() function and that still works. And then the last action in my submit button is to print the same viewModel.climbs value, and it only has a version of the array that appeared on page load, not the same version of the array printed in loadClimbs(). Therefore, my TodaysSessionView() view also does not get an updated version of viewModel.climbs either, which is what I ultimately want.

I have been running in circles around this and cannot figure it out. Any help would be appreciated. Thank you.

Edit: Here is the view where viewModel.climbs is bring used. It ends up looking like this enter image description here

struct TodaysSessionView: View {
    @EnvironmentObject var viewModel: VariableSetupModel
    let contentView = ContentView()
    
    var body: some View {
        let layout = [
            GridItem(.adaptive(minimum: 50)),
            GridItem(.adaptive(minimum: 50)),
            GridItem(.adaptive(minimum: 50)),
            GridItem(.adaptive(minimum: 50)),
            GridItem(.adaptive(minimum: 50))
        ]

        //Get last climb time
        let lastClimb = (viewModel.climbs.first as? Climb)?.climbdate
        
        if lastClimb ?? Date() < Date().addingTimeInterval(-28800) {
            Text("Last Session")
                .font(.title)
                .padding(.top)
                
        }else{
            Text("Todays Session")
                .font(.title)
                .padding(.top)
        }
        
        ScrollView{
            
                LazyVGrid (columns: layout, spacing: 15) {
                    ForEach(viewModel.climbs.reversed(), id: \.self){ thisClimb in
                        let gradeText = (thisClimb as? Climb)?.grade
                        let attemptsText = (thisClimb as? Climb)?.attempts
                        let passfailText = (thisClimb as? Climb)?.passfail ?? "climb string err"
                        let climbdateText = (thisClimb as? Climb)?.climbdate
                        
                        //how far back to pull into this. Maybe it should be -12 hours from the last climb
                        let cutoff = lastClimb!.addingTimeInterval(-28800)
                     
                        if climbdateText! > cutoff {
                            if passfailText == "Pass" {
                                VStack{
                                    ZStack {
                                        if attemptsText == 1 {
                                            Circle()
                                                .fill(Color("whiteblack-bg"))
                                                .frame(width: 50, height: 50)
                                                .overlay(RoundedRectangle(cornerRadius: 25)
                                                .strokeBorder(viewModel.gradesColor[Int(gradeText!)], lineWidth: 3))

                                            ZStack{
                                                Circle()
                                                    .fill(viewModel.gradesColor[Int(gradeText!)])
                                                    .frame(width: 22, height: 22)
                                                    
                                                Circle()
                                                    .fill(Color("whiteblack-bg"))
                                                    .frame(width: 18, height: 18)
                                                
                                                Image(systemName: "bolt.fill")
                                                    .foregroundColor(Color("flashColor"))
                                                    .font(.footnote)
                                            }.offset(x: 0, y: 23)
                                            
                                        }else{
                                            Circle()
                                                .fill(Color("whiteblack-bg"))
                                                .frame(width: 50, height: 50)
                                                .overlay(RoundedRectangle(cornerRadius: 25)
                                                .strokeBorder(viewModel.gradesColor[Int(gradeText!)], lineWidth: 3))
                                            
                                            ZStack{
                                                Circle()
                                                    .fill(viewModel.gradesColor[Int(gradeText!)])
                                                    .frame(width: 22, height: 22)
                                                
                                                Circle()
                                                    .fill(Color("whiteblack-bg"))
                                                    .frame(width: 18, height: 18)
                                                    
                                                Text("\(attemptsText!)")
                                                    .foregroundColor(Color("whiteblack"))
                                                    .font(.footnote)
                                            }.offset(x: 0, y: 23)
                                        }
                                        
                                        Text(viewModel.gradesV[Int(gradeText!)])
                                    }
                                }
                            }else{
                                //Didnt pass
                                VStack{
                                    ZStack{
                                        Circle()
                                            .fill(Color("whiteblack-bg"))
                                            .frame(width: 50, height: 50)
                                            .overlay(RoundedRectangle(cornerRadius: 25)
                                                        .strokeBorder(viewModel.gradesColor[Int(gradeText!)], lineWidth: 3))
                                        
                                        ZStack{
                                            Circle()
                                                .fill(Color(.red))
                                                .frame(width: 22, height: 22)
                                                
                                            
                                            
                                            Image(systemName: "xmark")
                                                .foregroundColor(.white)
                                                .font(.footnote)
                                        }.offset(x: 0, y: 22)
                                        
                                        Text(viewModel.gradesV[Int(gradeText!)])
                                    }
                                }
                            }
                        }
                    }
                }
        }
    }
}
1

There are 1 best solutions below

0
On BEST ANSWER

Ok I got it working. All I had to do was move the functions I had in my view to my viewModel. Im guessing to keep everything in the same scope. My guess is that everything in ContentView was it's own copy. I'm not 100% certain on that, but it works now.