commit 36dac94cc745eef10c4a10cc04c71499695ca350
parent d1cd9156fb537f31b4be4cef04bc528605e207a9
Author: Michael Camilleri <[email protected]>
Date: Fri, 1 May 2026 13:35:29 +0900
Centre grid in puzzle view
This commit attemps to centre the crossword grid in the space between
the navigation toolbar and the keyboard. This is particularly important
for larger phones where the previous arrangement resulted in the
placement feeling top heavy.
Co-Authored-By: Codex GPT 5.5 <[email protected]>
Diffstat:
1 file changed, 119 insertions(+), 47 deletions(-)
diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift
@@ -15,6 +15,7 @@ struct PuzzleView: View {
@State private var leaveError: String?
@State private var isRevokedBannerDismissed = false
@State private var isShowingShareSheet = false
+ @ScaledMetric(relativeTo: .headline) private var puzzleTitleOffset = 18
private func swatchImage(for color: PlayerColor) -> Image {
let tint = UIColor(color.tint)
@@ -45,23 +46,9 @@ struct PuzzleView: View {
VStack(spacing: 0) {
ZStack {
VStack(spacing: 0) {
- VStack(spacing: 2) {
- Text(titleParts.title)
- .font(.headline)
- .lineLimit(2)
- if let subtitle = titleParts.subtitle {
- Text(subtitle)
- .font(.subheadline)
- .foregroundStyle(.secondary)
- .lineLimit(1)
- }
- }
- .multilineTextAlignment(.center)
- .frame(maxWidth: .infinity)
- .padding(.horizontal)
- .padding(.bottom, 12)
+ PuzzleTitle(title: titleParts.title, subtitle: titleParts.subtitle)
GridView(session: session, roster: roster)
- Spacer(minLength: 12)
+ .padding(.bottom, puzzleTitleOffset)
}
if session.isRebusActive {
@@ -79,11 +66,11 @@ struct PuzzleView: View {
}
VStack(spacing: 0) {
- ClueBar(session: session)
+ ClueBarSlot(session: session)
KeyboardView(session: session)
.disabled(isSolved)
+ .background(Color(.systemGroupedBackground))
}
- .background(Color(.systemGroupedBackground))
}
.overlay(alignment: .top) {
if session.mutator.isAccessRevoked && !isRevokedBannerDismissed {
@@ -287,43 +274,82 @@ struct PuzzleView: View {
}
}
+private struct PuzzleTitle: View {
+ let title: String
+ let subtitle: String?
+
+ var body: some View {
+ VStack(spacing: 2) {
+ Text(title)
+ .font(.headline)
+ .lineLimit(2)
+ if let subtitle {
+ Text(subtitle)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ }
+ }
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: .infinity)
+ .padding(.horizontal)
+ .padding(.bottom, 12)
+ }
+}
+
private struct ClueKey: Hashable {
let direction: Puzzle.Direction
let number: Int
}
-private struct ClueBar: View {
+private struct ClueBarSlot: 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 }
+ var body: some View {
+ ZStack(alignment: .bottom) {
+ ClueBarReservation()
+ .opacity(0)
+ .accessibilityHidden(true)
+ .allowsHitTesting(false)
+
+ ClueBar(session: session)
+ }
+ }
+}
+private struct ClueBarReservation: View {
var body: some View {
- let clue = session.currentClue()
- let currentKey = clue.map { ClueKey(direction: session.direction, number: $0.number) }
+ ClueBarContent(
+ label: "99 Across",
+ clueText: "Clue reservation",
+ reservesClueSpace: true
+ )
+ }
+}
+
+private struct ClueBarContent: View {
+ let label: String
+ let clueText: String
+ var reservesClueSpace = false
+ var currentKey: ClueKey?
+ var slideEdge: Edge = .trailing
+ var onPrevious: (() -> Void)?
+ var onNext: (() -> Void)?
+ var onClueTap: (() -> Void)?
+ var body: some View {
HStack(alignment: .clueCenter, spacing: 12) {
- Button {
- slideEdge = .leading
- session.goToPreviousClue()
- } label: {
- Image(systemName: "chevron.left")
- .font(.title3.weight(.semibold))
- .frame(width: 32, height: 32)
- }
- .buttonStyle(.plain)
+ ClueBarIcon(systemName: "chevron.left", action: onPrevious)
VStack(alignment: .leading, spacing: 4) {
- Text(label(for: clue))
+ Text(label)
.font(.caption)
.textCase(.uppercase)
.foregroundStyle(.secondary)
ZStack(alignment: .leading) {
- Text(clue?.text ?? "—")
+ Text(clueText)
.font(.headline)
- .lineLimit(2)
+ .lineLimit(2, reservesSpace: reservesClueSpace)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
.id(currentKey)
@@ -338,21 +364,67 @@ private struct ClueBar: View {
}
.contentShape(Rectangle())
.onTapGesture {
- isShowingClueList = true
+ onClueTap?()
}
- Button {
- slideEdge = .trailing
- session.goToNextClue()
- } label: {
- Image(systemName: "chevron.right")
- .font(.title3.weight(.semibold))
- .frame(width: 32, height: 32)
- }
- .buttonStyle(.plain)
+ ClueBarIcon(systemName: "chevron.right", action: onNext)
}
.padding(.horizontal, 12)
.padding(.vertical, 12)
+ }
+}
+
+private struct ClueBarIcon: View {
+ let systemName: String
+ var action: (() -> Void)?
+
+ var body: some View {
+ if let action {
+ Button(action: action) {
+ icon
+ }
+ .buttonStyle(.plain)
+ } else {
+ icon
+ }
+ }
+
+ private var icon: some View {
+ Image(systemName: systemName)
+ .font(.title3.weight(.semibold))
+ .frame(width: 32, height: 32)
+ }
+}
+
+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 }
+
+ var body: some View {
+ let clue = session.currentClue()
+ let currentKey = clue.map { ClueKey(direction: session.direction, number: $0.number) }
+
+ ClueBarContent(
+ label: label(for: clue),
+ clueText: clue?.text ?? "—",
+ currentKey: currentKey,
+ slideEdge: slideEdge,
+ onPrevious: {
+ slideEdge = .leading
+ session.goToPreviousClue()
+ },
+ onNext: {
+ slideEdge = .trailing
+ session.goToNextClue()
+ },
+ onClueTap: {
+ isShowingClueList = true
+ }
+ )
.background(playerColor.highlightFill)
.animation(.smooth(duration: 0.22), value: currentKey)
.sheet(isPresented: $isShowingClueList) {