Reputation: 43
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
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