Fix grocery sync deletions not persisting and Reminders race condition

Stop cascading syncs by adding an isReconciling flag so that
reconcileFromServer no longer triggers scheduleSync via addItem/deleteItem.
Make Reminders write-only by removing the diff/sync logic from the
onDataChanged callback. Fetch fresh server state in RecipeView reconcile
instead of using stale local cache. Track pending removal recipe IDs via
DataStore so performInitialSync can push deletions for recipes whose
grocery keys have already been removed from groceryDict.

Fix a race condition in RemindersGroceryStore where EKEventStoreChanged
notifications triggered load() before saveMappings() finished writing to
disk, causing the correct in-memory state to be overwritten with stale
data. Add ignoreNextExternalChange flag to skip self-triggered reloads.

Restyle the add/remove all grocery button to match the Plan recipe button
style using Label, subheadline font, and rounded rectangle background.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 06:04:41 +01:00
parent 8b23652f10
commit c38d4075be
6 changed files with 123 additions and 35 deletions

View File

@@ -28,20 +28,7 @@ class GroceryListManager: ObservableObject {
remindersStore.onDataChanged = { [weak self] in
guard let self else { return }
if self.mode == .appleReminders {
let previousDict = self.groceryDict
self.groceryDict = self.remindersStore.groceryDict
// Only sync recipes that changed externally (e.g. checked off in Reminders app),
// not ones we just modified ourselves.
for recipeId in self.remindersStore.groceryDict.keys {
guard !self.recentlyModifiedByUs.contains(recipeId) else { continue }
// Detect if item count changed (external add/remove/complete)
let oldCount = previousDict[recipeId]?.items.count ?? 0
let newCount = self.remindersStore.groceryDict[recipeId]?.items.count ?? 0
if oldCount != newCount {
self.syncManager?.scheduleSync(forRecipeId: recipeId)
}
}
self.recentlyModifiedByUs.removeAll()
}
}
@@ -63,6 +50,7 @@ class GroceryListManager: ObservableObject {
remindersStore.addItem(itemName, toRecipe: recipeId, recipeName: recipeName)
groceryDict = remindersStore.groceryDict
}
syncManager?.clearPendingRemoval(recipeId: recipeId)
syncManager?.scheduleSync(forRecipeId: recipeId)
}
@@ -76,6 +64,7 @@ class GroceryListManager: ObservableObject {
remindersStore.addItems(items, toRecipe: recipeId, recipeName: recipeName)
groceryDict = remindersStore.groceryDict
}
syncManager?.clearPendingRemoval(recipeId: recipeId)
syncManager?.scheduleSync(forRecipeId: recipeId)
}
@@ -88,6 +77,9 @@ class GroceryListManager: ObservableObject {
recentlyModifiedByUs.insert(recipeId)
remindersStore.deleteItem(itemName, fromRecipe: recipeId)
}
if groceryDict[recipeId] == nil {
syncManager?.trackPendingRemoval(recipeId: recipeId)
}
syncManager?.scheduleSync(forRecipeId: recipeId)
}
@@ -100,6 +92,7 @@ class GroceryListManager: ObservableObject {
recentlyModifiedByUs.insert(recipeId)
remindersStore.deleteGroceryRecipe(recipeId)
}
syncManager?.trackPendingRemoval(recipeId: recipeId)
syncManager?.scheduleSync(forRecipeId: recipeId)
}
@@ -114,6 +107,7 @@ class GroceryListManager: ObservableObject {
remindersStore.deleteAll()
}
for recipeId in recipeIds {
syncManager?.trackPendingRemoval(recipeId: recipeId)
syncManager?.scheduleSync(forRecipeId: recipeId)
}
}