Skip to content

Commit 09ee494

Browse files
committed
WIP
1 parent 77b543a commit 09ee494

File tree

2 files changed

+132
-101
lines changed

2 files changed

+132
-101
lines changed

LoopFollow/Loop Follow.entitlements

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,10 @@
66
<string>development</string>
77
<key>com.apple.developer.aps-environment</key>
88
<string>development</string>
9-
<key>com.apple.developer.icloud-container-identifiers</key>
10-
<array>
11-
<string>iCloud.$(CFBundleIdentifier)</string>
12-
</array>
139
<key>com.apple.developer.icloud-services</key>
1410
<array>
15-
<string>CloudDocuments</string>
1611
<string>CloudKit</string>
1712
</array>
18-
<key>com.apple.developer.ubiquity-container-identifiers</key>
19-
<array>
20-
<string>iCloud.$(CFBundleIdentifier)</string>
21-
</array>
2213
<key>com.apple.security.app-sandbox</key>
2314
<true/>
2415
<key>com.apple.security.device.bluetooth</key>

LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift

Lines changed: 132 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import Foundation
55
import SwiftUI
6+
import CloudKit
67

78
struct 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\nPlease 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\nMake 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\nMake 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\nMake 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

Comments
 (0)