Post

Replies

Boosts

Views

Activity

Reply to Correct SwiftData Concurrency Logic for UI and Extensions
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
Nov ’25
Reply to Correct SwiftData Concurrency Logic for UI and Extensions
Hi Ziqiao, Thank you so much for the detailed response. Your guidance was extremely helpful. I've refactored my DatabaseManager to be a ModelActor (using the shared container) and updated its functions to work with PersistentIdentifier as you suggested. This has completely resolved the data race crashes I was experiencing. Thank you! I still have two small follow-up questions regarding the integration with App Intents and Widgets. 1. App Intents & EntityPropertyQuery with UUIDs For my App Intents, I have entities like AssignmentEntity (conforming to AppEntity) and an AssignmentQuery (conforming to EntityPropertyQuery, EnumerableEntityQuery, etc.). The protocol requires me to implement a function with this signature: func entities(for identifiers: [UUID]) async throws -> [AssignmentEntity] This creates a conflict, as my DatabaseManager actor now operates on PersistentIdentifiers, not UUIDs. My current workaround inside this required function is to create a new, temporary ModelContext just to fetch the models by their UUIDs. It looks like this: // Inside the required func entities(for identifiers: [UUID])... func entities(for identifiers: [UUID]) async throws -> [AssignmentEntity] { let context = ModelContext(DataModelEnum.container) // My shared container let f = FetchDescriptor<AssignmentModel>(predicate: #Predicate { identifiers.contains($0.id) // $0.id is my UUID property }) let models = try context.fetch(f) return models.map { $0.toAssignmentEntity() } // Converting Model to Entity } My question is: Is this pattern—creating a new, temporary ModelContext inside each of these entities(for:) functions (and presumably inside all the other required query/data-import functions for all of my entity types)—the correct way to bridge the gap between the UUID requirement from AppEntity and my PersistentIdentifier-based actor? 2. Interactive Widgets & PersistentIdentifier as a String My second question is about interactive widgets. I have a Button that triggers an intent, and the intent parameter must be a simple type like String. Previously, I did this: Button(intent: WidgetIntent1(eventIdString: event.id.uuidString), ...) Now that my DatabaseManager actor expects a PersistentIdentifier, I want to avoid passing the UUID string and then performing an extra fetch inside the intent just to get the PersistentIdentifier, only to then call the actor with that ID. My question is: Is there a recommended way to convert a PersistentIdentifier into a String (to pass to the intent) and then reliably convert that String back into a PersistentIdentifier inside the intent's perform() method? The alternative seems to be passing the UUID string, performing a fetch inside the intent just to get the model's PersistentIdentifier, and then passing that ID to my DatabaseManager actor (which then performs its own fetch using that ID). This "double fetch" feels very inefficient, and I'm wondering if there's a more direct approach. Thanks again for all your help! Michael
Nov ’25
Reply to SwiftData changes made in widget via AppIntent are not reflected in main app until full relaunch
Thank you very much for the detailed answer — it really helped me understand the problem and find a working solution! I’m not sure if this is a bug or just how @Query is designed to work, but here’s what I found: In my case, I have three models: A, B, and C. Model A contains arrays of both B and C. Initially, I used @Query only to fetch model A, and then displayed the related B and C items from within it — but I did not fetch B and C directly with @Query: @Query var a: [A] var body: some View { ForEach(a) { item in item.B item.C } } After reading your explanation, I changed my code to fetch B and C directly using @Query, and then filtered them to include only those related to the specific A instance: @Query var a: [A] @Query var b: [B] @Query var c: [C] var body: some View { ForEach(a) { item in b.filter{$0.a == item} c.filter{$0.a == item} } } Now, when the widget updates a B or C object, the corresponding view in the app does update as expected — so your explanation about remote changes and @Query behavior was exactly what I needed. Thanks again!
Jul ’25