crossmate

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

commit 412e38cdf3a44e290a3e4ab8e510af2cf6cd95f3
parent 23dcb635efd09a516f60aecebcb9856a42ec58f2
Author: Michael Camilleri <[email protected]>
Date:   Thu, 25 Jun 2026 00:43:26 +0900

Seed the share-screen checkmarks before the first frame

Reflecting already-invited friends through a CloudKit fetch meant the
checkmark animated after a brief delay whenever the share screen
re-opened, as though the invite had only just been sent — a confusing
flourish for an invitation that is known to have been sent.

This commit seeds each invite surface synchronously at construction from
ShareController.invitedAuthorIDsKnownThisSession(for:), a plain read of
the in-session invitee set. A friend invited this session now wears a
static checkmark on the first frame, since FriendAvatarView animates
only a transition into sent, not an initial value. The async fetch stays
as a backfill for invites made in a prior session or on another device,
where the delay — and the animation — is legitimate.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

Diffstat:
MCrossmate/Sync/ShareController.swift | 11++++++++++-
MCrossmate/Views/Friends/FriendPickerView.swift | 8+++++++-
MCrossmate/Views/GameList/GameShareItem.swift | 14+++++++++++++-
3 files changed, 30 insertions(+), 3 deletions(-)

diff --git a/Crossmate/Sync/ShareController.swift b/Crossmate/Sync/ShareController.swift @@ -374,7 +374,7 @@ final class ShareController { /// earlier. Best-effort: an unshared game or a transient fetch failure /// falls back to the session set alone. func invitedAuthorIDs(for gameID: UUID) async -> Set<String> { - var invited = sessionInvitedAuthorIDs[gameID] ?? [] + var invited = invitedAuthorIDsKnownThisSession(for: gameID) let ctx = persistence.viewContext let request = NSFetchRequest<GameEntity>(entityName: "GameEntity") request.predicate = NSPredicate(format: "id == %@", gameID as CVarArg) @@ -388,6 +388,15 @@ final class ShareController { return invited } + /// The author IDs added as invitees during this app session, readable + /// synchronously so a re-presented share screen can render their checkmark + /// on the first frame — no await, no animated transition — before the + /// async invitedAuthorIDs(for:) backfills anyone invited on another device + /// or in a prior session. + func invitedAuthorIDsKnownThisSession(for gameID: UUID) -> Set<String> { + sessionInvitedAuthorIDs[gameID] ?? [] + } + /// The game's grid silhouette for share-link previews, read from the /// cached block layout so it costs nothing at link-creation time. Returns /// `nil` when the cache hasn't been populated, in which case the link simply diff --git a/Crossmate/Views/Friends/FriendPickerView.swift b/Crossmate/Views/Friends/FriendPickerView.swift @@ -19,13 +19,19 @@ struct FriendPickerView: View { private var friends: FetchedResults<FriendEntity> @State private var invitingAuthorID: String? - @State private var invitedAuthorIDs: Set<String> = [] + @State private var invitedAuthorIDs: Set<String> @State private var isInviteLimitReached: Bool @State private var errorMessage: String? init(gameID: UUID, shareController: ShareController, isInviteLimitReached: Bool = false) { self.gameID = gameID self.shareController = shareController + // Seed from the in-memory session set so friends invited a moment ago + // already wear their checkmark on the first frame; the .task below + // backfills anyone invited in a prior session or on another device. + _invitedAuthorIDs = State( + initialValue: shareController.invitedAuthorIDsKnownThisSession(for: gameID) + ) _isInviteLimitReached = State(initialValue: isInviteLimitReached) } diff --git a/Crossmate/Views/GameList/GameShareItem.swift b/Crossmate/Views/GameList/GameShareItem.swift @@ -22,9 +22,21 @@ struct GameShareSheet: View { @State private var didLoadExistingLink = false @State private var didCopy = false @State private var invitingAuthorID: String? - @State private var invitedAuthorIDs: Set<String> = [] + @State private var invitedAuthorIDs: Set<String> @State private var isInviteLimitReached = false + init(gameID: UUID, title: String, shareController: ShareController) { + self.gameID = gameID + self.title = title + self.shareController = shareController + // Seed from the in-memory session set so friends invited a moment ago + // already wear their checkmark on the first frame; the .task below + // backfills anyone invited in a prior session or on another device. + _invitedAuthorIDs = State( + initialValue: shareController.invitedAuthorIDsKnownThisSession(for: gameID) + ) + } + private var visibleFriends: Array<FetchedResults<FriendEntity>.Element> { Array(friends.prefix(4)) }