crossmate

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

commit c532223bd9035bbe06ca0c8eacfda3dd95dbbc72
parent e7db7db51c539719ffc93e2bb546d23e5ae1398e
Author: Michael Camilleri <[email protected]>
Date:   Wed, 22 Apr 2026 06:48:40 +0900

Use clue list to display long clues

Prior to this commit, if a clue was too long to display in the Clue Bar,
it would simply be truncated. This commit adds a clue list that a user
can bring up by tapping on the clue in the Clue Bar (this means that a
previous commit that allowed a user to toggle the solving direction had
to be reverted).

Co-Authored-By: Claude Opus 4.7 <[email protected]>

Diffstat:
MCrossmate.xcodeproj/project.pbxproj | 4++++
MCrossmate/Models/PlayerSession.swift | 7+++++++
ACrossmate/Views/ClueList.swift | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCrossmate/Views/PuzzleView.swift | 8+++++++-
4 files changed, 94 insertions(+), 1 deletion(-)

diff --git a/Crossmate.xcodeproj/project.pbxproj b/Crossmate.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ AF4F1AE2A1F94E92C785C524 /* Square.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB851649DE78AAAC5A928C52 /* Square.swift */; }; B42454D72FAA219D60DEA334 /* garden.xd in Resources */ = {isa = PBXBuildFile; fileRef = 50992CDA4082429EBB17F65C /* garden.xd */; }; B762200F54C52E8377A80D15 /* NYTToXDConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6F111BE8750697C4BC7A17 /* NYTToXDConverter.swift */; }; + B94919176DEC6EC31637B037 /* ClueList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9BD3F7EAFD344D8E10E8C3B /* ClueList.swift */; }; C30C0C4E54E4209A22843872 /* CrossmateModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F93AC31640C40FCC039570A3 /* CrossmateModel.xcdatamodeld */; }; C6E0E5128565D3B822A41605 /* PendingChangePayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = E512D95B4518EE3DE6E350C0 /* PendingChangePayload.swift */; }; C7370BCAD585EEFD366204E3 /* GridThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BB7D9759D27F7BA6734FDE /* GridThumbnailView.swift */; }; @@ -124,6 +125,7 @@ E512D95B4518EE3DE6E350C0 /* PendingChangePayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChangePayload.swift; sourceTree = "<group>"; }; E524780E360E008FACE4F213 /* PendingChange+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PendingChange+Helpers.swift"; sourceTree = "<group>"; }; E7AFD37B03A1C2E23E5766E6 /* PuzzleSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PuzzleSource.swift; sourceTree = "<group>"; }; + E9BD3F7EAFD344D8E10E8C3B /* ClueList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClueList.swift; sourceTree = "<group>"; }; EAC61E2582D94B1E6EC67136 /* XDFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDFileType.swift; sourceTree = "<group>"; }; F13AB28AA016F8A3DF53E6AA /* OutboxRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutboxRecorder.swift; sourceTree = "<group>"; }; F2F7D62E5E9EE2AEFC8940F4 /* NewGameSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewGameSheet.swift; sourceTree = "<group>"; }; @@ -236,6 +238,7 @@ 9A4B7C6A8A23C6E4CCEC759F /* BundledBrowseView.swift */, C0CAA5E17BD406AFEEF96196 /* CalendarDayCell.swift */, F8E50E7BA98C88B4CAB39DC1 /* CellView.swift */, + E9BD3F7EAFD344D8E10E8C3B /* ClueList.swift */, 38DDAD9D6470A894C3FD6F90 /* GameListView.swift */, D9BB7D9759D27F7BA6734FDE /* GridThumbnailView.swift */, CAB4BB9E160C3A59C653E7A9 /* GridView.swift */, @@ -399,6 +402,7 @@ C944A5BD871C6ECC64DE8A5B /* CalendarDayCell.swift in Sources */, 6BE7E91158F4DF1F71247C6D /* CellMark.swift in Sources */, CFCA3C2C3CF6D88AE844D7AD /* CellView.swift in Sources */, + B94919176DEC6EC31637B037 /* ClueList.swift in Sources */, DE9E4FAB098731A650F2D306 /* CrossmateApp.swift in Sources */, C30C0C4E54E4209A22843872 /* CrossmateModel.xcdatamodeld in Sources */, 978F91DBAE94BC5DA1D94705 /* DriveMonitor.swift in Sources */, diff --git a/Crossmate/Models/PlayerSession.swift b/Crossmate/Models/PlayerSession.swift @@ -80,6 +80,13 @@ final class PlayerSession { } } + func selectClue(direction: Puzzle.Direction, number: Int) { + guard let cell = puzzle.cell(numbered: number) else { return } + self.direction = direction + selectedRow = cell.row + selectedCol = cell.col + } + // MARK: - Clue navigation func goToNextClue() { diff --git a/Crossmate/Views/ClueList.swift b/Crossmate/Views/ClueList.swift @@ -0,0 +1,76 @@ +import SwiftUI + +struct ClueList: View { + @Bindable var session: PlayerSession + @Environment(PlayerPreferences.self) private var preferences + @Environment(\.dismiss) private var dismiss + + var body: some View { + let current = session.currentClue() + let currentDirection = session.direction + let currentID = current.map { rowID(direction: currentDirection, number: $0.number) } + + NavigationStack { + ScrollViewReader { proxy in + List { + Section("Across") { + ForEach(session.puzzle.acrossClues) { clue in + row(for: clue, direction: .across, current: current, currentDirection: currentDirection) + .id(rowID(direction: .across, number: clue.number)) + } + } + Section("Down") { + ForEach(session.puzzle.downClues) { clue in + row(for: clue, direction: .down, current: current, currentDirection: currentDirection) + .id(rowID(direction: .down, number: clue.number)) + } + } + } + .listStyle(.insetGrouped) + .onAppear { + guard let currentID else { return } + proxy.scrollTo(currentID, anchor: .center) + } + } + .navigationTitle("Clues") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Done") { dismiss() } + } + } + } + } + + private func rowID(direction: Puzzle.Direction, number: Int) -> String { + "\(direction == .across ? "A" : "D")-\(number)" + } + + @ViewBuilder + private func row( + for clue: Puzzle.Clue, + direction: Puzzle.Direction, + current: Puzzle.Clue?, + currentDirection: Puzzle.Direction + ) -> some View { + let isCurrent = current?.number == clue.number && currentDirection == direction + Button { + session.selectClue(direction: direction, number: clue.number) + dismiss() + } label: { + HStack(alignment: .firstTextBaseline, spacing: 10) { + Text("\(clue.number)") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.secondary) + .frame(minWidth: 28, alignment: .trailing) + Text(clue.text) + .font(.body) + .foregroundStyle(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .listRowBackground(isCurrent ? preferences.color.highlightFill : Color.clear) + } +} diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift @@ -213,6 +213,7 @@ private struct ClueBar: View { @Bindable var session: PlayerSession @Environment(PlayerPreferences.self) private var preferences @State private var slideEdge: Edge = .trailing + @State private var isShowingClueList = false private var playerColor: PlayerColor { preferences.color } @@ -254,7 +255,7 @@ private struct ClueBar: View { } .contentShape(Rectangle()) .onTapGesture { - session.toggleDirection() + isShowingClueList = true } Button { @@ -271,6 +272,11 @@ private struct ClueBar: View { .padding(.vertical, 12) .background(playerColor.highlightFill) .animation(.smooth(duration: 0.22), value: currentKey) + .sheet(isPresented: $isShowingClueList) { + ClueList(session: session) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) + } } private func label(for clue: Puzzle.Clue?) -> String {