Reputation: 31
I'm working on a SwiftUI app using SwiftData for persistence. I have a TaskModel
class with a relationship to DailyProgress
objects. When I update the progress, the view doesn't reflect the changes. Here's an example to demonstrate: (You can just copy everything)
import SwiftUI
import SwiftData
@main
struct SO_model_testApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
TaskModel.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
@Model
class DailyProgress: Identifiable, CustomStringConvertible {
var id = UUID()
var date: Date
var goal: Int
var task: TaskModel?
init(date: Date, goal: Int) {
self.date = date
self.goal = goal
}
var description: String {
"\(date) - \(goal)"
}
var weekOfYear: Int {
let calendar = Calendar.autoupdatingCurrent
return calendar.component(.weekOfYear, from: date)
}
var yearForWeekOfYear: Int {
let calendar = Calendar.autoupdatingCurrent
return calendar.component(.yearForWeekOfYear, from: date)
}
}
@Model
class TaskModel: Identifiable {
@Attribute(.unique) var id = UUID().uuidString
var name: String
var goal: Int
@Relationship(deleteRule: .cascade)
var progress: [DailyProgress] = []
init(name: String, goal: Int) {
self.name = name
self.goal = goal
}
func goalForDate(_ date: Date) -> Int {
let calendar = Calendar.autoupdatingCurrent
for dailyProgress in progress {
if calendar.isDate(dailyProgress.date, inSameDayAs: date) {
return dailyProgress.goal
}
}
return 0
}
@MainActor
func updateGoal(date: Date, value: Int) {
let calendar = Calendar.autoupdatingCurrent
let dateIndex = progress.firstIndex(where: { calendar.isDate($0.date, inSameDayAs: date) })
if let index = dateIndex {
progress[index].goal += value
if progress[index].goal <= 0 {
progress.remove(at: index)
}
} else if value > 0 {
let newProgress = DailyProgress(date: date, goal: value)
progress.append(newProgress)
}
}
}
struct ContentView: View {
@Query private var tasks: [TaskModel]
@State private var selectedTask: TaskModel?
@State private var showingNewTaskSheet = false
var body: some View {
NavigationView {
List(tasks) { task in
TaskRow(task: task)
.onTapGesture {
selectedTask = task
}
}
.navigationTitle("Tasks")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showingNewTaskSheet = true
}) {
Image(systemName: "plus")
}
}
}
.sheet(item: $selectedTask) {
TaskDetailView(task: $0)
}
.sheet(isPresented: $showingNewTaskSheet) {
NewTaskView(isPresented: $showingNewTaskSheet)
}
}
}
}
struct TaskRow: View {
let task: TaskModel
var body: some View {
HStack {
Text(task.name)
Spacer()
Text("Goal: \(task.goal)")
.foregroundColor(.secondary)
}
}
}
struct TaskDetailView: View {
@Bindable var task: TaskModel
@Environment(\.modelContext) private var modelContext
@State private var today = Date()
var todayTotal: Int {
task.goalForDate(Date())
}
var body: some View {
Form {
Section(header: Text("Task Details")) {
TextField("Task Name", text: $task.name)
Stepper("Daily Goal: \(task.goal)", value: $task.goal, in: 1...100)
}
Section(header: Text("Total: \(todayTotal)")) {
Text("Total: \(task.goalForDate(today))")
Button(action: {
task.updateGoal(date: today, value: 1)
}) {
Label("Log Progress", systemImage: "plus.circle")
}
}
// Section(header: Text("All Progresses")) {
// if task.progress.isEmpty {
// Text("No progress logged yet")
// .foregroundColor(.secondary)
// } else {
// ForEach(task.progress.sorted(by: { $0.date > $1.date })) { progress in
// HStack {
// Text(progress.date, style: .date)
// Spacer()
// Text("Goal: \(progress.goal)")
// }
// }
// }
// }
}
.navigationTitle("Task Details")
}
}
struct NewTaskView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Binding var isPresented: Bool
@State private var taskName = ""
@State private var goal = 1
var body: some View {
NavigationView {
Form {
TextField("Task Name", text: $taskName)
Stepper("Daily Goal: \(goal)", value: $goal, in: 1...100)
}
.navigationTitle("New Task")
.navigationBarItems(
leading: Button("Cancel") {
isPresented = false
},
trailing: Button("Save") {
modelContext.insert(TaskModel(name: taskName, goal: goal))
isPresented = false
}
.disabled(taskName.isEmpty)
)
}
}
}
When I tap the "Log Progress" button, updateGoal
is called, but the "Total" text doesn't update to reflect the new value.
If you uncomment commented code, then it works fine, because I think the section needs this progress data to show. But why doesn't it get updated when commented out? I still use daily progress implicitly in task.goalForDate()
, so I expect it to be updated as well.
Upvotes: 1
Views: 482
Reputation: 52043
The problem is that when you update a related object in your function this will not trigger an update in SwiftUI since the view engine doesn't get notified about the update.
This seems to be a bug that has been fixed in iOS 18, macOS 15 but as a workaround for older versions one way is to use a property wrapper like @State
to get updates for the related object as well.
So first we change the function in the model to return the found object instead
func goalForDate(_ date: Date) -> DailyProgress? {
let calendar = Calendar.autoupdatingCurrent
return progress.first(where: { calendar.isDate($0.date, inSameDayAs: date) })
}
Then we add the property
@State private var currentProgress: DailyProgress?
and set it in onAppear
.onAppear {
currentProgress = task.goalForDate(Date())
}
and of course read the property where needed
var todayTotal: Int {
currentProgress?.goal ?? 0
}
//...
Text("Total: \(currentProgress?.goal ?? 0)")
Now the view will update whenever the object held by currentProgress
is updated by the updateGoal
function.
Upvotes: 1
Reputation: 30746
You're missing a @Query
for the DailyProgress
which match the required task
. You can use a predicate for that, e.g. something like:
@Query var progresses: [DailyProgress]
init(task: TaskModel) {
let predicate = #Predicate<DailyProgress> {
$0.task == task
}
_progresses = Query(filter: predicate, sort: \ DailyProgress.date)
}
You can see this pattern in TripListItem.swift
in Adopting SwiftData for a Core Data app.
Upvotes: 0