crossmate

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

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 }