1→// 2→// CloudKitManager.swift 3→// KortexOS 4→// 5→// Main orchestrator for CloudKit synchronization 6→// 7→ 8→import Foundation 9→import CloudKit 10→import Combine 11→import os.log 12→ 13→// MARK: - CloudKit Manager 14→ 15→@MainActor 16→final class CloudKitManager: ObservableObject { 17→ static let shared = CloudKitManager() 18→ 19→ // MARK: - Published Properties 20→ 21→ @Published private(set) var syncStatus: SyncStatus = .idle 22→ @Published private(set) var lastSyncDate: Date? 23→ @Published private(set) var isCloudAvailable = false 24→ @Published var isSyncEnabled = true 25→ 26→ // MARK: - Private Properties 27→ 28→ private let container = CloudKitConfiguration.container 29→ private let database = CloudKitConfiguration.privateDatabase 30→ private let zoneID = CloudKitConfiguration.zoneID 31→ private let tokenManager = ChangeTokenManager.shared 32→ private let logger = Logger(subsystem: "com.neog.KortexOS", category: "CloudKit") 33→ private let syncLogger = SyncLogger.shared 34→ 35→ private var subscriptionID = "KortexOS-zone-subscription" 36→ private var pendingChanges: [PendingChange] = [] 37→ private let pendingChangesKey = "KortexOS_PendingChanges" 38→ 39→ // Callbacks for DataManager integration 40→ var onRecordsReceived: (([CKRecord]) -> Void)? 41→ var onRecordsDeleted: (([CKRecord.ID]) -> Void)? 42→ 43→ // MARK: - Initialization 44→ 45→ private init() { 46→ loadPendingChanges() 47→ } 48→ 49→ // MARK: - Setup 50→ 51→ /// Initialize CloudKit - call this on app launch 52→ func setup() async { 53→ guard isSyncEnabled else { 54→ syncStatus = .disabled 55→ return 56→ } 57→ 58→ await checkAccountStatus() 59→ 60→ if isCloudAvailable { 61→ await createZoneIfNeeded() 62→ await createSubscriptionIfNeeded() 63→ await processPendingChanges() 64→ } 65→ } 66→ 67→ // MARK: - Account Status 68→ 69→ private func checkAccountStatus() async { 70→ do { 71→ let status = try await container.accountStatus() 72→ switch status { 73→ case .available: 74→ isCloudAvailable = true 75→ logger.info("iCloud account available") 76→ await MainActor.run { 77→ syncLogger.logConnection(available: true, accountStatus: "iCloud disponível") 78→ } 79→ case .noAccount: 80→ isCloudAvailable = false 81→ syncStatus = .error(.notAuthenticated) 82→ logger.warning("No iCloud account") 83→ await MainActor.run { 84→ syncLogger.logConnection(available: false, accountStatus: "Sem conta iCloud") 85→ } 86→ case .restricted, .couldNotDetermine: 87→ isCloudAvailable = false 88→ syncStatus = .error(.iCloudNotAvailable) 89→ logger.warning("iCloud restricted or unavailable") 90→ await MainActor.run { 91→ syncLogger.logConnection(available: false, accountStatus: "iCloud restrito/indisponível") 92→ } 93→ case .temporarilyUnavailable: 94→ isCloudAvailable = false 95→ syncStatus = .offline 96→ logger.warning("iCloud temporarily unavailable") 97→ await MainActor.run { 98→ syncLogger.logConnection(available: false, accountStatus: "iCloud temporariamente indisponível") 99→ } 100→ @unknown default: 101→ isCloudAvailable = false 102→ syncStatus = .error(.iCloudNotAvailable) 103→ await MainActor.run { 104→ syncLogger.logConnection(available: false, accountStatus: "Status desconhecido") 105→ } 106→ } 107→ } catch { 108→ isCloudAvailable = false 109→ syncStatus = .error(.unknown(error)) 110→ logger.error("Failed to check account status: \(error.localizedDescription)") 111→ await MainActor.run { 112→ syncLogger.logError("Falha ao verificar conta", error: error) 113→ } 114→ } 115→ } 116→ 117→ // MARK: - Zone Management 118→ 119→ private func createZoneIfNeeded() async { 120→ do { 121→ let zone = CloudKitConfiguration.zone 122→ _ = try await database.modifyRecordZones(saving: [zone], deleting: []) 123→ logger.info("Zone created/verified: \(self.zoneID.zoneName)") 124→ await MainActor.run { 125→ syncLogger.logZone(action: "Criada/verificada: \(self.zoneID.zoneName)") 126→ } 127→ } catch let error as CKError where error.code == .serverRecordChanged { 128→ // Zone already exists, that's fine 129→ logger.info("Zone already exists") 130→ await MainActor.run { 131→ syncLogger.logZone(action: "Zone já existe") 132→ } 133→ } catch { 134→ logger.error("Failed to create zone: \(error.localizedDescription)") 135→ syncStatus = .error(.zoneNotFound) 136→ await MainActor.run { 137→ syncLogger.logZone(action: "Falha ao criar zone", success: false, error: error.localizedDescription) 138→ } 139→ } 140→ } 141→ 142→ // MARK: - Subscription Management 143→ 144→ private func createSubscriptionIfNeeded() async { 145→ let subscription = CKRecordZoneSubscription(zoneID: zoneID, subscriptionID: subscriptionID) 146→ let notificationInfo = CKSubscription.NotificationInfo() 147→ notificationInfo.shouldSendContentAvailable = true 148→ subscription.notificationInfo = notificationInfo 149→ 150→ do { 151→ _ = try await database.modifySubscriptions(saving: [subscription], deleting: []) 152→ logger.info("Subscription created: \(self.subscriptionID)") 153→ await MainActor.run { 154→ syncLogger.logSubscription(action: "Criada para notificações push") 155→ } 156→ } catch let error as CKError where error.code == .serverRecordChanged { 157→ // Subscription already exists 158→ logger.info("Subscription already exists") 159→ await MainActor.run { 160→ syncLogger.logSubscription(action: "Subscription já existe") 161→ } 162→ } catch { 163→ logger.error("Failed to create subscription: \(error.localizedDescription)") 164→ await MainActor.run { 165→ syncLogger.logSubscription(action: "Falha: \(error.localizedDescription)", success: false) 166→ } 167→ } 168→ } 169→ 170→ // MARK: - Push Changes 171→ 172→ /// Push local changes to CloudKit 173→ func pushRecords(_ records: [CKRecord]) async throws { 174→ guard isCloudAvailable else { 175→ // Queue for later 176→ for record in records { 177→ if let data = try? NSKeyedArchiver.archivedData(withRootObject: record, requiringSecureCoding: true) { 178→ let change = PendingChange( 179→ recordType: record.recordType, 180→ recordId: record.recordID.recordName, 181→ operation: .update, 182→ data: data 183→ ) 184→ pendingChanges.append(change) 185→ } 186→ } 187→ savePendingChanges() 188→ await MainActor.run { 189→ syncLogger.logInfo("Offline - \(records.count) records queued", details: "Aguardando conexão") 190→ } 191→ throw SyncError.networkUnavailable 192→ } 193→ 194→ syncStatus = .syncing(progress: 0.0) 195→ await MainActor.run { 196→ syncLogger.logSyncStart() 197→ } 198→ 199→ // Group records by type for detailed logging 200→ let recordsByType = Dictionary(grouping: records) { $0.recordType } 201→ 202→ do { 203→ let results = try await database.modifyRecords( 204→ saving: records, 205→ deleting: [], 206→ savePolicy: .changedKeys 207→ ) 208→ 209→ // Count successfully saved records 210→ var savedCount = 0 211→ var savedIDs = Set() 212→ var failedCount = 0 213→ for (recordID, result) in results.saveResults { 214→ switch result { 215→ case .success: 216→ savedCount += 1 217→ savedIDs.insert(recordID.recordName) 218→ case .failure(let error): 219→ failedCount += 1 220→ await MainActor.run { 221→ syncLogger.logError("Falha ao salvar: \(recordID.recordName)", error: error) 222→ } 223→ } 224→ } 225→ 226→ logger.info("Pushed \(savedCount) records") 227→ syncStatus = .success 228→ lastSyncDate = Date() 229→ 230→ // Log each record type 231→ for (recordType, typeRecords) in recordsByType { 232→ let typeCount = typeRecords.filter { savedIDs.contains($0.recordID.recordName) }.count 233→ if typeCount > 0 { 234→ await MainActor.run { 235→ syncLogger.logPush(recordType: recordType, count: typeCount, success: true) 236→ } 237→ } 238→ } 239→ 240→ // Clear successful records from pending 241→ pendingChanges.removeAll { savedIDs.contains($0.recordId) } 242→ savePendingChanges() 243→ 244→ await MainActor.run { 245→ syncLogger.logSyncComplete(success: true, recordsPushed: savedCount, recordsPulled: 0) 246→ } 247→ } catch let error as CKError { 248→ handleCKError(error) 249→ await MainActor.run { 250→ syncLogger.logError("Push falhou", error: error) 251→ } 252→ throw mapCKError(error) 253→ } catch { 254→ syncStatus = .error(.unknown(error)) 255→ await MainActor.run { 256→ syncLogger.logError("Push falhou", error: error) 257→ } 258→ throw SyncError.unknown(error) 259→ } 260→ } 261→ 262→ /// Delete records from CloudKit 263→ func deleteRecords(_ recordIDs: [CKRecord.ID]) async throws { 264→ guard isCloudAvailable else { 265→ await MainActor.run { 266→ syncLogger.logError("Delete falhou - offline") 267→ } 268→ throw SyncError.networkUnavailable 269→ } 270→ 271→ syncStatus = .syncing(progress: 0.0) 272→ 273→ do { 274→ let results = try await database.modifyRecords( 275→ saving: [], 276→ deleting: recordIDs 277→ ) 278→ 279→ // Count successfully deleted records 280→ var deletedCount = 0 281→ for (_, result) in results.deleteResults { 282→ if case .success = result { 283→ deletedCount += 1 284→ } 285→ } 286→ 287→ logger.info("Deleted \(deletedCount) records") 288→ syncStatus = .success 289→ lastSyncDate = Date() 290→ 291→ await MainActor.run { 292→ syncLogger.logDelete(recordType: "records", count: deletedCount, success: true) 293→ } 294→ } catch let error as CKError { 295→ handleCKError(error) 296→ await MainActor.run { 297→ syncLogger.logError("Delete falhou", error: error) 298→ } 299→ throw mapCKError(error) 300→ } catch { 301→ syncStatus = .error(.unknown(error)) 302→ await MainActor.run { 303→ syncLogger.logError("Delete falhou", error: error) 304→ } 305→ throw SyncError.unknown(error) 306→ } 307→ } 308→ 309→ // MARK: - Pull Changes 310→ 311→ /// Fetch changes from CloudKit since last sync 312→ func pullChanges() async { 313→ guard isCloudAvailable else { 314→ syncStatus = .offline 315→ await MainActor.run { 316→ syncLogger.logInfo("Pull ignorado - offline") 317→ } 318→ return 319→ } 320→ 321→ syncStatus = .syncing(progress: 0.0) 322→ await MainActor.run { 323→ syncLogger.logSyncStart() 324→ } 325→ 326→ do { 327→ var changedRecords: [CKRecord] = [] 328→ var deletedRecordIDs: [CKRecord.ID] = [] 329→ 330→ let changes = try await database.recordZoneChanges( 331→ inZoneWith: zoneID, 332→ since: tokenManager.zoneChangeToken 333→ ) 334→ 335→ // Process modifications - the value contains the record directly 336→ for (_, modification) in changes.modificationResultsByID { 337→ switch modification { 338→ case .success(let modificationResult): 339→ changedRecords.append(modificationResult.record) 340→ case .failure(let error): 341→ logger.error("Failed to fetch record: \(error.localizedDescription)") 342→ await MainActor.run { 343→ syncLogger.logError("Falha ao buscar record", error: error) 344→ } 345→ } 346→ } 347→ 348→ // Process deletions 349→ for deletion in changes.deletions { 350→ deletedRecordIDs.append(deletion.recordID) 351→ } 352→ 353→ // Update token 354→ tokenManager.zoneChangeToken = changes.changeToken 355→ 356→ // Log received records by type 357→ let recordsByType = Dictionary(grouping: changedRecords) { $0.recordType } 358→ for (recordType, records) in recordsByType { 359→ await MainActor.run { 360→ syncLogger.logPull(recordType: recordType, count: records.count, success: true) 361→ } 362→ } 363→ 364→ // Notify DataManager 365→ if !changedRecords.isEmpty { 366→ onRecordsReceived?(changedRecords) 367→ await MainActor.run { 368→ syncLogger.logMerge(recordType: "records", action: "Aplicando \(changedRecords.count) alterações localmente") 369→ } 370→ } 371→ if !deletedRecordIDs.isEmpty { 372→ onRecordsDeleted?(deletedRecordIDs) 373→ await MainActor.run { 374→ syncLogger.logDelete(recordType: "records", count: deletedRecordIDs.count, success: true) 375→ } 376→ } 377→ 378→ logger.info("Pulled \(changedRecords.count) changed, \(deletedRecordIDs.count) deleted") 379→ syncStatus = .success 380→ lastSyncDate = Date() 381→ 382→ await MainActor.run { 383→ if changedRecords.isEmpty && deletedRecordIDs.isEmpty { 384→ syncLogger.logSuccess("Nenhuma alteração remota") 385→ } else { 386→ syncLogger.logSyncComplete(success: true, recordsPushed: 0, recordsPulled: changedRecords.count) 387→ } 388→ } 389→ 390→ } catch let error as CKError { 391→ handleCKError(error) 392→ await MainActor.run { 393→ syncLogger.logError("Pull falhou", error: error) 394→ } 395→ } catch { 396→ syncStatus = .error(.unknown(error)) 397→ logger.error("Pull failed: \(error.localizedDescription)") 398→ await MainActor.run { 399→ syncLogger.logError("Pull falhou", error: error) 400→ } 401→ } 402→ } 403→ 404→ // MARK: - Full Sync 405→ 406→ /// Perform a full sync (push pending + pull changes) 407→ func performFullSync() async { 408→ guard isSyncEnabled else { 409→ syncStatus = .disabled 410→ return 411→ } 412→ 413→ await checkAccountStatus() 414→ 415→ guard isCloudAvailable else { 416→ return 417→ } 418→ 419→ syncStatus = .syncing(progress: 0.0) 420→ 421→ // Process pending changes first 422→ await processPendingChanges() 423→ 424→ // Then pull changes 425→ await pullChanges() 426→ } 427→ 428→ // MARK: - Pending Changes 429→ 430→ private func processPendingChanges() async { 431→ guard !pendingChanges.isEmpty else { return } 432→ 433→ var recordsToSave: [CKRecord] = [] 434→ 435→ for change in pendingChanges { 436→ if let record = try? NSKeyedUnarchiver.unarchivedObject(ofClass: CKRecord.self, from: change.data) { 437→ recordsToSave.append(record) 438→ } 439→ } 440→ 441→ if !recordsToSave.isEmpty { 442→ do { 443→ try await pushRecords(recordsToSave) 444→ } catch { 445→ logger.error("Failed to process pending changes: \(error.localizedDescription)") 446→ } 447→ } 448→ } 449→ 450→ private func loadPendingChanges() { 451→ guard let data = UserDefaults.standard.data(forKey: pendingChangesKey), 452→ let changes = try? JSONDecoder().decode([PendingChange].self, from: data) else { 453→ return 454→ } 455→ pendingChanges = changes 456→ } 457→ 458→ private func savePendingChanges() { 459→ if let data = try? JSONEncoder().encode(pendingChanges) { 460→ UserDefaults.standard.set(data, forKey: pendingChangesKey) 461→ } 462→ } 463→ 464→ // MARK: - Error Handling 465→ 466→ private func handleCKError(_ error: CKError) { 467→ switch error.code { 468→ case .notAuthenticated: 469→ syncStatus = .error(.notAuthenticated) 470→ case .networkUnavailable, .networkFailure: 471→ syncStatus = .offline 472→ case .quotaExceeded: 473→ syncStatus = .error(.quotaExceeded) 474→ case .serverRecordChanged: 475→ syncStatus = .error(.serverRecordChanged) 476→ case .zoneNotFound: 477→ syncStatus = .error(.zoneNotFound) 478→ // Try to recreate zone 479→ Task { 480→ await createZoneIfNeeded() 481→ } 482→ case .partialFailure: 483→ if let partialErrors = error.partialErrorsByItemID { 484→ let failed = partialErrors.count 485→ syncStatus = .error(.partialFailure(failed: failed, total: failed)) 486→ } 487→ default: 488→ syncStatus = .error(.unknown(error)) 489→ } 490→ logger.error("CloudKit error: \(error.localizedDescription)") 491→ } 492→ 493→ private func mapCKError(_ error: CKError) -> SyncError { 494→ switch error.code { 495→ case .notAuthenticated: return .notAuthenticated 496→ case .networkUnavailable, .networkFailure: return .networkUnavailable 497→ case .quotaExceeded: return .quotaExceeded 498→ case .serverRecordChanged: return .serverRecordChanged 499→ case .zoneNotFound: return .zoneNotFound 500→ default: return .unknown(error) 501→ } 502→ } 503→ 504→ // MARK: - Remote Notification Handler 505→ 506→ /// Call this when receiving a remote notification 507→ func handleRemoteNotification(_ userInfo: [AnyHashable: Any]) async { 508→ let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) 509→ 510→ if notification?.subscriptionID == subscriptionID { 511→ logger.info("Received zone change notification") 512→ await MainActor.run { 513→ syncLogger.logInfo("Push notification recebido", details: "Iniciando pull de alterações") 514→ } 515→ await pullChanges() 516→ } 517→ } 518→ 519→ // MARK: - Debug / Reset 520→ 521→ /// Reset sync state (for debugging) 522→ func resetSyncState() { 523→ tokenManager.resetAllTokens() 524→ pendingChanges.removeAll() 525→ savePendingChanges() 526→ lastSyncDate = nil 527→ syncStatus = .idle 528→ logger.info("Sync state reset") 529→ } 530→ 531→ /// Toggle sync enabled state 532→ func setSyncEnabled(_ enabled: Bool) { 533→ isSyncEnabled = enabled 534→ UserDefaults.standard.set(enabled, forKey: "KortexOS_SyncEnabled") 535→ 536→ if enabled { 537→ Task { 538→ await setup() 539→ } 540→ } else { 541→ syncStatus = .disabled 542→ } 543→ } 544→} 545→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.