commit b8525fe9112789ca00fc17fa05eb1f9435e626ba
parent a1d4e680ad9511d79fb402403776a79ea9ea3b3d
Author: Michael Camilleri <[email protected]>
Date: Tue, 28 Apr 2026 12:46:35 +0900
Fix handling of shared games from links
Following a link to a shared game launches Crossmate but does not
successfully add the game to the player's list of in-progress games.
This commit attempts to fix that.
Co-Authored-By: Codex GPT 5.5 <[email protected]>
Diffstat:
3 files changed, 64 insertions(+), 2 deletions(-)
diff --git a/Crossmate/CrossmateApp.swift b/Crossmate/CrossmateApp.swift
@@ -32,7 +32,10 @@ struct CrossmateApp: App {
final class AppDelegate: NSObject, UIApplicationDelegate, @unchecked Sendable {
var onRemoteNotification: (() async -> Void)?
- var onAcceptShare: ((CKShare.Metadata) async -> Void)?
+ var onAcceptShare: ((CKShare.Metadata) async -> Void)? {
+ didSet { flushPendingAcceptedShares() }
+ }
+ private var pendingAcceptedShares: [CKShare.Metadata] = []
func application(
_ application: UIApplication,
@@ -54,7 +57,20 @@ final class AppDelegate: NSObject, UIApplicationDelegate, @unchecked Sendable {
_ application: UIApplication,
userDidAcceptCloudKitShareWith metadata: CKShare.Metadata
) {
- Task { await onAcceptShare?(metadata) }
+ guard let onAcceptShare else {
+ pendingAcceptedShares.append(metadata)
+ return
+ }
+ Task { await onAcceptShare(metadata) }
+ }
+
+ private func flushPendingAcceptedShares() {
+ guard let onAcceptShare, !pendingAcceptedShares.isEmpty else { return }
+ let metadatas = pendingAcceptedShares
+ pendingAcceptedShares.removeAll()
+ for metadata in metadatas {
+ Task { await onAcceptShare(metadata) }
+ }
}
}
@@ -93,6 +109,13 @@ struct RootView: View {
navigationPath.append(id)
}
}
+ .onReceive(NotificationCenter.default.publisher(for: .cloudShareAcceptanceStarted)) { _ in
+ UIApplication.shared.dismissPresentedViewControllers()
+ }
+ .onReceive(NotificationCenter.default.publisher(for: .cloudShareAcceptanceCompleted)) { notification in
+ guard let gameID = notification.userInfo?["gameID"] as? UUID else { return }
+ navigationPath.append(gameID)
+ }
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .active:
@@ -106,6 +129,17 @@ struct RootView: View {
}
}
+private extension UIApplication {
+ func dismissPresentedViewControllers() {
+ for scene in connectedScenes {
+ guard let windowScene = scene as? UIWindowScene else { continue }
+ for window in windowScene.windows where window.isKeyWindow {
+ window.rootViewController?.dismiss(animated: true)
+ }
+ }
+ }
+}
+
// MARK: - Game Destination
/// Loads a game when navigated to.
diff --git a/Crossmate/Persistence/GameStore.swift b/Crossmate/Persistence/GameStore.swift
@@ -336,6 +336,18 @@ final class GameStore {
return (try? context.fetch(request).first?.id)
}
+ /// Returns joined CloudKit-share games that have a usable puzzle payload.
+ /// Placeholders created from shared-zone discovery are intentionally
+ /// excluded until the root Game record has arrived.
+ func joinedSharedGameIDs() -> Set<UUID> {
+ let request = NSFetchRequest<GameEntity>(entityName: "GameEntity")
+ request.predicate = NSPredicate(
+ format: "databaseScope == 1 AND puzzleSource != nil AND puzzleSource != %@",
+ ""
+ )
+ return Set(((try? context.fetch(request)) ?? []).compactMap(\.id))
+ }
+
// MARK: - Create a new game
/// Creates a new game from XD source text. Returns the new game's UUID.
diff --git a/Crossmate/Services/CloudService.swift b/Crossmate/Services/CloudService.swift
@@ -1,5 +1,10 @@
import CloudKit
+extension Notification.Name {
+ static let cloudShareAcceptanceStarted = Notification.Name("cloudShareAcceptanceStarted")
+ static let cloudShareAcceptanceCompleted = Notification.Name("cloudShareAcceptanceCompleted")
+}
+
@MainActor
final class CloudService {
private let ckContainer: CKContainer
@@ -20,6 +25,8 @@ final class CloudService {
}
func acceptShare(metadata: CKShare.Metadata) async {
+ NotificationCenter.default.post(name: .cloudShareAcceptanceStarted, object: nil)
+
guard metadata.containerIdentifier == ckContainer.containerIdentifier else {
syncMonitor.note(
"acceptShare: container mismatch — metadata=\(metadata.containerIdentifier) " +
@@ -27,6 +34,7 @@ final class CloudService {
)
return
}
+ let existingJoinedGameIDs = store.joinedSharedGameIDs()
do {
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
let op = CKAcceptSharesOperation(shareMetadatas: [metadata])
@@ -37,6 +45,14 @@ final class CloudService {
await syncMonitor.run("share-accept fetch") {
try await syncEngine.fetchChanges()
}
+ let joinedGameID = store.joinedSharedGameIDs()
+ .subtracting(existingJoinedGameIDs)
+ .first
+ NotificationCenter.default.post(
+ name: .cloudShareAcceptanceCompleted,
+ object: nil,
+ userInfo: joinedGameID.map { ["gameID": $0] }
+ )
} catch {
syncMonitor.recordError("acceptShare", error)
}