crossmate

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

commit 26dcb6b904318f4679d766d1264a533a586b8589
parent f194d2243745cda62eacf99aa25416f7a467d02a
Author: Michael Camilleri <[email protected]>
Date:   Fri,  1 May 2026 22:54:37 +0900

Improve scoreboard reveal

This commit makes a few small tweaks to improve the look of the
scoreboard as it's revealed in the game.

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

Diffstat:
MCrossmate/Models/PlayerRoster.swift | 35+++++++++++++++++++++++------------
MCrossmate/Models/PlayerSession.swift | 2++
MCrossmate/Views/PuzzleView.swift | 16+++++++++++++---
3 files changed, 38 insertions(+), 15 deletions(-)

diff --git a/Crossmate/Models/PlayerRoster.swift b/Crossmate/Models/PlayerRoster.swift @@ -26,6 +26,14 @@ final class PlayerRoster { let updatedAt: Date } + private struct RawSelection { + let authorID: String + let row: Int + let col: Int + let direction: Puzzle.Direction + let updatedAt: Date + } + /// Peer cursors keyed by `authorID`. Stale entries (older than the /// freshness window) are dropped on each refresh. The local player is /// never present in this map. @@ -121,17 +129,6 @@ final class PlayerRoster { func refresh() async { let localAuthorID = authorIdentity.currentID ?? "" - // Raw selection tuple — colour is resolved on the main actor below - // so we don't have to thread the colour store through Core Data - // closures. - struct RawSelection { - let authorID: String - let row: Int - let col: Int - let direction: Puzzle.Direction - let updatedAt: Date - } - // Pull Core Data fields off a background context. let ctx = persistence.container.newBackgroundContext() let (databaseScope, ckShareRecordName, ckZoneName, ckZoneOwnerName, namesMap, moveAuthorIDs, rawSelections) = @@ -185,13 +182,27 @@ final class PlayerRoster { ) } - // Fetch the CKShare if not already cached. + applyRoster(namesMap: namesMap, moveAuthorIDs: moveAuthorIDs, rawSelections: rawSelections, share: nil) + + // Fetch the CKShare if not already cached. This can be noticeably + // slower on device, so publish the local Core Data roster first and + // then refine names/participants if share metadata arrives. let share = await fetchShare( databaseScope: databaseScope, ckShareRecordName: ckShareRecordName, ckZoneName: ckZoneName, ckZoneOwnerName: ckZoneOwnerName ) + applyRoster(namesMap: namesMap, moveAuthorIDs: moveAuthorIDs, rawSelections: rawSelections, share: share) + } + + private func applyRoster( + namesMap: [String: String], + moveAuthorIDs: [String], + rawSelections: [RawSelection], + share: CKShare? + ) { + let localAuthorID = authorIdentity.currentID ?? "" // Collect all remote participant authorIDs. var otherAuthorIDs = Set<String>() diff --git a/Crossmate/Models/PlayerSession.swift b/Crossmate/Models/PlayerSession.swift @@ -202,6 +202,7 @@ final class PlayerSession { let cell = puzzle.cells[selectedRow][selectedCol] guard !cell.isBlock else { return } mutator.setLetter(letter, atRow: selectedRow, atCol: selectedCol, pencil: isPencilMode) + guard game.completionState != .solved else { return } advance() } @@ -242,6 +243,7 @@ final class PlayerSession { isRebusActive = false rebusBuffer = "" mutator.setLetter(value, atRow: selectedRow, atCol: selectedCol, pencil: isPencilMode) + guard game.completionState != .solved else { return } advance() } diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift @@ -14,6 +14,7 @@ struct PuzzleView: View { @State private var leaveError: String? @State private var isRevokedBannerDismissed = false @State private var isShowingShareSheet = false + @State private var hasSolved = false private func swatchImage(for color: PlayerColor) -> Image { let tint = UIColor(color.tint) @@ -38,7 +39,7 @@ struct PuzzleView: View { return TitleParts(title: title, subtitle: subtitle) } - private var isSolved: Bool { session.game.completionState == .solved } + private var isSolved: Bool { hasSolved } var body: some View { VStack(spacing: 0) { @@ -168,6 +169,11 @@ struct PuzzleView: View { guard let roster else { return } await roster.refresh() } + .onAppear { + if session.game.completionState == .solved { + hasSolved = true + } + } .onChange(of: session.game.completionState) { _, newValue in switch newValue { case .incomplete: @@ -175,10 +181,14 @@ struct PuzzleView: View { case .filledWithErrors: showErrorsAlert = true case .solved: + guard !hasSolved else { return } + hasSolved = true if session.isPencilMode { session.togglePencil() } - onComplete?() + Task { @MainActor in + onComplete?() + } } } .alert("Not Quite Right", isPresented: $showErrorsAlert) { @@ -274,7 +284,7 @@ struct PuzzleView: View { Color(.systemGroupedBackground) .ignoresSafeArea(edges: .bottom) } - .animation(.smooth(duration: 0.4), value: isSolved) + .animation(.easeOut(duration: 0.25), value: isSolved) } }