crossmate

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

commit 8d6a819380ddc268c111ae085f31bd6c0a8ed878
parent 9420b8be60df96208558b6973e97274780937844
Author: Michael Camilleri <[email protected]>
Date:   Tue, 28 Apr 2026 11:25:55 +0900

Use existing link in share sheet

If a user has already shared a game via iCloud, Crossmate should
recognise that and reuse the existing URL. This commit makes changes to
accomplish that.

Co-Authored-By: Co-Authored-By: Codex GPT 5.5 <[email protected]>

Diffstat:
MCrossmate/Sync/ShareController.swift | 31+++++++++++++++++++++++++++++++
MCrossmate/Views/GameShareItem.swift | 29+++++++++++++++++++++++++++--
2 files changed, 58 insertions(+), 2 deletions(-)

diff --git a/Crossmate/Sync/ShareController.swift b/Crossmate/Sync/ShareController.swift @@ -68,6 +68,37 @@ final class ShareController { } } + /// Returns the saved public share URL for a game, if Crossmate already + /// knows about its `CKShare`. Stale local share references are cleared so + /// the caller can safely offer to create a fresh link. + func existingShareLink(for gameID: UUID) async throws -> URL? { + let ctx = persistence.viewContext + let request = NSFetchRequest<GameEntity>(entityName: "GameEntity") + request.predicate = NSPredicate(format: "id == %@", gameID as CVarArg) + request.fetchLimit = 1 + guard let entity = try ctx.fetch(request).first else { + throw ShareError.gameNotFound + } + guard entity.databaseScope == 0 else { + throw ShareError.notAnOwner + } + guard let existingName = entity.ckShareRecordName else { + return nil + } + + do { + let share = try await fetchExistingShare( + recordName: existingName, + zoneName: entity.ckZoneName ?? "game-\(gameID.uuidString)" + ) + return share.url + } catch let error as CKError where error.code == .unknownItem { + entity.ckShareRecordName = nil + try ctx.save() + return nil + } + } + private func prepareShareRecord( for gameID: UUID, publicPermission: CKShare.ParticipantPermission diff --git a/Crossmate/Views/GameShareItem.swift b/Crossmate/Views/GameShareItem.swift @@ -9,7 +9,9 @@ struct GameShareSheet: View { @Environment(\.dismiss) private var dismiss @State private var shareURL: URL? @State private var errorMessage: String? + @State private var isLoadingExistingLink = false @State private var isCreating = false + @State private var didLoadExistingLink = false @State private var didCopy = false var body: some View { @@ -59,6 +61,12 @@ struct GameShareSheet: View { ShareLink(item: shareURL) { Label("Send Link", systemImage: "square.and.arrow.up") } + } else if isLoadingExistingLink { + HStack { + Label("Checking Link", systemImage: "link") + Spacer() + ProgressView() + } } else { Button { Task { await createLink() } @@ -71,7 +79,7 @@ struct GameShareSheet: View { } } } - .disabled(isCreating) + .disabled(isCreating || isLoadingExistingLink) } } @@ -96,11 +104,28 @@ struct GameShareSheet: View { Button("Done") { dismiss() } } } + .task { + await loadExistingLink() + } + } + } + + private func loadExistingLink() async { + guard !didLoadExistingLink else { return } + didLoadExistingLink = true + isLoadingExistingLink = true + errorMessage = nil + defer { isLoadingExistingLink = false } + + do { + shareURL = try await shareController.existingShareLink(for: gameID) + } catch { + errorMessage = describe(error) } } private func createLink() async { - guard !isCreating else { return } + guard !isCreating, !isLoadingExistingLink else { return } isCreating = true didCopy = false errorMessage = nil