crossmate

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

JoiningPuzzleView.swift (3249B)


      1 import SwiftUI
      2 
      3 /// The placeholder shown the instant a share link is tapped, while the share
      4 /// is being accepted and its zone fetched. When the link carried a grid
      5 /// silhouette it paints that grid greyed-out — so the user immediately sees
      6 /// the shape of the puzzle they're joining instead of an empty screen — with a
      7 /// spinner underneath. With no silhouette it falls back to the spinner alone.
      8 struct JoiningPuzzleView: View {
      9     let shape: GridSilhouette.Grid?
     10     /// Cancels the in-flight join and dismisses this screen. The view is a
     11     /// full-screen overlay over the navigation stack, so without this the user
     12     /// would have no way out while a slow (or stalled) join is in progress.
     13     var onCancel: () -> Void
     14 
     15     /// Seconds the view has been on screen. The join has no observable stages
     16     /// to drive the message (only coarse start/complete signals exist), so the
     17     /// text advances purely on elapsed time to reassure the user that work is
     18     /// still happening rather than stalled.
     19     @State private var elapsed = 0
     20 
     21     /// Seconds before the Cancel button appears, so a fast join — the common
     22     /// case — completes without ever flashing it.
     23     private static let cancelRevealDelay = 4
     24 
     25     private var message: String {
     26         switch elapsed {
     27         case ..<5:  return "Joining puzzle…"
     28         case ..<10: return "Syncing with iCloud…"
     29         case ..<15: return "Accepting invitation…"
     30         default:    return "Waiting for response…"
     31         }
     32     }
     33 
     34     var body: some View {
     35         ZStack {
     36             Color(.systemBackground).ignoresSafeArea()
     37 
     38             VStack(spacing: 28) {
     39                 if let shape {
     40                     GridThumbnailView(
     41                         width: shape.width,
     42                         height: shape.height,
     43                         // Open cells render grey (not white) to read as
     44                         // "not editable yet" rather than a playable grid.
     45                         cells: shape.blocks.map { $0 ? .block : .filled },
     46                         size: 220
     47                     )
     48                     .opacity(0.55)
     49                     .accessibilityHidden(true)
     50                 }
     51 
     52                 VStack(spacing: 12) {
     53                     ProgressView()
     54                     Text(message)
     55                         .font(.headline)
     56                         .foregroundStyle(.secondary)
     57                         .animation(.default, value: message)
     58                 }
     59             }
     60 
     61             if elapsed >= Self.cancelRevealDelay {
     62                 VStack {
     63                     Spacer()
     64                     Button("Cancel", role: .cancel, action: onCancel)
     65                         .buttonStyle(.bordered)
     66                         .padding(.bottom, 40)
     67                 }
     68                 .transition(.opacity)
     69             }
     70         }
     71         .animation(.default, value: elapsed >= Self.cancelRevealDelay)
     72         .accessibilityElement(children: .combine)
     73         .accessibilityLabel("Joining puzzle")
     74         .task {
     75             // Cancelled automatically when the placeholder clears on join.
     76             while !Task.isCancelled {
     77                 try? await Task.sleep(for: .seconds(1))
     78                 elapsed += 1
     79             }
     80         }
     81     }
     82 }