commit 03a62dda1ebaa53ef026bffe48e30cbb385b2866
parent 26dcb6b904318f4679d766d1264a533a586b8589
Author: Michael Camilleri <[email protected]>
Date: Sat, 2 May 2026 01:32:39 +0900
Reduce calculations during rendering
Diffstat:
2 files changed, 45 insertions(+), 17 deletions(-)
diff --git a/Crossmate/Models/PlayerSession.swift b/Crossmate/Models/PlayerSession.swift
@@ -28,6 +28,12 @@ final class PlayerSession {
/// Unset for solo (non-shared) games.
var onSelectionChanged: ((PlayerSelection) -> Void)?
+ /// Optional sink fired after local mutations that can fill or solve the
+ /// puzzle. This keeps completion UI out of SwiftUI's render-time body
+ /// evaluation path.
+ @ObservationIgnored
+ var onCompletionStateChanged: ((Game.CompletionState) -> Void)?
+
/// Rebus mode lets the player type a multi-character value into a single
/// cell (e.g. "STAR" or "♥"). While active, keyboard input accumulates in
/// `rebusBuffer` rather than going straight to `Game.squares`; on commit
@@ -163,14 +169,17 @@ final class PlayerSession {
let cell = puzzle.cells[selectedRow][selectedCol]
guard !cell.isBlock else { return }
mutator.revealCells([cell])
+ publishTerminalCompletionState()
}
func revealCurrentWord() {
mutator.revealCells(currentWordCells())
+ publishTerminalCompletionState()
}
func revealPuzzle() {
mutator.revealCells(puzzle.cells.flatMap { $0 })
+ publishTerminalCompletionState()
}
func clearCurrentWord() {
@@ -202,7 +211,9 @@ 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 }
+ let completionState = game.completionState
+ publishTerminalCompletionState(completionState)
+ guard completionState != .solved else { return }
advance()
}
@@ -243,7 +254,9 @@ final class PlayerSession {
isRebusActive = false
rebusBuffer = ""
mutator.setLetter(value, atRow: selectedRow, atCol: selectedCol, pencil: isPencilMode)
- guard game.completionState != .solved else { return }
+ let completionState = game.completionState
+ publishTerminalCompletionState(completionState)
+ guard completionState != .solved else { return }
advance()
}
@@ -298,6 +311,12 @@ final class PlayerSession {
direction == .across ? (0, 1) : (1, 0)
}
+ private func publishTerminalCompletionState(_ state: Game.CompletionState? = nil) {
+ let state = state ?? game.completionState
+ guard state != .incomplete else { return }
+ onCompletionStateChanged?(state)
+ }
+
private func advance() {
let (dr, dc) = step(for: direction)
let r = selectedRow + dr
diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift
@@ -174,23 +174,14 @@ struct PuzzleView: View {
hasSolved = true
}
}
- .onChange(of: session.game.completionState) { _, newValue in
- switch newValue {
- case .incomplete:
- break
- case .filledWithErrors:
- showErrorsAlert = true
- case .solved:
- guard !hasSolved else { return }
- hasSolved = true
- if session.isPencilMode {
- session.togglePencil()
- }
- Task { @MainActor in
- onComplete?()
- }
+ .onAppear {
+ session.onCompletionStateChanged = { newValue in
+ handleCompletionState(newValue)
}
}
+ .onDisappear {
+ session.onCompletionStateChanged = nil
+ }
.alert("Not Quite Right", isPresented: $showErrorsAlert) {
Button("OK", role: .cancel) {}
} message: {
@@ -242,6 +233,24 @@ struct PuzzleView: View {
}
}
+ private func handleCompletionState(_ newValue: Game.CompletionState) {
+ switch newValue {
+ case .incomplete:
+ break
+ case .filledWithErrors:
+ showErrorsAlert = true
+ case .solved:
+ guard !hasSolved else { return }
+ hasSolved = true
+ if session.isPencilMode {
+ session.togglePencil()
+ }
+ Task { @MainActor in
+ onComplete?()
+ }
+ }
+ }
+
private var puzzleArea: some View {
ZStack {
VStack(spacing: 4) {