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:
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)
}
}