commit 7dadc6878c760a90617ec8dd5493382706144c54
parent 33c3468e93d6b62e4d3bee70b593abef113bf25f
Author: Michael Camilleri <[email protected]>
Date: Wed, 22 Apr 2026 04:25:29 +0900
Provide feedback to user when puzzle is filled in
When a puzzle is filled in, there should be feedback provided to the
user indicating that there are incorrect answers or that the puzzle is
solved. This commit is aimed at achieving that outcome.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
2 files changed, 94 insertions(+), 0 deletions(-)
diff --git a/Crossmate/Models/Game.swift b/Crossmate/Models/Game.swift
@@ -102,4 +102,32 @@ final class Game {
func clearPuzzle() {
clearCells(puzzle.cells.flatMap { $0 })
}
+
+ // MARK: - Completion
+
+ enum CompletionState: Sendable, Equatable {
+ case incomplete
+ case filledWithErrors
+ case solved
+ }
+
+ /// `.incomplete` while any non-block cell is empty; once every cell has an
+ /// entry, `.solved` if every entry matches its solution (cells with no
+ /// known solution are treated as correct) and `.filledWithErrors`
+ /// otherwise.
+ var completionState: CompletionState {
+ var hasErrors = false
+ for r in 0..<puzzle.height {
+ for c in 0..<puzzle.width {
+ let cell = puzzle.cells[r][c]
+ if cell.isBlock { continue }
+ let entry = squares[r][c].entry
+ if entry.isEmpty { return .incomplete }
+ if let solution = cell.solution, entry != solution.uppercased() {
+ hasErrors = true
+ }
+ }
+ }
+ return hasErrors ? .filledWithErrors : .solved
+ }
}
diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift
@@ -5,6 +5,8 @@ struct PuzzleView: View {
@Environment(PlayerPreferences.self) private var preferences
@State private var isRenaming = false
@State private var renameDraft = ""
+ @State private var showSuccessScreen = false
+ @State private var showErrorsAlert = false
private var isShared: Bool { false }
@@ -25,6 +27,8 @@ struct PuzzleView: View {
return TitleParts(title: title, subtitle: subtitle)
}
+ private var isSolved: Bool { session.game.completionState == .solved }
+
var body: some View {
VStack(spacing: 0) {
ZStack {
@@ -65,6 +69,7 @@ struct PuzzleView: View {
VStack(spacing: 0) {
ClueBar(session: session)
KeyboardView(session: session)
+ .disabled(isSolved)
}
.background(Color(.systemGroupedBackground))
}
@@ -86,6 +91,7 @@ struct PuzzleView: View {
)
}
.accessibilityLabel("Pencil")
+ .disabled(isSolved)
Menu {
Section {
@@ -101,6 +107,7 @@ struct PuzzleView: View {
} label: {
Label("Hints", systemImage: "lightbulb")
}
+ .disabled(isSolved)
Menu {
Button("Clear Word") { session.clearCurrentWord() }
@@ -108,6 +115,7 @@ struct PuzzleView: View {
} label: {
Label("Clear", systemImage: "eraser")
}
+ .disabled(isSolved)
Menu {
Section {
@@ -157,6 +165,27 @@ struct PuzzleView: View {
}
}
}
+ .onChange(of: session.game.completionState) { _, newValue in
+ switch newValue {
+ case .incomplete:
+ break
+ case .filledWithErrors:
+ showErrorsAlert = true
+ case .solved:
+ if session.isPencilMode {
+ session.togglePencil()
+ }
+ showSuccessScreen = true
+ }
+ }
+ .alert("Not Quite Right", isPresented: $showErrorsAlert) {
+ Button("OK", role: .cancel) {}
+ } message: {
+ Text("One or more squares are incorrect.")
+ }
+ .sheet(isPresented: $showSuccessScreen) {
+ SuccessScreen(session: session)
+ }
.alert("Change Name", isPresented: $isRenaming) {
TextField("Name", text: $renameDraft)
.textInputAutocapitalization(.never)
@@ -253,6 +282,43 @@ private struct ClueBar: View {
}
}
+private struct SuccessScreen: View {
+ let session: PlayerSession
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ NavigationStack {
+ VStack(spacing: 24) {
+ VStack(spacing: 8) {
+ Image(systemName: "checkmark.seal.fill")
+ .font(.system(size: 64))
+ .foregroundStyle(.tint)
+ Text("Solved!")
+ .font(.largeTitle.weight(.bold))
+ Text(session.puzzle.title)
+ .font(.headline)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ if let author = session.puzzle.author {
+ Text(author)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ }
+ Spacer()
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.top, 48)
+ .padding(.horizontal)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button("Done") { dismiss() }
+ }
+ }
+ }
+ }
+}
+
private struct RebusModal: View {
let text: String