crossmate

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

commit decd060e26895c988ee02ef08ede9e90da608e1a
parent f81539b62cec3dc2d9707b8e6d7cb36b0e70ec82
Author: Michael Camilleri <[email protected]>
Date:   Thu,  2 Jul 2026 22:19:28 +0900

Surface leave failures on the Game List

Leaving a shared puzzle from the Game List swipe action could fail
silently: the error was captured into a leaveError state property that
no view ever read, so the puzzle simply stayed in the list with no
explanation. The failure now posts an error Announcement in the same
single-slot banner the list's other destructive actions (decline,
resign, delete) already use.

This commit also drops the GeometryReader that wrapped the entire list
just to read its height for the roomier-type check. The height now
arrives through onGeometryChange into a containerHeight state property
and usesRoomierType became a computed property over it, so the check
behaves as before without a layout-wrapping container.

Co-Authored-By: Claude Fable 5 <[email protected]>

Diffstat:
MCrossmate/Views/GameList/GameListView.swift | 71++++++++++++++++++++++++++++++++++++++++-------------------------------
1 file changed, 40 insertions(+), 31 deletions(-)

diff --git a/Crossmate/Views/GameList/GameListView.swift b/Crossmate/Views/GameList/GameListView.swift @@ -58,7 +58,6 @@ struct GameListView: View { @State private var deleteTarget: GameSummary? @State private var resignTarget: GameSummary? @State private var leaveTarget: GameSummary? - @State private var leaveError: Error? @State private var showingNamePrompt = false @State private var nameDraft = "" @State private var summaryCache = GameSummaryCache() @@ -67,36 +66,40 @@ struct GameListView: View { /// seconds after a tip is dismissed; cleared by the timer or by tapping it. @State private var showTipOptOut = false @State private var tipOptOutHideTask: Task<Void, Never>? + @State private var containerHeight: CGFloat = 0 private static let completedPageSize = 7 var body: some View { - GeometryReader { geometry in - VStack(spacing: 0) { - if let announcement = announcements.currentGlobal() { - AnnouncementBanner(announcement: announcement) { - dismissAnnouncement(announcement) - } - .padding(.horizontal) - .padding(.top, 8) - .transition(.move(edge: .top).combined(with: .opacity)) - } else if showTipOptOut { - Button("Never Show Tips") { - tipOptOutHideTask?.cancel() - tips.disable() - showTipOptOut = false - } - .buttonStyle(.borderedProminent) - .buttonBorderShape(.capsule) - .controlSize(.small) - .padding(.top, 8) - .transition(.opacity) + VStack(spacing: 0) { + if let announcement = announcements.currentGlobal() { + AnnouncementBanner(announcement: announcement) { + dismissAnnouncement(announcement) } - content(usesRoomierType: usesRoomierType(for: geometry.size)) + .padding(.horizontal) + .padding(.top, 8) + .transition(.move(edge: .top).combined(with: .opacity)) + } else if showTipOptOut { + Button("Never Show Tips") { + tipOptOutHideTask?.cancel() + tips.disable() + showTipOptOut = false + } + .buttonStyle(.borderedProminent) + .buttonBorderShape(.capsule) + .controlSize(.small) + .padding(.top, 8) + .transition(.opacity) } - .background(Color(.systemGroupedBackground)) - .animation(.easeInOut(duration: 0.3), value: announcements.currentGlobal()) - .animation(.easeInOut(duration: 0.3), value: showTipOptOut) + content(usesRoomierType: usesRoomierType) + } + .background(Color(.systemGroupedBackground)) + .animation(.easeInOut(duration: 0.3), value: announcements.currentGlobal()) + .animation(.easeInOut(duration: 0.3), value: showTipOptOut) + .onGeometryChange(for: CGFloat.self) { proxy in + proxy.size.height + } action: { height in + containerHeight = height } .navigationTitle("") .navigationBarTitleDisplayMode(.inline) @@ -727,7 +730,7 @@ struct GameListView: View { private static let newGameInviteErrorID = "new-game-friend-invite-error" /// Single-slot id for game-list destructive-action failures (decline, - /// resign, delete) — a fresh failure replaces the prior one. + /// resign, delete, leave) — a fresh failure replaces the prior one. private static let destructiveActionErrorID = "game-list-destructive-action-error" private func decline(_ invite: InviteEntity) async { @@ -775,16 +778,22 @@ struct GameListView: View { } private func leaveShare(game: GameSummary) async { + defer { leaveTarget = nil } do { try await shareController.leaveShare(gameID: game.id) - leaveTarget = nil } catch { - leaveError = error - leaveTarget = nil + announcements.post(Announcement( + id: Self.destructiveActionErrorID, + scope: .global, + severity: .error, + title: "Leaving Failed", + body: error.localizedDescription, + dismissal: .manual + )) } } - private func usesRoomierType(for size: CGSize) -> Bool { - size.height >= 760 && dynamicTypeSize <= .medium + private var usesRoomierType: Bool { + containerHeight >= 760 && dynamicTypeSize <= .medium } }