crossmate

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

commit c23765286a5321f8029d158d3c9202a47a3a528e
parent a330dabc8067d55568ce9a26a1b747b88bfe2fdb
Author: Michael Camilleri <[email protected]>
Date:   Wed,  8 Jul 2026 13:09:43 +0900

Extract Clue List rows into view types

The Clue List was using helper builders for its sheet and sidebar rows,
so both presentations carried separate row and section-header structures
inside the parent view. That kept the repeated clue rows inside the same
invalidation boundary as the scroll and selection plumbing.

This commit moves the shared row and header rendering into small private
view types while leaving clue selection, sheet dismissal, and
scroll-to-current behaviour in ClueList. The sheet and sidebar still
keep their different padding and highlight chrome, but the repeated clue
content now has a narrower view surface.

Co-Authored-By: Codex GPT 5.5 <[email protected]>

Diffstat:
MCrossmate/Views/Puzzle/ClueList.swift | 156++++++++++++++++++++++++++++++++++++++++++--------------------------------------
1 file changed, 82 insertions(+), 74 deletions(-)

diff --git a/Crossmate/Views/Puzzle/ClueList.swift b/Crossmate/Views/Puzzle/ClueList.swift @@ -70,15 +70,15 @@ struct ClueList: View { ) -> some View { ScrollViewReader { proxy in List { - headingRow("Across") + ClueSectionHeader(title: "Across", presentation: .sheet) ForEach(session.puzzle.acrossClues) { clue in - row(for: clue, direction: .across, current: current, currentDirection: currentDirection) + rowButton(for: clue, direction: .across, current: current, currentDirection: currentDirection) .id(rowID(direction: .across, number: clue.number)) } - headingRow("Down") + ClueSectionHeader(title: "Down", presentation: .sheet) ForEach(session.puzzle.downClues) { clue in - row(for: clue, direction: .down, current: current, currentDirection: currentDirection) + rowButton(for: clue, direction: .down, current: current, currentDirection: currentDirection) .id(rowID(direction: .down, number: clue.number)) } } @@ -99,18 +99,6 @@ struct ClueList: View { } } - private func headingRow(_ title: String) -> some View { - Text(title) - .font(.footnote.weight(.semibold)) - .foregroundStyle(.secondary) - .textCase(.uppercase) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.leading, 12) - .padding(.vertical, 6) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowBackground(Color.clear) - } - private func sidebarList( current: Puzzle.Clue?, currentDirection: Puzzle.Direction, @@ -119,16 +107,16 @@ struct ClueList: View { ScrollViewReader { proxy in ScrollView { VStack(alignment: .leading, spacing: 0) { - sidebarSectionHeader("Across") + ClueSectionHeader(title: "Across", presentation: .sidebar) ForEach(session.puzzle.acrossClues) { clue in - sidebarRow(for: clue, direction: .across, current: current, currentDirection: currentDirection) + rowButton(for: clue, direction: .across, current: current, currentDirection: currentDirection) .id(rowID(direction: .across, number: clue.number)) } - sidebarSectionHeader("Down") + ClueSectionHeader(title: "Down", presentation: .sidebar) .padding(.top, 12) ForEach(session.puzzle.downClues) { clue in - sidebarRow(for: clue, direction: .down, current: current, currentDirection: currentDirection) + rowButton(for: clue, direction: .down, current: current, currentDirection: currentDirection) .id(rowID(direction: .down, number: clue.number)) } } @@ -147,44 +135,6 @@ struct ClueList: View { } } - private func sidebarSectionHeader(_ title: String) -> some View { - Text(title) - .font(.footnote.weight(.semibold)) - .foregroundStyle(.secondary) - .textCase(.uppercase) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 18) - .padding(.vertical, 6) - } - - private func sidebarRow( - for clue: Puzzle.Clue, - direction: Puzzle.Direction, - current: Puzzle.Clue?, - currentDirection: Puzzle.Direction - ) -> some View { - let isCurrent = current?.number == clue.number && currentDirection == direction - return Button { - session.selectClue(direction: direction, number: clue.number) - } label: { - HStack(alignment: .firstTextBaseline, spacing: 10) { - Text("\(clue.number)") - .font(.subheadline.weight(.semibold)) - .foregroundStyle(.secondary) - .frame(minWidth: 28, alignment: .trailing) - Text(clue.attributedText) - .font(.body) - .foregroundStyle(.primary) - .frame(maxWidth: .infinity, alignment: .leading) - } - .padding(.horizontal, 18) - .padding(.vertical, 8) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .background(isCurrent ? currentClueBackground : Color.clear) - } - private func rowID(direction: Puzzle.Direction, number: Int) -> String { "\(direction == .across ? "A" : "D")-\(number)" } @@ -210,7 +160,7 @@ struct ClueList: View { } @ViewBuilder - private func row( + private func rowButton( for clue: Puzzle.Clue, direction: Puzzle.Direction, current: Puzzle.Clue?, @@ -223,22 +173,80 @@ struct ClueList: View { dismiss() } } label: { - HStack(alignment: .firstTextBaseline, spacing: 10) { - Text("\(clue.number)") - .font(.subheadline.weight(.semibold)) - .foregroundStyle(.secondary) - .frame(minWidth: 28, alignment: .trailing) - Text(clue.attributedText) - .font(.body) - .foregroundStyle(.primary) - .frame(maxWidth: .infinity, alignment: .leading) - } - .padding(.leading, 12) - .padding(.vertical, 10) - .contentShape(Rectangle()) + ClueRow( + number: clue.number, + text: clue.attributedText, + isCurrent: isCurrent, + currentBackground: currentClueBackground, + presentation: presentation + ) } .buttonStyle(.plain) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowBackground(isCurrent ? currentClueBackground : Color.clear) + .modifier(SheetClueListRowChrome( + isActive: presentation == .sheet, + background: isCurrent ? currentClueBackground : .clear + )) + } +} + +private struct ClueSectionHeader: View { + let title: String + let presentation: ClueList.Presentation + + var body: some View { + Text(title) + .font(.footnote.weight(.semibold)) + .foregroundStyle(.secondary) + .textCase(.uppercase) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, presentation == .sheet ? 12 : 18) + .padding(.trailing, presentation == .sheet ? 0 : 18) + .padding(.vertical, 6) + .modifier(SheetClueListRowChrome(isActive: presentation == .sheet, background: .clear)) + } +} + +private struct ClueRow: View { + let number: Int + let text: AttributedString + let isCurrent: Bool + let currentBackground: Color + let presentation: ClueList.Presentation + + private var background: Color { + isCurrent ? currentBackground : .clear + } + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 10) { + Text("\(number)") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.secondary) + .frame(minWidth: 28, alignment: .trailing) + Text(text) + .font(.body) + .foregroundStyle(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.leading, presentation == .sheet ? 12 : 18) + .padding(.trailing, presentation == .sheet ? 0 : 18) + .padding(.vertical, presentation == .sheet ? 10 : 8) + .contentShape(Rectangle()) + .background(presentation == .sidebar ? background : .clear) + } +} + +private struct SheetClueListRowChrome: ViewModifier { + let isActive: Bool + let background: Color + + func body(content: Content) -> some View { + if isActive { + content + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowBackground(background) + } else { + content + } } }