crossmate

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

commit 03a62dda1ebaa53ef026bffe48e30cbb385b2866
parent 26dcb6b904318f4679d766d1264a533a586b8589
Author: Michael Camilleri <[email protected]>
Date:   Sat,  2 May 2026 01:32:39 +0900

Reduce calculations during rendering

Diffstat:
MCrossmate/Models/PlayerSession.swift | 23+++++++++++++++++++++--
MCrossmate/Views/PuzzleView.swift | 39++++++++++++++++++++++++---------------
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) {