I have a problem with the SwiftData model not updating properly when changing some its child property's property. haha.. I know :)
Let me provide some code samples so you can understand my problem. I have the following models:
@Model
final class Exercise {
@Attribute(.unique) var id: UUID
@Attribute(.unique) var title: String
@Relationship(deleteRule: .cascade, inverse: \WorkoutExercise.exercise)
var exercises: [WorkoutExercise] = [WorkoutExercise]()
init(id: UUID = .init(), title: String = "", exercises: [WorkoutExercise] = []) {
self.id = id
self.title = title
self.exercises = exercises
}
}
@Model
final class Workout {
@Attribute(.unique) var id: UUID
@Attribute(.unique) var title: String
@Relationship(deleteRule: .cascade, inverse: \WorkoutExercise.workout)
var exercises: [WorkoutExercise] = [WorkoutExercise]()
init(id: UUID = .init(), title: String = "", exercises: [WorkoutExercise] = []) {
self.id = id
self.title = title
self.exercises = exercises
}
}
extension Workout {
@Transient
var subtitle: String {
let exerciseTitles = self.exercises
.compactMap { $0.title.lowercased() }
.joined(separator: ", ")
return exercises.isEmpty ? "No exercises" : exerciseTitles
}
}
@Model
final class WorkoutExercise {
var id: UUID
var exercise: Exercise?
var workout: Workout?
var sets: [ExerciseSet] = [ExerciseSet]()
init(id: UUID = .init(), exercise: Exercise, sets: [ExerciseSet] = []) {
self.id = id
self.exercise = exercise
self.sets = sets
}
}
extension WorkoutExercise {
@Transient
var title: String {
exercise?.title ?? "Empty"
}
}
struct ExerciseSet: Identifiable, Codable {
var id: UUID
var weight: Int
var reps: Int
var rest: Int
init(id: UUID = .init(), weight: Int, reps: Int, rest: Int) {
self.id = id
self.weight = weight
self.reps = reps
self.rest = rest
}
}
Long story short, I separated the Exercise from Workout by using WorkoutExercise so I can create multiple workouts using the same exercise (Exercise) while each of these exercises (WorkoutExercise) has its own implementation using its own sets (ExerciseSet). I want somehow to use then Exercise type to create some statistics based on its exercises (WorkoutExercise).
My problem is that when I want to edit an WorkoutExercise title, I am actually gonna edit its exercise: Exercise? title instead in order to update the title in all workouts workouts that includes the exercise I edited. But somehow this change is not updating the whole UI instantly, but only after I restart the app.
As far as I can tell, the subtitle Workout property is not correctly updating.
Here is how I am displaying the workouts.
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \Workout.title, order: .forward) private var workouts: [Workout]
var body: some View {
NavigationStack {
List {
ForEach(workouts) { workout in
Section {
WorkoutListItem(workout: workout)
}
}
}
.navigationTitle("Workouts")
.scrollDisabled(workouts.isEmpty)
.listSectionSpacing(.compact)
.navigationDestination(for: Workout.self) { workout in
WorkoutDetailView(workout: workout)
}
}
}
}
struct WorkoutListItem: View {
@Environment(\.modelContext) private var modelContext
private var workout: Workout
init(workout: Workout) {
self.workout = workout
}
var body: some View {
NavigationLink(value: workout) {
VStack(alignment: .leading) {
Text(workout.title)
.font(.title3.bold())
Divider()
Text(workout.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
.italic()
}
}
}
}
struct WorkoutDetailView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
private var workout: Workout
init(workout: Workout) {
self.workout = workout
}
var body: some View {
List {
Section {
ForEach(workout.exercises) { exercise in
Text(exercise.title)
}
} header: {
Text("Exercises")
}
}
.navigationTitle(workout.title)
.navigationDestination(for: WorkoutExercise.self) { exercise in
EditWorkoutExerciseView(exercise: exercise)
}
}
}
This is how I edit the WorkoutExercise.
The update
struct EditWorkoutExerciseView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
var exercise: WorkoutExercise
@State private var title: String = ""
init(exercise: WorkoutExercise) {
self.exercise = exercise
}
var body: some View {
Form {
Section {
TextField(exercise.title.isEmpty ? "Enter title here..." : exercise.title, text: $title)
} header: {
Text("Exercise Title")
} footer: {
Text("This title will be updated in all workouts that include this exercise.")
}
}
.navigationTitle("Edit Exercise")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
update()
dismiss()
} label: {
Text("Save")
}
}
}
}
private func update() {
guard let exerciseToEdit = exercise.exercise else { return }
if !title.isEmpty {
exerciseToEdit.title = title
}
do {
try modelContext.save()
} catch {
print("Error")
}
}
}
The problem is that the updated exercise title inside the WorkoutDetailView is displayed correctly, but the subtitle is not updating inside the WorkoutListItem, so in the ContentView list also.
Can anyone explain to me why does this happen?
WorkoutListIteminit is called again, but the body does not update at all even though it contains the subtitle (which I printed on init so I can see it updates and it does).