GameShareItem.swift (5723B)
1 import Foundation 2 import SwiftUI 3 4 struct GameShareSheet: View { 5 let gameID: UUID 6 let title: String 7 let shareController: ShareController 8 9 @Environment(\.dismiss) private var dismiss 10 @State private var shareURL: URL? 11 @State private var errorMessage: String? 12 @State private var isLoadingExistingLink = true 13 @State private var isCreating = false 14 @State private var didLoadExistingLink = false 15 @State private var didCopy = false 16 17 var body: some View { 18 NavigationStack { 19 List { 20 VStack(spacing: 18) { 21 Image(systemName: "flag.pattern.checkered.2.crossed") 22 .font(.system(size: 58, weight: .semibold)) 23 .symbolRenderingMode(.hierarchical) 24 .foregroundStyle(Color.accentColor) 25 .frame(width: 88, height: 88) 26 .accessibilityHidden(true) 27 28 VStack(spacing: 12) { 29 Text("Share via iCloud") 30 .font(.title3.weight(.semibold)) 31 .multilineTextAlignment(.center) 32 .fixedSize(horizontal: false, vertical: true) 33 34 Text("Any iCloud user with the link will be able to join your game and collaborate.") 35 .font(.body) 36 .multilineTextAlignment(.center) 37 .fixedSize(horizontal: false, vertical: true) 38 39 Text("Crossmate syncs games using iCloud. Please be aware that your player name is shared with other players. Players can work on the same puzzle simultaneously or at different times.") 40 .font(.footnote) 41 .foregroundStyle(.secondary) 42 .multilineTextAlignment(.center) 43 .fixedSize(horizontal: false, vertical: true) 44 } 45 } 46 .frame(maxWidth: .infinity) 47 .padding(.horizontal, 8) 48 .padding(.vertical, 4) 49 .listRowInsets(EdgeInsets()) 50 .listRowBackground(Color.clear) 51 52 Section { 53 if let shareURL { 54 Button { 55 UIPasteboard.general.string = shareURL.absoluteString 56 didCopy = true 57 } label: { 58 Label(didCopy ? "Copied" : "Copy Link", systemImage: didCopy ? "checkmark" : "doc.on.doc") 59 } 60 61 ShareLink(item: shareURL) { 62 Label("Send Link", systemImage: "square.and.arrow.up") 63 } 64 } else if isLoadingExistingLink { 65 HStack { 66 Label("Checking Link", systemImage: "link") 67 Spacer() 68 ProgressView() 69 } 70 } else { 71 Button { 72 Task { await createLink() } 73 } label: { 74 HStack { 75 Label("Create Link", systemImage: "link") 76 if isCreating { 77 Spacer() 78 ProgressView() 79 } 80 } 81 } 82 .disabled(isCreating || isLoadingExistingLink) 83 } 84 } 85 86 if let errorMessage { 87 Section("Error") { 88 Text(errorMessage) 89 .font(.caption.monospaced()) 90 .foregroundStyle(.red) 91 .textSelection(.enabled) 92 Button { 93 UIPasteboard.general.string = errorMessage 94 } label: { 95 Label("Copy Error", systemImage: "doc.on.doc") 96 } 97 } 98 } 99 } 100 .navigationTitle("Share Game") 101 .navigationBarTitleDisplayMode(.inline) 102 .toolbar { 103 ToolbarItem(placement: .cancellationAction) { 104 Button("Done") { dismiss() } 105 } 106 } 107 .task { 108 await loadExistingLink() 109 } 110 } 111 } 112 113 private func loadExistingLink() async { 114 guard !didLoadExistingLink else { return } 115 didLoadExistingLink = true 116 isLoadingExistingLink = true 117 errorMessage = nil 118 defer { isLoadingExistingLink = false } 119 120 do { 121 shareURL = try await shareController.existingShareLink(for: gameID) 122 } catch { 123 errorMessage = describe(error) 124 } 125 } 126 127 private func createLink() async { 128 guard !isCreating, !isLoadingExistingLink else { return } 129 isCreating = true 130 didCopy = false 131 errorMessage = nil 132 defer { isCreating = false } 133 134 do { 135 shareURL = try await shareController.createShareLink(for: gameID) 136 } catch { 137 errorMessage = describe(error) 138 } 139 } 140 141 private func describe(_ error: Error) -> String { 142 let nsError = error as NSError 143 let userInfo = nsError.userInfo 144 .map { "\($0.key)=\($0.value)" } 145 .joined(separator: " | ") 146 return "domain=\(nsError.domain) code=\(nsError.code) \(nsError.localizedDescription)\n\(userInfo)" 147 } 148 }