CloudService.swift (4203B)
1 import CloudKit 2 3 extension Notification.Name { 4 static let cloudShareAcceptanceStarted = Notification.Name("cloudShareAcceptanceStarted") 5 static let cloudShareAcceptanceCompleted = Notification.Name("cloudShareAcceptanceCompleted") 6 } 7 8 @MainActor 9 final class CloudService { 10 private let ckContainer: CKContainer 11 private let syncEngine: SyncEngine 12 private let syncMonitor: SyncMonitor 13 private let store: GameStore 14 15 /// Fired after a successful share acceptance once the shared zone has been 16 /// fetched. Used to enqueue a `.join` ping so other collaborators are 17 /// notified that someone has joined the puzzle. 18 var onShareJoined: ((UUID) async -> Void)? 19 20 init( 21 container: CKContainer, 22 syncEngine: SyncEngine, 23 syncMonitor: SyncMonitor, 24 store: GameStore 25 ) { 26 self.ckContainer = container 27 self.syncEngine = syncEngine 28 self.syncMonitor = syncMonitor 29 self.store = store 30 } 31 32 func acceptShare(metadata: CKShare.Metadata) async { 33 NotificationCenter.default.post(name: .cloudShareAcceptanceStarted, object: nil) 34 35 guard metadata.containerIdentifier == ckContainer.containerIdentifier else { 36 syncMonitor.note( 37 "acceptShare: container mismatch — metadata=\(metadata.containerIdentifier) " + 38 "expected=\(ckContainer.containerIdentifier ?? "nil")" 39 ) 40 return 41 } 42 let existingJoinedGameIDs = store.joinedSharedGameIDs() 43 do { 44 try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in 45 let op = CKAcceptSharesOperation(shareMetadatas: [metadata]) 46 op.acceptSharesResultBlock = { result in cont.resume(with: result) } 47 ckContainer.add(op) 48 } 49 syncMonitor.note("Share accepted — fetching shared zone") 50 await syncMonitor.run("share-accept fetch") { 51 try await syncEngine.fetchChanges() 52 } 53 let joinedGameID = store.joinedSharedGameIDs() 54 .subtracting(existingJoinedGameIDs) 55 .first 56 NotificationCenter.default.post( 57 name: .cloudShareAcceptanceCompleted, 58 object: nil, 59 userInfo: joinedGameID.map { ["gameID": $0] } 60 ) 61 if let joinedGameID, let onShareJoined { 62 await onShareJoined(joinedGameID) 63 } 64 } catch { 65 syncMonitor.recordError("acceptShare", error) 66 } 67 } 68 69 func resetAllData() async { 70 await syncEngine.resetSyncState() 71 72 async let privateCleanup: Void = deleteAllPrivateZones() 73 async let sharedCleanup: Void = leaveAllSharedZones() 74 _ = await (privateCleanup, sharedCleanup) 75 76 store.resetAllData() 77 UserDefaults.standard.removeObject(forKey: "gamePlayerColors") 78 syncMonitor.note("Database reset — all games and sync state cleared") 79 } 80 81 private func deleteAllPrivateZones() async { 82 do { 83 let zones = try await ckContainer.privateCloudDatabase.allRecordZones() 84 guard !zones.isEmpty else { return } 85 let ids = zones.map(\.zoneID) 86 try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in 87 let op = CKModifyRecordZonesOperation( 88 recordZonesToSave: nil, 89 recordZoneIDsToDelete: ids 90 ) 91 op.modifyRecordZonesResultBlock = { result in cont.resume(with: result) } 92 ckContainer.privateCloudDatabase.add(op) 93 } 94 } catch { 95 syncMonitor.note("reset: private zone cleanup failed — \(error)") 96 } 97 } 98 99 private func leaveAllSharedZones() async { 100 do { 101 let zones = try await ckContainer.sharedCloudDatabase.allRecordZones() 102 for zone in zones { 103 try await ckContainer.sharedCloudDatabase.deleteRecordZone(withID: zone.zoneID) 104 } 105 } catch { 106 syncMonitor.note("reset: shared zone cleanup failed — \(error)") 107 } 108 } 109 }