Hi Ziqiao,
Your previous guidance to use a ModelActor and PersistentIdentifiers has successfully resolved all my data race crashes.
However, I'm left with one persistent UI-level race condition, specifically related to deletion.
My View Structure
My setup is a standard master-detail pattern, which on iPhone (where I'm seeing this bug) acts like a NavigationStack:
ScheduleView (Master): Uses @Query to fetch and display all events in a List. Each row is a NavigationLink. This view also has a swipe-to-delete action on its rows.
EventDetailsView (Detail): The destination of the NavigationLink. It contains a "Delete" button.
The Problem: A dismiss() vs. @Query Race
When I tap the "Delete" button in EventDetailsView, I call my actor (e.g., await databaseManager.deleteEvent(id: event.persistentModelID)) and then immediately call dismiss() to pop the view.
The Bug: When the app returns to ScheduleView, the row for the just-deleted event is still visibly active in the list for at least 5 seconds before it finally animates out. This is a very significant delay, not just a flicker.
The Key Clue: I also have a swipe-to-delete action directly on the ScheduleView list. When I use that action, which calls the exact same databaseManager.deleteEvent(id:) function, the row disappears instantaneously as expected.
This strongly suggests the race condition is not with the ModelActor or @Query's observation itself. The problem is introduced specifically by the dismiss() action from the detail view.
What I've Tried That Failed
I have tried several approaches to solve this, but the race condition persists.
Attempt 1: Forcing @MainActor
My first attempt was to wrap the entire operation in a Task marked with @MainActor inside EventDetailsView:
Task { @MainActor in
await databaseManager.deleteEvent(id: event.persistentModelID)
dismiss()
}
This did not fix the problem. The race condition remained, and the 5-second delay was still present upon returning to ScheduleView.
Attempt 2: "State Down, Actions Up" Pattern
Next, I refactored my code to pass an onDelete: () async -> Void closure from ScheduleView into EventDetailsView. The body of this closure, which lives in ScheduleView, is: await databaseManager.deleteEvent(id: event.persistentModelID).
EventDetailsView now just calls await onDelete() and then dismiss().
This also did not fix the problem. Even though the await on the actor call finished within ScheduleView (the view that owns the @Query), the dismiss() in the child view still won the race, and the UI returned to ScheduleView before @Query could update the list.
I am now stuck. All my attempts to serialize these operations have failed, and the UI remains inconsistent for a long period.
What is the correct architectural pattern to solve this specific dismiss() vs. @Query race condition? How can I ensure ScheduleView's data is consistent before it reappears after the dismiss()?
Thanks,
Michael