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 }