JoiningPuzzleView.swift (2394B)
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 11 /// Seconds the view has been on screen. The join has no observable stages 12 /// to drive the message (only coarse start/complete signals exist), so the 13 /// text advances purely on elapsed time to reassure the user that work is 14 /// still happening rather than stalled. 15 @State private var elapsed = 0 16 17 private var message: String { 18 switch elapsed { 19 case ..<5: return "Joining puzzle…" 20 case ..<10: return "Syncing with iCloud…" 21 case ..<15: return "Accepting invitation…" 22 default: return "Waiting for response…" 23 } 24 } 25 26 var body: some View { 27 ZStack { 28 Color(.systemBackground).ignoresSafeArea() 29 30 VStack(spacing: 28) { 31 if let shape { 32 GridThumbnailView( 33 width: shape.side, 34 height: shape.side, 35 // Open cells render grey (not white) to read as 36 // "not editable yet" rather than a playable grid. 37 cells: shape.blocks.map { $0 ? .block : .filled }, 38 size: 220 39 ) 40 .opacity(0.55) 41 .accessibilityHidden(true) 42 } 43 44 VStack(spacing: 12) { 45 ProgressView() 46 Text(message) 47 .font(.headline) 48 .foregroundStyle(.secondary) 49 .animation(.default, value: message) 50 } 51 } 52 } 53 .accessibilityElement(children: .combine) 54 .accessibilityLabel("Joining puzzle") 55 .task { 56 // Cancelled automatically when the placeholder clears on join. 57 while !Task.isCancelled { 58 try? await Task.sleep(for: .seconds(1)) 59 elapsed += 1 60 } 61 } 62 } 63 }