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:
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