33
44import Foundation
55import SwiftUI
6+ import CloudKit
67
78struct ImportPreview {
89 let nightscoutURL : String ?
@@ -238,19 +239,33 @@ class ImportExportSettingsViewModel: ObservableObject {
238239 exportedAlarmIds. removeAll ( )
239240 }
240241
241- // MARK: - iCloud Methods
242+ // MARK: - iCloud Methods (using CloudKit)
242243
243- /// Returns the iCloud container URL for persistent storage (survives app deletion)
244- /// Falls back to local Documents if iCloud is not available
245- private func getICloudContainerURL( ) -> URL ? {
246- if let iCloudURL = FileManager . default. url ( forUbiquityContainerIdentifier: nil ) {
247- let documentsURL = iCloudURL. appendingPathComponent ( " Documents " )
248- if !FileManager. default. fileExists ( atPath: documentsURL. path) {
249- try ? FileManager . default. createDirectory ( at: documentsURL, withIntermediateDirectories: true )
244+ private let cloudKitRecordType = " AppSettings "
245+ private var cloudKitRecordID : CKRecord . ID {
246+ CKRecord . ID ( recordName: " \( AppConstants . appInstanceId) _settings " )
247+ }
248+
249+ /// Check if CloudKit is available
250+ private func checkCloudKitAvailability( completion: @escaping ( Bool , String ? ) -> Void ) {
251+ CKContainer . default ( ) . accountStatus { status, error in
252+ DispatchQueue . main. async {
253+ switch status {
254+ case . available:
255+ completion ( true , nil )
256+ case . noAccount:
257+ completion ( false , " iCloud is not available. Please sign in to iCloud in Settings. " )
258+ case . restricted:
259+ completion ( false , " iCloud access is restricted on this device. " )
260+ case . couldNotDetermine:
261+ completion ( false , " Could not determine iCloud status: \( error? . localizedDescription ?? " Unknown error " ) " )
262+ case . temporarilyUnavailable:
263+ completion ( false , " iCloud is temporarily unavailable. Please try again later. " )
264+ @unknown default :
265+ completion ( false , " Unknown iCloud status. " )
266+ }
250267 }
251- return documentsURL
252268 }
253- return nil
254269 }
255270
256271 func exportToiCloud( ) {
@@ -268,109 +283,134 @@ class ImportExportSettingsViewModel: ObservableObject {
268283 exportType: " All Settings "
269284 )
270285
271- guard let jsonData = allSettings. encodeToJSON ( ) ? . data ( using : . utf8 ) else {
286+ guard let jsonString = allSettings. encodeToJSON ( ) else {
272287 qrCodeErrorMessage = " Failed to prepare settings for iCloud export "
273288 return
274289 }
275290
276- guard let iCloudDocumentsPath = getICloudContainerURL ( ) else {
277- qrCodeErrorMessage = " iCloud is not available. Please sign in to iCloud in Settings and enable iCloud Drive. "
278- LogManager . shared. log ( category: . general, message: " iCloud ubiquity container not available " )
279- return
280- }
281-
282- let fileName = " \( AppConstants . appInstanceId) Settings.json "
283- let iCloudPath = iCloudDocumentsPath. appendingPathComponent ( fileName)
291+ LogManager . shared. log ( category: . general, message: " Attempting to export settings to CloudKit " )
284292
285- LogManager . shared. log ( category: . general, message: " Attempting to export settings to iCloud " )
286- LogManager . shared. log ( category: . general, message: " iCloud ubiquity container path: \( iCloudDocumentsPath. path) " )
287- LogManager . shared. log ( category: . general, message: " Saving settings file to: \( iCloudPath. path) " )
288-
289- do {
290- try jsonData. write ( to: iCloudPath)
291-
292- // Build export details for the success alert - show what's being exported
293- var details : [ String ] = [ ]
294-
295- // Nightscout - show if URL is configured
296- if !nightscoutSettings. url. isEmpty {
297- details. append ( " Nightscout: \( nightscoutSettings. url) " )
298- }
293+ checkCloudKitAvailability { [ weak self] available, errorMessage in
294+ guard let self = self else { return }
299295
300- // Dexcom - show if username is configured
301- if !dexcomSettings. userName. isEmpty {
302- details. append ( " Dexcom: \( dexcomSettings. userName) " )
296+ if !available {
297+ self . qrCodeErrorMessage = errorMessage ?? " iCloud is not available. "
298+ LogManager . shared. log ( category: . general, message: " CloudKit not available: \( errorMessage ?? " Unknown " ) " )
299+ return
303300 }
304301
305- // Remote - only show if a remote type is actually configured (not "none")
306- if remoteSettings. remoteType != . none {
307- details. append ( " Remote: \( remoteSettings. remoteType. rawValue) " )
308- }
302+ let privateDatabase = CKContainer . default ( ) . privateCloudDatabase
309303
310- // Alarms - show if any alarms exist
311- if !alarmSettings. alarms. isEmpty {
312- details. append ( " Alarms: \( alarmSettings. alarms. count) alarm(s) " )
313- }
304+ // First try to fetch existing record to update it, or create new one
305+ privateDatabase. fetch ( withRecordID: self . cloudKitRecordID) { [ weak self] existingRecord, error in
306+ guard let self = self else { return }
314307
315- exportSuccessDetails = details
316- exportSuccessMessage = " Settings saved to iCloud "
317- showExportSuccessAlert = true
318- qrCodeErrorMessage = " " // Clear any previous error
308+ let record : CKRecord
309+ if let existing = existingRecord {
310+ record = existing
311+ } else {
312+ record = CKRecord ( recordType: self . cloudKitRecordType, recordID: self . cloudKitRecordID)
313+ }
319314
320- LogManager . shared. log ( category: . general, message: " All settings exported to iCloud successfully " )
321- } catch {
322- qrCodeErrorMessage = " Failed to export to iCloud: \( error. localizedDescription) "
315+ record [ " settingsJSON " ] = jsonString as CKRecordValue
316+ record [ " exportDate " ] = Date ( ) as CKRecordValue
317+ record [ " appVersion " ] = AppVersionManager ( ) . version ( ) as CKRecordValue
318+
319+ privateDatabase. save ( record) { [ weak self] _, saveError in
320+ DispatchQueue . main. async {
321+ guard let self = self else { return }
322+
323+ if let saveError = saveError {
324+ self . qrCodeErrorMessage = " Failed to export to iCloud: \( saveError. localizedDescription) "
325+ LogManager . shared. log ( category: . general, message: " CloudKit save failed: \( saveError. localizedDescription) " )
326+ return
327+ }
328+
329+ // Build export details for the success alert
330+ var details : [ String ] = [ ]
331+
332+ if !nightscoutSettings. url. isEmpty {
333+ details. append ( " Nightscout: \( nightscoutSettings. url) " )
334+ }
335+ if !dexcomSettings. userName. isEmpty {
336+ details. append ( " Dexcom: \( dexcomSettings. userName) " )
337+ }
338+ if remoteSettings. remoteType != . none {
339+ details. append ( " Remote: \( remoteSettings. remoteType. rawValue) " )
340+ }
341+ if !alarmSettings. alarms. isEmpty {
342+ details. append ( " Alarms: \( alarmSettings. alarms. count) alarm(s) " )
343+ }
344+
345+ self . exportSuccessDetails = details
346+ self . exportSuccessMessage = " Settings saved to iCloud "
347+ self . showExportSuccessAlert = true
348+ self . qrCodeErrorMessage = " "
349+
350+ LogManager . shared. log ( category: . general, message: " All settings exported to CloudKit successfully " )
351+ }
352+ }
353+ }
323354 }
324355 }
325356
326357 func importFromiCloud( ) {
327- // Try to read from iCloud ubiquity container (persists after app deletion)
328- guard let iCloudDocumentsPath = getICloudContainerURL ( ) else {
329- importNotFoundMessage = " iCloud is not available. \n \n Please sign in to iCloud in Settings and enable iCloud Drive. "
330- showImportNotFoundAlert = true
331- LogManager . shared. log ( category: . general, message: " iCloud ubiquity container not available for import " )
332- return
333- }
358+ LogManager . shared. log ( category: . general, message: " Attempting to import settings from CloudKit " )
334359
335- let fileName = " \( AppConstants . appInstanceId ) Settings.json "
336- let iCloudPath = iCloudDocumentsPath . appendingPathComponent ( fileName )
360+ checkCloudKitAvailability { [ weak self ] available , errorMessage in
361+ guard let self = self else { return }
337362
338- LogManager . shared. log ( category: . general, message: " Attempting to import settings from iCloud " )
339- LogManager . shared. log ( category: . general, message: " iCloud ubiquity container path: \( iCloudDocumentsPath. path) " )
340- LogManager . shared. log ( category: . general, message: " Looking for settings file: \( iCloudPath. path) " )
341-
342- guard FileManager . default. fileExists ( atPath: iCloudPath. path) else {
343- LogManager . shared. log ( category: . general, message: " Settings file not found at: \( iCloudPath. path) " )
344- importNotFoundMessage = " No settings file found in iCloud. \n \n Make sure you have previously exported settings to iCloud from this app. "
345- showImportNotFoundAlert = true
346- return
347- }
348-
349- do {
350- LogManager . shared. log ( category: . general, message: " Settings file found, attempting to read data " )
351-
352- let data = try Data ( contentsOf: iCloudPath)
353- guard let settings = SettingsMigrationManager . migrateSettings ( data) else {
354- qrCodeErrorMessage = " Failed to decode settings from iCloud "
363+ if !available {
364+ self . importNotFoundMessage = errorMessage ?? " iCloud is not available. "
365+ self . showImportNotFoundAlert = true
366+ LogManager . shared. log ( category: . general, message: " CloudKit not available for import: \( errorMessage ?? " Unknown " ) " )
355367 return
356368 }
357369
358- // Check version compatibility
359- let currentVersion = AppVersionManager ( ) . version ( )
360- if !SettingsMigrationManager. isCompatibleVersion ( settings. appVersion) {
361- qrCodeErrorMessage = SettingsMigrationManager . getCompatibilityMessage ( for: settings. appVersion)
362- // Still try to apply settings, but warn user
370+ let privateDatabase = CKContainer . default ( ) . privateCloudDatabase
371+
372+ privateDatabase. fetch ( withRecordID: self . cloudKitRecordID) { [ weak self] record, error in
373+ DispatchQueue . main. async {
374+ guard let self = self else { return }
375+
376+ if let error = error {
377+ if let ckError = error as? CKError , ckError. code == . unknownItem {
378+ self . importNotFoundMessage = " No settings file found in iCloud. \n \n Make sure you have previously exported settings to iCloud from this app. "
379+ self . showImportNotFoundAlert = true
380+ LogManager . shared. log ( category: . general, message: " No CloudKit record found " )
381+ } else {
382+ self . qrCodeErrorMessage = " Failed to import from iCloud: \( error. localizedDescription) "
383+ LogManager . shared. log ( category: . general, message: " CloudKit fetch failed: \( error. localizedDescription) " )
384+ }
385+ return
386+ }
387+
388+ guard let record = record,
389+ let jsonString = record [ " settingsJSON " ] as? String ,
390+ let jsonData = jsonString. data ( using: . utf8) else {
391+ self . importNotFoundMessage = " No settings file found in iCloud. \n \n Make sure you have previously exported settings to iCloud from this app. "
392+ self . showImportNotFoundAlert = true
393+ return
394+ }
395+
396+ LogManager . shared. log ( category: . general, message: " Settings record found in CloudKit, attempting to decode " )
397+
398+ guard let settings = SettingsMigrationManager . migrateSettings ( jsonData) else {
399+ self . qrCodeErrorMessage = " Failed to decode settings from iCloud "
400+ return
401+ }
402+
403+ // Check version compatibility
404+ if !SettingsMigrationManager. isCompatibleVersion ( settings. appVersion) {
405+ self . qrCodeErrorMessage = SettingsMigrationManager . getCompatibilityMessage ( for: settings. appVersion)
406+ }
407+
408+ // Store settings and create preview for confirmation
409+ self . pendingImportSettings = settings
410+ self . pendingImportSource = " iCloud "
411+ self . createImportPreview ( from: settings)
412+ }
363413 }
364-
365- // Store settings and create preview for confirmation
366- pendingImportSettings = settings
367- pendingImportSource = " iCloud "
368- createImportPreview ( from: settings)
369-
370- } catch {
371- let currentVersion = AppVersionManager ( ) . version ( )
372- qrCodeErrorMessage = " iCloud import failed. This might be due to a version change (current: \( currentVersion) ). Please try exporting settings to iCloud again. "
373- LogManager . shared. log ( category: . general, message: " iCloud import failed: \( error. localizedDescription) " )
374414 }
375415 }
376416
0 commit comments