crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 750e205c61860950eda4ad54b38b33bdefa7d8ae
parent 39e4b818fd13cc6f1aae8132c6fc5f670a3fcb1e
Author: Michael Camilleri <[email protected]>
Date:   Sat, 25 Jul 2026 00:24:21 +0900

Preserve Game List identity across Chronicle replacement

When a Chronicle replaced its live Game as the visible completed
representation, its derived archive UUID made SwiftUI animate the old
row away and insert a new one.

This commit gives GameSummary a separate stable Game List identity based
on the original game UUID. Both list and grid layouts use that identity
for completed games, while navigation continues to use the actual
persisted row ID.

Co-Authored-By: Codex GPT 5.6 Sol <[email protected]>

Diffstat:
MCrossmate/Persistence/GameStore.swift | 8++++++++
MCrossmate/Views/GameList/GameListView.swift | 4++--
MTests/Unit/ArchiveTests.swift | 6++++++
3 files changed, 16 insertions(+), 2 deletions(-)

diff --git a/Crossmate/Persistence/GameStore.swift b/Crossmate/Persistence/GameStore.swift @@ -94,7 +94,12 @@ enum GameThumbnailCell: Equatable { /// SwiftUI's `@FetchRequest` can drive the list and still render through /// an immutable, diff-friendly model. struct GameSummary: Identifiable, Equatable { + /// The persisted row to open. A Chronicle uses its derived archive UUID. let id: UUID + /// Stable Game List identity across the live Game → Chronicle handoff. + /// SwiftUI can therefore update the existing row instead of animating a + /// removal and insertion when the visible representation changes. + let listID: UUID let title: String let publisher: String? let puzzleDate: Date? @@ -211,6 +216,9 @@ struct GameSummary: Identifiable, Equatable { } self.id = id + self.listID = entity.ckRecordName.flatMap( + Archive.originalGameID(fromName:) + ) ?? id self.title = entity.title ?? "Untitled" self.publisher = publisher self.puzzleDate = puzzleDate diff --git a/Crossmate/Views/GameList/GameListView.swift b/Crossmate/Views/GameList/GameListView.swift @@ -535,7 +535,7 @@ struct GameListView: View { if !completed.isEmpty || hasMore { Section { - ForEach(completed) { game in + ForEach(completed, id: \.listID) { game in rowView(for: game, usesRoomierType: usesRoomierType) } } header: { @@ -620,7 +620,7 @@ struct GameListView: View { if !completed.isEmpty || hasMore { Section { LazyVGrid(columns: gridColumns, spacing: 12) { - ForEach(completed) { game in + ForEach(completed, id: \.listID) { game in gameCard(for: game, usesRoomierType: usesRoomierType) } } diff --git a/Tests/Unit/ArchiveTests.swift b/Tests/Unit/ArchiveTests.swift @@ -847,6 +847,12 @@ struct ArchiveTests { ) let archive = try #require(ctx.fetch(archiveReq).first) #expect(!archive.isHidden) + + let liveSummary = try #require(GameSummary(entity: live)) + let archiveSummary = try #require(GameSummary(entity: archive)) + #expect(liveSummary.id != archiveSummary.id) + #expect(liveSummary.listID == original) + #expect(archiveSummary.listID == original) } @Test("applier materializes when the original is absent")