KMerenda
KMerenda

Reputation: 43

ManagedObject and ObservedObject

I'm trying to learn SwiftUI and Core Data (no experience with swift or objective C at all), and working my way through it by writing an app to track gym workouts.

I have a Workout entity that has a one-many relationship to an Exercise entity.

In my app, I start with a list of Workouts in a nav view pulled from core data. Then I use a nav link to pass the selected Workout object to a details view that displays the properties of the Workout object. The details view includes the workout name, date created, and a list of exercises tied to that workout. The exercise list is loaded from the relationship property of the Workout object.

Next, I use a button to pass the Workout object to a sheet view that has a list of exercises. The exercise list is a fetchrequest of the Exercise entity, and there is a checkmark next to each exercise in the list that has a relationship to the workout. Each row in the list is an instance of another "row" view, and the entire row is a button. The user can tap on row to add/remove it from the workout.

So far, this all works great. My problem is that the workout details view doesn't update when I use the exercise list sheet view to make changes to the workout's relationship to the exercise entity. Core data does get updated, but the details view doesn't refresh the updated object from core data when the exercise sheet view is closed.

If I go back one step more to the parent view with the list of Workouts, and select that same Workout object again, now the details view refreshes with the correct exercise list from core data.

I think this is because the parent workout list is using a fetch request to get workout objects, and all the child views are just passing a single instance of the workout object down but not monitoring it for changes. When I get back to the workout list, I guess fetch request is replacing the object instance with the latest data.

I want the details view to realize when the relationship property is changed in the exercise sheet view, and then update the Exercise list that appears on that details view with those changes.

How can I make this work? I've tried @ObservedObject on the Workout object that is getting passed through the view hierarchy, and I've tried managedObjectContext.refresh(Workout, mergeChanges: true) when the dismiss button on the sheet view is tapped, but nothing.

Any ideas?

//
//  WorkoutDetail.swift


import SwiftUI

struct WorkoutDetail: View {

    @Environment (\.managedObjectContext) var moc

    @ObservedObject var workout: Workout
    @State private var workoutDate: Date
    @State private var exerciseArray: [Exercise]
    @State private var workoutName: String
    @State private var showingAddScreen = false
    var fetchRequest: FetchRequest<Workout>



    static let setDateFormat: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "MMMM d, y, h:mm a"
        return formatter
    }()

    init(workout: Workout) {
        self.workout = workout
        // Define the initial form field values
        _workoutName = .init(initialValue: workout.wrappedWorkoutName)
        _workoutDate = .init(initialValue: workout.wrappedWorkoutDate)
        _exerciseArray = .init(initialValue: workout.workoutExerciseArray)
        let workoutPredicate = NSPredicate(format: "workoutName == %@", workout.workoutName!)
        fetchRequest = FetchRequest<Workout>(entity: Workout.entity(), sortDescriptors: [], predicate: workoutPredicate)

    }

    var body: some View {
            Form {
                Section(header: Text("Workout Name")) {
                    TextField("Workout Name", text: $workoutName)
                    Text("Created \(workoutDate, formatter: Self.setDateFormat)")

                    Button("Save Changes") {
                        self.workout.setValue(self.workoutName, forKey: "workoutName")
                        try? self.moc.save()
                    }
                }
                Section(header: Text("Exercises")) {
                    Button(action: {self.showingAddScreen.toggle()}) {
                    HStack {
                        Image(systemName: "pencil")
                        Text("Add/Remove Exercises")}
                    }
                    List{
                        ForEach(exerciseArray, id:\ .self) { exercise in
                            VStack(alignment: .leading) {
                                NavigationLink(destination: ExerciseSet(exerciseSetExercise: exercise, exerciseSetWorkout: self.workout)) {
                                    WorkoutExercise(exercise: exercise)
                                }
                            }
                        }
                    }
                }
            }.navigationBarTitle("\(workoutName)")
            .sheet(isPresented: $showingAddScreen) {
                AddExerciseToWorkout(workout: self.workout).environment(\.managedObjectContext, self.moc)}
        }
}
//
//  AddExerciseToWorkout.swift


import SwiftUI


struct AddExerciseToWorkout: View {
    @ObservedObject var workout: Workout
    @State private var exerciseArray: [Exercise]
    @State private var workoutName: String
    var exerciseNameArray: [String] = []
    @State var selectedExercises: [String] = []
    @FetchRequest(entity: Type.entity(), sortDescriptors: [NSSortDescriptor(key:"typeName", ascending: true)]) var strengthExercises: FetchedResults<Type>
    @Environment(\.presentationMode) var presentationMode


    init(workout: Workout) {
        self.workout = workout
        _exerciseArray = .init(initialValue: workout.workoutExerciseArray)
        _workoutName = .init(initialValue: workout.wrappedWorkoutName)
    }


    var body: some View {
        VStack {

            Text("\(workoutName) exercises:")
                .font(.headline)
                .padding()
            List{
                ForEach(strengthExercises, id:\.self) { strengthExercise in
                    Section(header: Text(strengthExercise.wrappedTypeName)) {
                        ForEach(strengthExercise.typeExerciseArray, id: \.self) { exercise in
                            AddExerciseToWorkoutRow(workout: self.workout, exercise: exercise)

                        }
                    }
                }
            }
            Button("Done") {self.presentationMode.wrappedValue.dismiss()
            }
        }
    }
}
//
//  AddExerciseToWorkoutRow.swift


import SwiftUI
import CoreData

struct AddExerciseToWorkoutRow: View {
    @Environment(\.managedObjectContext) var moc
    let exercise: Exercise
    @ObservedObject var workout: Workout
    @State private var exerciseArray: [Exercise]
    @State private var isSelected: Bool = false


    init(workout: Workout, exercise: Exercise) {
        self.workout = workout
        self.exercise = exercise
        _exerciseArray = .init(initialValue: workout.workoutExerciseArray)
        if exerciseArray.contains(exercise) {
            _isSelected = .init(initialValue: true)
        } else {
            _isSelected = .init(initialValue: false)
        }
    }


    var body: some View {
        Button(action: {
            if self.exerciseArray.contains(self.exercise) {
                self.workout.removeFromWorkoutToExercise(self.exercise)
                self.isSelected = false
            } else {
                self.workout.addToWorkoutToExercise(self.exercise)
                self.isSelected = true
            }
            try? self.moc.save()
                self.moc.refresh(self.workout, mergeChanges: true)
                self.workout.didChangeValue(forKey: "workoutToExercise")
        }) {
            HStack {
                Text(exercise.wrappedExerciseName)
                Spacer()
                if self.isSelected {
                    Image(systemName: "checkmark")
                        .foregroundColor(.green)
                }
            }
        }
    }
}

Upvotes: 2

Views: 710

Answers (1)

KMerenda
KMerenda

Reputation: 43

I was able to resolve this by taking the exercise list that appears on the Workout detail view and putting it into another subview. I would pass the workout date (which is the unique ID for each workout) to the subview, then use that date as a NSPredicate within a FetchRequest on the subview to pull the updated list of exercises assigned to the workout. It works, but it sure feels like there must be an easier way.

Upvotes: 1

Related Questions