crossmate

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

commit a3e5c5d609b9298d20b47d4aa179e34d3eb67129
parent b8525fe9112789ca00fc17fa05eb1f9435e626ba
Author: Michael Camilleri <[email protected]>
Date:   Tue, 28 Apr 2026 14:11:42 +0900

Add CloudKit shares to a queue on launch

Diffstat:
MCrossmate/CrossmateApp.swift | 40++++++++++++++++++++++++++++++++++------
MCrossmate/Services/AppServices.swift | 28++++++++++++++++++++++++++--
MCrossmate/Views/SettingsView.swift | 43-------------------------------------------
3 files changed, 60 insertions(+), 51 deletions(-)

diff --git a/Crossmate/CrossmateApp.swift b/Crossmate/CrossmateApp.swift @@ -32,10 +32,6 @@ struct CrossmateApp: App { final class AppDelegate: NSObject, UIApplicationDelegate, @unchecked Sendable { var onRemoteNotification: (() async -> Void)? - var onAcceptShare: ((CKShare.Metadata) async -> Void)? { - didSet { flushPendingAcceptedShares() } - } - private var pendingAcceptedShares: [CKShare.Metadata] = [] func application( _ application: UIApplication, @@ -55,8 +51,31 @@ final class AppDelegate: NSObject, UIApplicationDelegate, @unchecked Sendable { func application( _ application: UIApplication, - userDidAcceptCloudKitShareWith metadata: CKShare.Metadata - ) { + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + let configuration = UISceneConfiguration( + name: nil, + sessionRole: connectingSceneSession.role + ) + configuration.delegateClass = SceneDelegate.self + return configuration + } +} + +@MainActor +final class CloudShareAcceptanceBroker { + static let shared = CloudShareAcceptanceBroker() + + var onAcceptShare: ((CKShare.Metadata) async -> Void)? { + didSet { flushPendingAcceptedShares() } + } + + private var pendingAcceptedShares: [CKShare.Metadata] = [] + + private init() {} + + func acceptCloudKitShare(_ metadata: CKShare.Metadata) { guard let onAcceptShare else { pendingAcceptedShares.append(metadata) return @@ -74,6 +93,15 @@ final class AppDelegate: NSObject, UIApplicationDelegate, @unchecked Sendable { } } +final class SceneDelegate: NSObject, UIWindowSceneDelegate { + func windowScene( + _ windowScene: UIWindowScene, + userDidAcceptCloudKitShareWith metadata: CKShare.Metadata + ) { + CloudShareAcceptanceBroker.shared.acceptCloudKitShare(metadata) + } +} + // MARK: - Root View struct RootView: View { diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift @@ -20,6 +20,9 @@ final class AppServices { private let ckContainer = CKContainer(identifier: "iCloud.net.inqk.crossmate.v2") private var started = false private var nameBroadcaster: NameBroadcaster? + private var isReadyForShareAcceptance = false + private var isProcessingShareAcceptanceQueue = false + private var pendingShareMetadatas: [CKShare.Metadata] = [] init() { self.persistence = PersistenceController() @@ -88,8 +91,8 @@ final class AppServices { appDelegate.onRemoteNotification = { await self.handleRemoteNotification() } - appDelegate.onAcceptShare = { metadata in - await self.cloudService.acceptShare(metadata: metadata) + CloudShareAcceptanceBroker.shared.onAcceptShare = { metadata in + await self.enqueueShareAcceptance(metadata) } await syncEngine.setTracer { [syncMonitor] message in @@ -132,6 +135,8 @@ final class AppServices { ) await syncEngine.start() + isReadyForShareAcceptance = true + await processPendingShareAcceptances() await syncMonitor.run("initial fetch") { try await syncEngine.fetchChanges() @@ -142,6 +147,14 @@ final class AppServices { await refreshSnapshot() } + func enqueueShareAcceptance(_ metadata: CKShare.Metadata) async { + pendingShareMetadatas.append(metadata) + syncMonitor.note( + "share acceptance queued: container=\(metadata.containerIdentifier)" + ) + await processPendingShareAcceptances() + } + func syncOnForeground() async { await moveBuffer.flush() await syncMonitor.run("foreground fetch") { @@ -174,6 +187,17 @@ final class AppServices { } } + private func processPendingShareAcceptances() async { + guard isReadyForShareAcceptance, !isProcessingShareAcceptanceQueue else { return } + isProcessingShareAcceptanceQueue = true + defer { isProcessingShareAcceptanceQueue = false } + + while !pendingShareMetadatas.isEmpty { + let metadata = pendingShareMetadatas.removeFirst() + await cloudService.acceptShare(metadata: metadata) + } + } + private func refreshSnapshot() async { let snapshot = await syncEngine.diagnosticSnapshot() syncMonitor.updateSnapshot(snapshot) diff --git a/Crossmate/Views/SettingsView.swift b/Crossmate/Views/SettingsView.swift @@ -7,8 +7,6 @@ struct SettingsView: View { @State private var showingNYTLogin = false @State private var showResetConfirmation = false - @State private var fetchResult: String? - @State private var isFetching = false var body: some View { NavigationStack { @@ -66,47 +64,6 @@ struct SettingsView: View { Button("Sign Out", role: .destructive) { nytAuth.signOut() } - Button { - isFetching = true - fetchResult = nil - let cookie = nytAuth.cookie - Task.detached { - let fetcher = NYTPuzzleFetcher { cookie } - do { - let xdSource = try await fetcher.fetchPuzzle(for: .now) - // Verify the XD parses correctly - let xd = try XD.parse(xdSource) - let title = xd.title ?? "Untitled" - let clueCount = xd.acrossClues.count + xd.downClues.count - await MainActor.run { - fetchResult = "Success: \"\(title)\" — \(clueCount) clues. Check console." - isFetching = false - } - } catch { - print("=== FETCH/PARSE ERROR ===") - print(error) - print("=== END ERROR ===") - await MainActor.run { - fetchResult = "Error: \(error.localizedDescription)" - isFetching = false - } - } - } - } label: { - HStack { - Text("Fetch Today's Puzzle") - if isFetching { - Spacer() - ProgressView() - } - } - } - .disabled(isFetching) - if let fetchResult { - Text(fetchResult) - .font(.caption) - .foregroundStyle(fetchResult.hasPrefix("Success") ? .green : .red) - } } @ViewBuilder