Search code examples
core-dataswiftui

SwiftUI ForEach force UI update when updating the contents of a core data relationship


My app is meant to have a bunch of workouts in core data, each with a relationship to many exercises. A view should display the data in each workout (name, description etc.) and then iterate and display each exercise belonging to that workout.

Adding exercises and displaying them works fine. If an exercise is deleted, however it:

  • deletes from coredata no worries
  • the information seems to delete from iterableExercises
  • however, the Text line does not disappear. it goes from, for example "Squat, Description" to simply " , "
  • If I close the app entirely and reopen, then the " , " lines do completely disappear.

The problem code:

if let iterableExercises = workout.exercises?.array as? [ExerciseEntity] {
    ForEach(iterableExercises) {exercise in
        Text("\(exercise.name ?? ""), \(exercise.desc ?? "")")
    }
}

I've got the entity relationship set as ordered, but I've also tried unordered with .allObjects instead of .array. This clearly isn't the problem as it's the array iterableExercises that's not correctly being reset?

EDIT: to reproduce, here's all the code you need and some screenshots of the CoreData model.

import SwiftUI
import CoreData

class ViewModel: ObservableObject {
    
    let container: NSPersistentCloudKitContainer
    @Published var savedWorkouts: [WorkoutEntity] = []
    @Published var savedExercises: [ExerciseEntity] = []


    // MARK: INIT
    init() {
        container = NSPersistentCloudKitContainer(name: "mre")
        container.loadPersistentStores { description, error in
            if let error = error {
                print("Error loading CoreData: \(error)")
            }
        }
        fetchWorkoutEntities()
        fetchExerciseEntities()
    }

    // MARK: FETCHERS
    func fetchWorkoutEntities() {
        let request = NSFetchRequest<WorkoutEntity>(entityName: "WorkoutEntity")
        do {
            savedWorkouts = try container.viewContext.fetch(request)
        } catch let error {
            print("Error fetching WorkoutEntity: \(error)")
        }
    }
    
    func fetchExerciseEntities() {
        let request = NSFetchRequest<ExerciseEntity>(entityName: "ExerciseEntity")
        do {
            savedExercises = try container.viewContext.fetch(request)
        } catch let error {
            print("Error fetching ExerciseEntity: \(error)")
        }
    }
    
    // MARK: SAVE
    func saveData() {
        do {
            try container.viewContext.save()
            fetchWorkoutEntities()
            fetchExerciseEntities()
        } catch let error {
            print("Error saving: \(error)")
        }
    }
    
    // MARK: ADDERS
    func addWorkout(name: String) {
        let _ = WorkoutEntity(context: container.viewContext)
        saveData()
    }
    
    func addExerciseToWorkout(workout: WorkoutEntity, name: String) {
        let newExercise = ExerciseEntity(context: container.viewContext)
        newExercise.name = name
        workout.addToExercises(newExercise)
        saveData()
    }
    
    // MARK: DELETERS
    func deleteWorkout(workout: WorkoutEntity) {
        container.viewContext.delete(workout)
        saveData()
    }
    
    func deleteExercise(exercise: ExerciseEntity) {
        container.viewContext.delete(exercise)
        saveData()
    }
    // MARK: TODO: UPDATERS
}


struct ContentView: View {
    
    @StateObject var data = ViewModel()
    
    var body: some View {
        VStack {
            Button {
                data.addWorkout(name: "workout")
                data.addExerciseToWorkout(workout: data.savedWorkouts[0], name: "[exercisename]")
            } label: {
                Text("Click ONCE to add workout to work with")
            }
            Spacer()
            if let iterableExercises = data.savedWorkouts[0].exercises?.array as? [ExerciseEntity] {
                ForEach(iterableExercises) { exercise in
                    Button {
                        data.deleteExercise(exercise: exercise)
                    } label: {
                        Text("Click to delete \(exercise.name ?? "") AFTER DELETING IF THIS STILL SHOWS BUT DOESN'T SHOW THE EXERCISE NAME THEN IT'S BROKEN")
                    }
                }
            }
            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

screenshots of model


Solution

  • I’m not sure if this is the ONLY solution as @malhal gave quite an extensive and seemingly useful response.

    But I came across a much easier and immediate fix, within my original solution. The inverse relationships must be specified. Doing this resolved all issues.