commit e5f5fdc8a5ec555784791c4b3f402f7a2467b11a
parent bc26a85e9ed8b8d2eb3605a9640e7c42427e5803
Author: Michael Camilleri <[email protected]>
Date: Thu, 14 May 2026 14:05:12 +0900
Refresh viewContext after MovesUpdater flush
MovesUpdater.persistAndMerge bumps game.updatedAt on a newBackgroundContext()
and saves. viewContext.automaticallyMergesChangesFromParent applies that change
to the in-memory snapshot but doesn't reliably fire the ObjectsDidChange
notification that @FetchRequest's NSFetchedResultsController listens for, so
GameListView kept showing a stale "last updated" relative time on the device
that did the typing.
The MovesUpdater sink in AppServices now hops to MainActor and calls
viewContext.refresh(entity, mergeChanges: true) for each affected GameEntity
before falling through to the existing iCloud enqueue. Refresh emits
ObjectsDidChange with refreshedObjects, which NSFRC treats as a per-entity
update, so @FetchRequest re-fires and GameSummaryCache picks up the merged
updatedAt. The nudge is not conditioned on isICloudSyncEnabled so local-only
games get the same fix.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift
@@ -1,4 +1,5 @@
import CloudKit
+import CoreData
import Foundation
import UserNotifications
@@ -58,7 +59,29 @@ final class AppServices {
debounceInterval: .milliseconds(500),
persistence: persistence,
writerAuthorIDProvider: { await MainActor.run { identity.currentID } },
- sink: { gameIDs in
+ sink: { [persistence] gameIDs in
+ // MovesUpdater bumps game.updatedAt on a background context.
+ // viewContext.automaticallyMergesChangesFromParent applies that
+ // change in-memory but doesn't reliably fire the ObjectsDidChange
+ // notification that @FetchRequest's NSFetchedResultsController
+ // listens for, so the library list keeps showing the stale
+ // "last updated" time until something else nudges the context.
+ // The inbound path is masked by noteIncomingMovesUpdate's
+ // explicit viewContext save; the outbound path has no analog.
+ // Refreshing the affected entities re-emits ObjectsDidChange
+ // with refreshedObjects, which NSFRC treats as a per-entity
+ // update — that path runs unconditionally so local-only games
+ // get the same nudge even when iCloud sync is off.
+ await MainActor.run {
+ let viewContext = persistence.viewContext
+ for gameID in gameIDs {
+ let req = NSFetchRequest<GameEntity>(entityName: "GameEntity")
+ req.predicate = NSPredicate(format: "id == %@", gameID as CVarArg)
+ req.fetchLimit = 1
+ guard let entity = try? viewContext.fetch(req).first else { continue }
+ viewContext.refresh(entity, mergeChanges: true)
+ }
+ }
let isEnabled = await MainActor.run { preferences.isICloudSyncEnabled }
guard isEnabled else { return }
await syncEngine.enqueueMoves(gameIDs: gameIDs)