From 65e9408c09e4cf06a845fa85973a277cd419bc44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 8 Jul 2026 22:04:12 +0200 Subject: [PATCH] Remove migration step 1 and the legacy UserDefaults framework it kept alive Step 1 shipped in v3.0.0 (2025-07-07) and its one-year window has passed. It was already unreachable: migrationStep defaults to 9, so any user without a stored migrationStep skips every step, and users who have one are at >= 1. Removing migrateStep1() also retires its exclusive dependencies: the UserDefaultsValue / ObservableUserDefaultsValue stack, UserDefaultsValueGroups, ObservableUserDefaults, ObservationToken, AnyConvertible, and the orphaned HKQuantity+AnyConvertible. Nothing in Swift reads the app-group UserDefaults suite anymore, so AppConstants.APP_GROUP_ID goes too. --- LoopFollow.xcodeproj/project.pbxproj | 24 - .../HKQuantity+AnyConvertible.swift | 31 - LoopFollow/Helpers/AnyConvertible.swift | 130 -- LoopFollow/Helpers/AppConstants.swift | 2 - LoopFollow/Helpers/ObservationToken.swift | 17 - .../Settings/ContactSettingsViewModel.swift | 1 - .../ObservableUserDefaultsValue.swift | 111 -- .../Storage/Framework/UserDefaultsValue.swift | 121 -- .../Framework/UserDefaultsValueGroups.swift | 66 - .../Storage/ObservableUserDefaults.swift | 20 - LoopFollow/Storage/Storage+Migrate.swift | 1673 ----------------- .../ViewControllers/MainViewController.swift | 6 - 12 files changed, 2202 deletions(-) delete mode 100644 LoopFollow/Extensions/HKQuantity+AnyConvertible.swift delete mode 100644 LoopFollow/Helpers/AnyConvertible.swift delete mode 100644 LoopFollow/Helpers/ObservationToken.swift delete mode 100644 LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift delete mode 100644 LoopFollow/Storage/Framework/UserDefaultsValue.swift delete mode 100644 LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift delete mode 100644 LoopFollow/Storage/ObservableUserDefaults.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index b162633e0..f5a8eaacf 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -251,8 +251,6 @@ DDCF9A882D85FD33004DF4DD /* AlarmData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A872D85FD33004DF4DD /* AlarmData.swift */; }; DDCF9A8C2D86005E004DF4DD /* AlarmManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */; }; DDCFCAF22B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */; }; - DDD10EFF2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10EFE2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift */; }; - DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */; }; DDD10F032C518A6500D76A8E /* TreatmentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F022C518A6500D76A8E /* TreatmentResponse.swift */; }; DDD10F052C529DA200D76A8E /* ObservableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F042C529DA200D76A8E /* ObservableValue.swift */; }; DDD10F072C529DE800D76A8E /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F062C529DE800D76A8E /* Observable.swift */; }; @@ -427,10 +425,6 @@ FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */; }; FCC6885C2489559400A0279D /* blank.wav in Resources */ = {isa = PBXBuildFile; fileRef = FCC6885B2489559400A0279D /* blank.wav */; }; FCC6885E24896A6C00A0279D /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FCC6885D24896A6C00A0279D /* silence.mp3 */; }; - FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886624898F8000A0279D /* UserDefaultsValue.swift */; }; - FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */; }; - FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886A24898FD800A0279D /* ObservationToken.swift */; }; - FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886C2489909D00A0279D /* AnyConvertible.swift */; }; FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886E2489A53800A0279D /* AppConstants.swift */; }; 9C7FB98C98BE4FF98F4815EE /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFBE69CEF18416D84959974 /* Telemetry.swift */; }; FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD2A27C24C9D044009F7B7B /* Globals.swift */; }; @@ -720,8 +714,6 @@ DDCF9A872D85FD33004DF4DD /* AlarmData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmData.swift; sourceTree = ""; }; DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmManager.swift; sourceTree = ""; }; DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = LoopFollowDisplayNameConfig.xcconfig; sourceTree = ""; }; - DDD10EFE2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableUserDefaultsValue.swift; sourceTree = ""; }; - DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableUserDefaults.swift; sourceTree = ""; }; DDD10F022C518A6500D76A8E /* TreatmentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentResponse.swift; sourceTree = ""; }; DDD10F042C529DA200D76A8E /* ObservableValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableValue.swift; sourceTree = ""; }; DDD10F062C529DE800D76A8E /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; @@ -899,10 +891,6 @@ A1A1A10001000000A0CFEED2 /* APNsCredentialValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNsCredentialValidator.swift; sourceTree = ""; }; FCC6885B2489559400A0279D /* blank.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = blank.wav; sourceTree = ""; }; FCC6885D24896A6C00A0279D /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; }; - FCC6886624898F8000A0279D /* UserDefaultsValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsValue.swift; sourceTree = ""; }; - FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsValueGroups.swift; sourceTree = ""; }; - FCC6886A24898FD800A0279D /* ObservationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationToken.swift; sourceTree = ""; }; - FCC6886C2489909D00A0279D /* AnyConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyConvertible.swift; sourceTree = ""; }; FCC6886E2489A53800A0279D /* AppConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; BDFBE69CEF18416D84959974 /* Telemetry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Telemetry.swift; sourceTree = ""; }; FCC688702489A57C00A0279D /* Loop Follow.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Loop Follow.entitlements"; sourceTree = ""; }; @@ -1339,7 +1327,6 @@ isa = PBXGroup; children = ( FCC688512489363F00A0279D /* Framework */, - DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */, DDD10F062C529DE800D76A8E /* Observable.swift */, DD4878042C7B2C970048F05C /* Storage.swift */, DD8316472DE49EE5004467AA /* Storage+Migrate.swift */, @@ -1698,9 +1685,6 @@ FCC688512489363F00A0279D /* Framework */ = { isa = PBXGroup; children = ( - FCC6886624898F8000A0279D /* UserDefaultsValue.swift */, - FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */, - DDD10EFE2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift */, DDD10F042C529DA200D76A8E /* ObservableValue.swift */, DD4878022C7B297E0048F05C /* StorageValue.swift */, DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */, @@ -1724,8 +1708,6 @@ DDF6999C2C5AAA4C0058A8D9 /* Views */, FCC6886E2489A53800A0279D /* AppConstants.swift */, BDFBE69CEF18416D84959974 /* Telemetry.swift */, - FCC6886A24898FD800A0279D /* ObservationToken.swift */, - FCC6886C2489909D00A0279D /* AnyConvertible.swift */, FCC688592489554800A0279D /* BackgroundTaskAudio.swift */, A1A1A10001000000A0CFEED2 /* APNsCredentialValidator.swift */, A8CA8BE0B3D247408FE088B4 /* BackgroundRefreshManager.swift */, @@ -2174,7 +2156,6 @@ DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */, 656F8C122E49F3780008DC1D /* QRCodeGenerator.swift in Sources */, 6541341A2E1DC27900BDBE08 /* OverridePresetData.swift in Sources */, - FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */, DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */, DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */, DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */, @@ -2203,7 +2184,6 @@ DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */, DDCF9A8C2D86005E004DF4DD /* AlarmManager.swift in Sources */, DDCC3A4F2DDC5B54006F1C10 /* BatteryDropCondition.swift in Sources */, - FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */, DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */, DDB9FC7B2DDB573F00EFAA76 /* IOBCondition.swift in Sources */, DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */, @@ -2329,7 +2309,6 @@ DDC6CA492DD8E47A0060EE25 /* PumpVolumeCondition.swift in Sources */, DD9ACA0A2D33095600415D8A /* MinAgoTask.swift in Sources */, DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */, - DDD10EFF2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift in Sources */, DDB9FC7D2DDB575300EFAA76 /* IOBAlarmEditor.swift in Sources */, DD58171A2D299EF80041FB98 /* DexcomHeartbeatBluetoothDevice.swift in Sources */, DD7F4C0B2DD51C5500D449E9 /* OverrideEndCondition.swift in Sources */, @@ -2367,7 +2346,6 @@ DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */, DD493AE92ACF2445009A6922 /* BGData.swift in Sources */, DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */, - FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */, DD4AFB6B2DB6BF2A00BB593F /* Binding+Optional.swift in Sources */, DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */, DD5DA27C2DC930D6003D44FC /* GlucoseValue.swift in Sources */, @@ -2385,7 +2363,6 @@ DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */, 65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */, DD0C0C682C48529400DBADDF /* Metric.swift in Sources */, - FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */, DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */, DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */, DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */, @@ -2423,7 +2400,6 @@ 379BECAA2F6588300069DC62 /* RestartLiveActivityIntent.swift in Sources */, DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */, DD7F4C132DD51FD500D449E9 /* TempTargetEndCondition.swift in Sources */, - DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */, DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */, DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */, DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */, diff --git a/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift b/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift deleted file mode 100644 index 466421e70..000000000 --- a/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift +++ /dev/null @@ -1,31 +0,0 @@ -// LoopFollow -// HKQuantity+AnyConvertible.swift - -import HealthKit - -class HKQuantityWrapper: AnyConvertible { - let quantity: HKQuantity - - init(quantity: HKQuantity) { - self.quantity = quantity - } - - func toAny() -> Any { - return ["unit": UserDefaultsRepository.getPreferredUnit().unitString, - "value": quantity.doubleValue(for: UserDefaultsRepository.getPreferredUnit())] - } - - static func fromAny(_ anyValue: Any) -> HKQuantityWrapper? { - // Convert dictionary back to HKQuantity - guard let dict = anyValue as? [String: Any], - let unitString = dict["unit"] as? String, - let value = dict["value"] as? Double - else { - return nil - } - - let unit = HKUnit(from: unitString) - let quantity = HKQuantity(unit: unit, doubleValue: value) - return HKQuantityWrapper(quantity: quantity) - } -} diff --git a/LoopFollow/Helpers/AnyConvertible.swift b/LoopFollow/Helpers/AnyConvertible.swift deleted file mode 100644 index 7c7badfa1..000000000 --- a/LoopFollow/Helpers/AnyConvertible.swift +++ /dev/null @@ -1,130 +0,0 @@ -// LoopFollow -// AnyConvertible.swift - -import Foundation - -/// A type that can be converted to/from Any -protocol AnyConvertible { - func toAny() -> Any - static func fromAny(_ anyValue: Any) -> Self? -} - -// common type implementations -extension Bool: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Bool? { - return anyValue as? Bool - } -} - -extension String: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> String? { - return anyValue as? String - } -} - -extension Int: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Int? { - return anyValue as? Int - } -} - -extension Float: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Float? { - return anyValue as? Float - } -} - -extension Double: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Double? { - return anyValue as? Double - } -} - -extension Date: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Date? { - return anyValue as? Date - } -} - -extension Data: AnyConvertible { - func toAny() -> Any { - return self - } - - static func fromAny(_ anyValue: Any) -> Data? { - return anyValue as? Data - } -} - -extension UUID: AnyConvertible { - func toAny() -> Any { - return uuidString - } - - static func fromAny(_ anyValue: Any) -> UUID? { - guard let uuidString = anyValue as? String else { - return nil - } - - return UUID(uuidString: uuidString) - } -} - -// extension Array: AnyConvertible { -// func toAny() -> Any { -// return self -// } -// -// static func fromAny(_ anyValue: Any) -> Array? { -// return anyValue as? Array -// } -// } - -extension Array: AnyConvertible where Element: AnyConvertible { - func toAny() -> Any { - return map { $0.toAny() } - } - - static func fromAny(_ anyValue: Any) -> Array? { - return (anyValue as? Array)?.compactMap { Element.fromAny($0) } - } -} - -extension Optional: AnyConvertible where Wrapped: AnyConvertible { - func toAny() -> Any { - switch self { - case let .some(value): - return value.toAny() - case .none: - return self as Any - } - } - - static func fromAny(_ anyValue: Any) -> Wrapped?? { - return Wrapped.fromAny(anyValue) - } -} diff --git a/LoopFollow/Helpers/AppConstants.swift b/LoopFollow/Helpers/AppConstants.swift index e81ba159c..7c88dea52 100644 --- a/LoopFollow/Helpers/AppConstants.swift +++ b/LoopFollow/Helpers/AppConstants.swift @@ -5,8 +5,6 @@ import Foundation // Class that contains general constants used in different classes class AppConstants { - static let APP_GROUP_ID = "group.com.$(unique_id).LoopFollow" - /// Extracts the app suffix from the bundle identifier /// Bundle identifier format: com.$(unique_id).LoopFollow$(app_suffix) /// Returns the suffix part (e.g., "2" for "com.example.LoopFollow2") diff --git a/LoopFollow/Helpers/ObservationToken.swift b/LoopFollow/Helpers/ObservationToken.swift deleted file mode 100644 index c2092a5f1..000000000 --- a/LoopFollow/Helpers/ObservationToken.swift +++ /dev/null @@ -1,17 +0,0 @@ -// LoopFollow -// ObservationToken.swift - -import Foundation - -/// The token received by an observe when subscribes to its subject. The observer can cancel observation, so the subject will remove it from its observers list. -class ObservationToken { - private let cancellationClosure: () -> Void - - init(cancellationClosure: @escaping () -> Void) { - self.cancellationClosure = cancellationClosure - } - - func cancel() { - cancellationClosure() - } -} diff --git a/LoopFollow/Settings/ContactSettingsViewModel.swift b/LoopFollow/Settings/ContactSettingsViewModel.swift index 15ec8411b..c0ed95857 100644 --- a/LoopFollow/Settings/ContactSettingsViewModel.swift +++ b/LoopFollow/Settings/ContactSettingsViewModel.swift @@ -106,7 +106,6 @@ class ContactSettingsViewModel: ObservableObject { } } - private let storage = ObservableUserDefaults.shared private var cancellables = Set() init() { diff --git a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift deleted file mode 100644 index bc2c1a98d..000000000 --- a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift +++ /dev/null @@ -1,111 +0,0 @@ -// LoopFollow -// ObservableUserDefaultsValue.swift - -import Combine -import Foundation - -class ObservableUserDefaultsValue: ObservableObject, UserDefaultsAnyValue { - // user defaults key (UserDefaultsAnyValue protocol implementation) - let key: String - typealias ValueType = T - - // The value (strong typed) - @Published var value: T { - didSet { - // Continue only if the new value is different from the old value - guard value != oldValue else { return } - - if let validation = validation { - guard let validatedValue = validation(value) else { - DispatchQueue.main.async { - self.value = oldValue - } - return - } - value = validatedValue - } - - // Store value to user defaults - ObservableUserDefaultsValue.defaults.setValue(value.toAny(), forKey: key) - - // Execute custom closure - DispatchQueue.main.async { - self.onChange?(self.value) - - // Notify observers - self.observers.values.forEach { $0(self.value) } - - // Notify UserDefaultsValueGroups that value has changed - UserDefaultsValueGroups.valueChanged(self) - } - } - } - - /// Get/set the value from Any value (UserDefaultsAnyValue protocol implementation) - var anyValue: Any? { - get { - return value.toAny() - } - set { - guard let newValue = T.fromAny(newValue) as T? else { - return - } - DispatchQueue.main.async { - self.value = newValue - } - } - } - - /// Is there this key already stored in UserDefaults? - var exists: Bool { - return ObservableUserDefaultsValue.defaults.object(forKey: key) != nil - } - - // On change closure - private let onChange: ((T) -> Void)? - - // Validate & transform closure : given the new value, validate it; if validation passes, return the new value; - // if validation fails, transform the value, returning a modified version or return nil and the change will not happen - private let validation: ((T) -> T?)? - - // Value change observers - private var observers: [UUID: (T) -> Void] = [:] - - // User defaults used for persistence - private class var defaults: UserDefaults { - return UserDefaults(suiteName: AppConstants.APP_GROUP_ID)! - } - - init(key: String, default defaultValue: T, onChange: ((T) -> Void)? = nil, validation: ((T) -> T?)? = nil) { - self.key = key - self.onChange = onChange - self.validation = validation - - if let anyValue = ObservableUserDefaultsValue.defaults.object(forKey: key), let value = T.fromAny(anyValue) as T? { - self.value = validation?(value) ?? value - } else { - value = defaultValue - } - } - - /// Insert this value in a group, useful for observing changes in the whole group, instead of particular values - func group(_ groupName: String) -> Self { - UserDefaultsValueGroups.add(self, to: groupName) - return self - } - - /// Register observers, will be notified when value changes - @discardableResult - func observeChanges(using closure: @escaping (T) -> Void) -> ObservationToken { - let id = UUID() - observers[id] = closure - - return ObservationToken { [weak self] in - self?.observers.removeValue(forKey: id) - } - } - - func setNil(key: String) { - ObservableUserDefaultsValue.defaults.removeObject(forKey: key) - } -} diff --git a/LoopFollow/Storage/Framework/UserDefaultsValue.swift b/LoopFollow/Storage/Framework/UserDefaultsValue.swift deleted file mode 100644 index 797ae94f1..000000000 --- a/LoopFollow/Storage/Framework/UserDefaultsValue.swift +++ /dev/null @@ -1,121 +0,0 @@ -// LoopFollow -// UserDefaultsValue.swift - -import Foundation - -/// A type that presents a key and a mutable value (type erased key-value) -protocol UserDefaultsAnyValue { - var key: String { get } - var anyValue: Any? { get set } -} - -/// A value holder that keeps its internal data (the value) synchronized with its UserDefaults value. The value is read from UserDefaults on initialization (if exists, otherwise a default value is used) and written when the value changes. Also a custom validation & onChange closure can be set on initialization. -/// -/// Another feature of this class is that when the value changes, the change can be observed by multiple observers. There are two observation levels: the instance level, when the observers register directly to the UserDefaultsValue instance, and group level, if the UserDefaultsValue instance is embeded in a group (UserDefaultsValueGroups class manages the groups). It is very convenient to declare related values in the same group, so when one of them changes, the group observers are notified that a change occured in that group (no need to observe each particular UserDefaultsValue instance). -class UserDefaultsValue: UserDefaultsAnyValue { - // user defaults key (UserDefaultsAnyValue protocol implementation) - let key: String - - typealias ValueType = T - - // the value (strong typed) - var value: T { - didSet { - // continue only if the new value is different than old value - guard value != oldValue else { - return - } - - if let validation = validation { - guard let validatedValue = validation(value) else { - value = oldValue - return - } - - value = validatedValue - } - - // store value to user defaults - UserDefaultsValue.defaults.setValue(value.toAny(), forKey: key) - - // execute custom closure - onChange?(value) - - // notify observers - observers.values.forEach { $0(value) } - - // notify UserDefaultsValueGroups that value has changed - UserDefaultsValueGroups.valueChanged(self) - } - } - - /// get/set the value from Any value (UserDefaultsAnyValue protocol implementation) - var anyValue: Any? { - get { - return value.toAny() - } - - set { - guard let newValue = T.fromAny(newValue) as T? else { - return - } - - value = newValue - } - } - - /// is there this key already stored in UserDefaults? - var exists: Bool { - return UserDefaultsValue.defaults.object(forKey: key) != nil - } - - // on change closure - private let onChange: ((T) -> Void)? - - // validate & transform closure : giving the new value, validate it; if validations passes, return the new value; if fails, transform the value, returning a modified version or ... return nil and the change will not gonna happen - private let validation: ((T) -> T?)? - - // value change observers - private var observers: [UUID: (T) -> Void] = [:] - - // user defaults used for persistence - private class var defaults: UserDefaults { - return UserDefaults(suiteName: AppConstants.APP_GROUP_ID)! - } - - init(key: String, default defaultValue: T, onChange: ((T) -> Void)? = nil, validation: ((T) -> T?)? = nil) { - self.key = key - if let anyValue = UserDefaultsValue.defaults.object(forKey: key), let value = T.fromAny(anyValue) as T? { - if let validation = validation { - self.value = validation(value) ?? defaultValue - } else { - self.value = value - } - } else { - value = defaultValue - } - self.onChange = onChange - self.validation = validation - } - - /// Insert this value in a group, useful for observing changes in the whole group, instead of particular values - func group(_ groupName: String) -> Self { - UserDefaultsValueGroups.add(self, to: groupName) - return self - } - - /// register observers, will be notified when value changes - @discardableResult - func observeChanges(using closure: @escaping (T) -> Void) -> ObservationToken { - let id = UUID() - observers[id] = closure - - return ObservationToken { [weak self] in - self?.observers.removeValue(forKey: id) - } - } - - func setNil(key: String) { - UserDefaultsValue.defaults.removeObject(forKey: key) - } -} diff --git a/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift deleted file mode 100644 index cd03a53cb..000000000 --- a/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift +++ /dev/null @@ -1,66 +0,0 @@ -// LoopFollow -// UserDefaultsValueGroups.swift - -import Foundation - -/// UserDefaultValue groups manager class, providing change observation capabilities (keep clients informed when a change occured in a given group). -class UserDefaultsValueGroups { - class func values(from groupName: String) -> [UserDefaultsAnyValue]? { - return groupNameToValues[groupName] - } - - class func add(_ value: UserDefaultsAnyValue, to groupName: String) { - // add to "value-key to groupNames" dictionary - var groupNames = valueKeyToGroupNames[value.key] ?? [] - guard !groupNames.contains(groupName) else { - // already added value to this group! - return - } - - groupNames.append(groupName) - valueKeyToGroupNames[value.key] = groupNames - - // add to "groupName to value" dictionary - var values = groupNameToValues[groupName] ?? [] - values.append(value) - groupNameToValues[groupName] = values - } - - @discardableResult - class func observeChanges(in groupName: String, using closure: @escaping (UserDefaultsAnyValue, String) -> Void) -> ObservationToken { - let id = UUID() - - var observers = groupNameToObservers[groupName] ?? [:] - observers[id] = closure - groupNameToObservers[groupName] = observers - - return ObservationToken { - groupNameToObservers[groupName]?.removeValue(forKey: id) - } - } - - // called by UserDefaultsValue instances when value changes - class func valueChanged(_ value: UserDefaultsAnyValue) { - valueKeyToGroupNames[value.key]?.forEach { groupName in - notifyValueChanged(value, in: groupName) - } - } - - private class func notifyValueChanged(_ value: UserDefaultsAnyValue, in groupName: String) { - groupNameToObservers[groupName]?.values.forEach { closure in - closure(value, groupName) - } - } - - private static var groupNameToValues: [String: [UserDefaultsAnyValue]] = [:] - private static var valueKeyToGroupNames: [String: [String]] = [:] - private static var groupNameToObservers: [String: [UUID: (UserDefaultsAnyValue, String) -> Void]] = [:] -} - -// user default values group definitions -extension UserDefaultsValueGroups { - enum GroupNames { - static let watchSync = "watchSync" - static let alarm = "alarm" - } -} diff --git a/LoopFollow/Storage/ObservableUserDefaults.swift b/LoopFollow/Storage/ObservableUserDefaults.swift deleted file mode 100644 index d4282db10..000000000 --- a/LoopFollow/Storage/ObservableUserDefaults.swift +++ /dev/null @@ -1,20 +0,0 @@ -// LoopFollow -// ObservableUserDefaults.swift - -import Combine -import Foundation - -/* - Legacy storage, we are moving away from this - */ - -class ObservableUserDefaults { - static let shared = ObservableUserDefaults() - - var old_url = ObservableUserDefaultsValue(key: "url", default: "") - var old_device = ObservableUserDefaultsValue(key: "device", default: "") - var old_nsWriteAuth = ObservableUserDefaultsValue(key: "nsWriteAuth", default: false) - var old_nsAdminAuth = ObservableUserDefaultsValue(key: "nsAdminAuth", default: false) - - private init() {} -} diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index b2274675d..30a23cbfd 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -159,1679 +159,6 @@ extension Storage { nightscoutPosition.value = .position4 } - func migrateStep1() { - // Guard each field with .exists so that if the App Group suite is unreadable - // (e.g. Before-First-Unlock state after a reboot), we skip the write rather - // than overwriting the already-migrated Standard value with an empty default. - if ObservableUserDefaults.shared.old_url.exists { - Storage.shared.url.value = ObservableUserDefaults.shared.old_url.value - } - if ObservableUserDefaults.shared.old_device.exists { - Storage.shared.device.value = ObservableUserDefaults.shared.old_device.value - } - if ObservableUserDefaults.shared.old_nsWriteAuth.exists { - Storage.shared.nsWriteAuth.value = ObservableUserDefaults.shared.old_nsWriteAuth.value - } - if ObservableUserDefaults.shared.old_nsAdminAuth.exists { - Storage.shared.nsAdminAuth.value = ObservableUserDefaults.shared.old_nsAdminAuth.value - } - - // Helper: 1-to-1 type ----------------------------------------------------------------- - func move( - _ legacy: @autoclosure () -> UserDefaultsValue, - into newValue: StorageValue - ) { - let item = legacy() - guard item.exists else { return } - newValue.value = item.value - item.setNil(key: item.key) - } - - // Helper: Float → Double ------------------------------------------------------------ - func moveFloatToDouble( - _ legacy: @autoclosure () -> UserDefaultsValue, - into newValue: StorageValue - ) { - let item = legacy() - guard item.exists else { return } - newValue.value = Double(item.value) - item.setNil(key: item.key) - } - - // Remove this in a year later than the release of the new Alarms [BEGIN] - let legacyColorBGText = UserDefaultsValue(key: "colorBGText", default: true) - if legacyColorBGText.exists { - Storage.shared.colorBGText.value = legacyColorBGText.value - legacyColorBGText.setNil(key: "colorBGText") - } - - let legacyAppBadge = UserDefaultsValue(key: "appBadge", default: true) - if legacyAppBadge.exists { - Storage.shared.appBadge.value = legacyAppBadge.value - legacyAppBadge.setNil(key: "appBadge") - } - - let legacyForceDarkMode = UserDefaultsValue(key: "forceDarkMode", default: true) - if legacyForceDarkMode.exists { - // Migrate from Bool to AppearanceMode: true -> .dark, false -> .system - Storage.shared.appearanceMode.value = legacyForceDarkMode.value ? .dark : .system - legacyForceDarkMode.setNil(key: "forceDarkMode") - } - - let legacyShowStats = UserDefaultsValue(key: "showStats", default: true) - if legacyShowStats.exists { - Storage.shared.showStats.value = legacyShowStats.value - legacyShowStats.setNil(key: "showStats") - } - - let legacyUseIFCC = UserDefaultsValue(key: "useIFCC", default: false) - if legacyUseIFCC.exists { - Storage.shared.useIFCC.value = legacyUseIFCC.value - legacyUseIFCC.setNil(key: "useIFCC") - } - - let legacyShowSmallGraph = UserDefaultsValue(key: "showSmallGraph", default: true) - if legacyShowSmallGraph.exists { - Storage.shared.showSmallGraph.value = legacyShowSmallGraph.value - legacyShowSmallGraph.setNil(key: "showSmallGraph") - } - - let legacyScreenlockSwitchState = UserDefaultsValue(key: "screenlockSwitchState", default: true) - if legacyScreenlockSwitchState.exists { - Storage.shared.screenlockSwitchState.value = legacyScreenlockSwitchState.value - legacyScreenlockSwitchState.setNil(key: "screenlockSwitchState") - } - - let legacyShowDisplayName = UserDefaultsValue(key: "showDisplayName", default: false) - if legacyShowDisplayName.exists { - Storage.shared.showDisplayName.value = legacyShowDisplayName.value - legacyShowDisplayName.setNil(key: "showDisplayName") - } - - let legacySpeakBG = UserDefaultsValue(key: "speakBG", default: false) - if legacySpeakBG.exists { - Storage.shared.speakBG.value = legacySpeakBG.value - legacySpeakBG.setNil(key: "speakBG") - } - - let legacySpeakBGAlways = UserDefaultsValue(key: "speakBGAlways", default: true) - if legacySpeakBGAlways.exists { - Storage.shared.speakBGAlways.value = legacySpeakBGAlways.value - legacySpeakBGAlways.setNil(key: "speakBGAlways") - } - - let legacySpeakLowBG = UserDefaultsValue(key: "speakLowBG", default: false) - if legacySpeakLowBG.exists { - Storage.shared.speakLowBG.value = legacySpeakLowBG.value - legacySpeakLowBG.setNil(key: "speakLowBG") - } - - let legacySpeakProactiveLowBG = UserDefaultsValue(key: "speakProactiveLowBG", default: false) - if legacySpeakProactiveLowBG.exists { - Storage.shared.speakProactiveLowBG.value = legacySpeakProactiveLowBG.value - legacySpeakProactiveLowBG.setNil(key: "speakProactiveLowBG") - } - - let legacySpeakFastDropDelta = UserDefaultsValue(key: "speakFastDropDelta", default: 10.0) - if legacySpeakFastDropDelta.exists { - Storage.shared.speakFastDropDelta.value = Double(legacySpeakFastDropDelta.value) - legacySpeakFastDropDelta.setNil(key: "speakFastDropDelta") - } - - let legacySpeakLowBGLimit = UserDefaultsValue(key: "speakLowBGLimit", default: 72.0) - if legacySpeakLowBGLimit.exists { - Storage.shared.speakLowBGLimit.value = Double(legacySpeakLowBGLimit.value) - legacySpeakLowBGLimit.setNil(key: "speakLowBGLimit") - } - - let legacySpeakHighBGLimit = UserDefaultsValue(key: "speakHighBGLimit", default: 180.0) - if legacySpeakHighBGLimit.exists { - Storage.shared.speakHighBGLimit.value = Double(legacySpeakHighBGLimit.value) - legacySpeakHighBGLimit.setNil(key: "speakHighBGLimit") - } - - let legacySpeakHighBG = UserDefaultsValue(key: "speakHighBG", default: false) - if legacySpeakHighBG.exists { - Storage.shared.speakHighBG.value = legacySpeakHighBG.value - legacySpeakHighBG.setNil(key: "speakHighBG") - } - - let legacySpeakLanguage = UserDefaultsValue(key: "speakLanguage", default: "en") - if legacySpeakLanguage.exists { - Storage.shared.speakLanguage.value = legacySpeakLanguage.value - legacySpeakLanguage.setNil(key: "speakLanguage") - } - - move(UserDefaultsValue(key: "persistentNotification", default: true), into: Storage.shared.persistentNotification) - - // ── General (done earlier, but safe to repeat) ── - move(UserDefaultsValue(key: "colorBGText", default: true), into: Storage.shared.colorBGText) - move(UserDefaultsValue(key: "appBadge", default: true), into: appBadge) - // Note: forceDarkMode migration to appearanceMode is handled earlier in migrateGeneralSettings() - move(UserDefaultsValue(key: "showStats", default: true), into: showStats) - move(UserDefaultsValue(key: "useIFCC", default: false), into: useIFCC) - move(UserDefaultsValue(key: "showSmallGraph", default: true), into: showSmallGraph) - move(UserDefaultsValue(key: "screenlockSwitchState", default: false), into: screenlockSwitchState) - move(UserDefaultsValue(key: "showDisplayName", default: false), into: showDisplayName) - - // ── Speak-BG ── - move(UserDefaultsValue(key: "speakBG", default: false), into: speakBG) - move(UserDefaultsValue(key: "speakBGAlways", default: true), into: speakBGAlways) - move(UserDefaultsValue(key: "speakLowBG", default: false), into: speakLowBG) - move(UserDefaultsValue(key: "speakProactiveLowBG", default: false), into: speakProactiveLowBG) - move(UserDefaultsValue(key: "speakHighBG", default: false), into: speakHighBG) - moveFloatToDouble(UserDefaultsValue(key: "speakLowBGLimit", default: 72.0), into: speakLowBGLimit) - moveFloatToDouble(UserDefaultsValue(key: "speakHighBGLimit", default: 180.0), into: speakHighBGLimit) - moveFloatToDouble(UserDefaultsValue(key: "speakFastDropDelta", default: 10.0), into: speakFastDropDelta) - move(UserDefaultsValue(key: "speakLanguage", default: "en"), into: speakLanguage) - - // ── Graph ── - move(UserDefaultsValue(key: "showDots", default: true), into: showDots) - move(UserDefaultsValue(key: "showLines", default: true), into: showLines) - move(UserDefaultsValue(key: "showValues", default: true), into: showValues) - move(UserDefaultsValue(key: "showAbsorption", default: true), into: showAbsorption) - move(UserDefaultsValue(key: "showDIAMarkers", default: true), into: showDIALines) - move(UserDefaultsValue(key: "show30MinLine", default: false), into: show30MinLine) - move(UserDefaultsValue(key: "show90MinLine", default: false), into: show90MinLine) - move(UserDefaultsValue(key: "showMidnightMarkers", default: false), into: showMidnightLines) - move(UserDefaultsValue(key: "smallGraphTreatments", default: true), into: smallGraphTreatments) - - move(UserDefaultsValue(key: "smallGraphHeight", default: 40), into: smallGraphHeight) - move(UserDefaultsValue(key: "predictionToLoad", default: 1.0), into: predictionToLoad) - move(UserDefaultsValue(key: "minBasalScale", default: 5.0), into: minBasalScale) - moveFloatToDouble(UserDefaultsValue(key: "minBGScale", default: 250.0), into: minBGScale) - moveFloatToDouble(UserDefaultsValue(key: "lowLine", default: 70.0), into: lowLine) - moveFloatToDouble(UserDefaultsValue(key: "highLine", default: 180.0), into: highLine) - move(UserDefaultsValue(key: "downloadDays", default: 1), into: downloadDays) - // Remove this in a year later than the release of the new Alarms [END] - - // ── Watch / Calendar ──────────────────────────────────────────────── - move(UserDefaultsValue(key: "writeCalendarEvent", default: false), into: writeCalendarEvent) - move(UserDefaultsValue(key: "calendarIdentifier", default: ""), into: calendarIdentifier) - move(UserDefaultsValue(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%"), into: watchLine1) - move(UserDefaultsValue(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%"), into: watchLine2) - - // Migration of generic alarm settings - // ── AlarmConfiguration migration ───────────────────────────────────────── - do { - // Work on a mutable copy, then write the whole thing back once. - var cfg = Storage.shared.alarmConfiguration.value - let cal = Calendar.current - - /// Copy *one* legacy value → struct field → delete old key - func move( - _ legacy: @autoclosure () -> UserDefaultsValue, - write: (inout AlarmConfiguration, T) -> Void - ) { - let item = legacy() - guard item.exists else { return } - write(&cfg, item.value) - item.setNil(key: item.key) - } - - // 1. Override-volume toggle - move(UserDefaultsValue(key: "overrideSystemOutputVolume", - default: cfg.overrideSystemOutputVolume)) - { - $0.overrideSystemOutputVolume = $1 - } - - // 2. Forced output volume itself. - // Prefer newer key (“forcedOutputVolume”); otherwise fall back. - if UserDefaultsValue(key: "forcedOutputVolume", - default: cfg.forcedOutputVolume).exists - { - move(UserDefaultsValue(key: "forcedOutputVolume", - default: cfg.forcedOutputVolume)) - { - $0.forcedOutputVolume = $1 - } - } else { - move(UserDefaultsValue(key: "systemOutputVolume", - default: cfg.forcedOutputVolume)) - { - $0.forcedOutputVolume = $1 - } - } - - // 3. Play audio during phone calls - move(UserDefaultsValue(key: "alertAudioDuringPhone", - default: cfg.audioDuringCalls)) - { - $0.audioDuringCalls = $1 - } - - // 4. Auto-snooze CGM-start alarm - move(UserDefaultsValue(key: "alertAutoSnoozeCGMStart", - default: cfg.autoSnoozeCGMStart)) - { - $0.autoSnoozeCGMStart = $1 - } - - // 5. Global “Snooze all” → snoozeUntil - move(UserDefaultsValue(key: "alertSnoozeAllTime", - default: cfg.snoozeUntil)) - { - $0.snoozeUntil = $1 - } - - // 6. Global “Mute all” → muteUntil - move(UserDefaultsValue(key: "alertMuteAllTime", - default: cfg.muteUntil)) - { - $0.muteUntil = $1 - } - - // 7 & 8. Legacy quiet-hours → day/night start - // (only if both dates exist and are on the same “reference” day) - let qStart = UserDefaultsValue(key: "quietHourStart", default: nil) - let qEnd = UserDefaultsValue(key: "quietHourEnd", default: nil) - if let s = qStart.value, let e = qEnd.value { - let compsStart = cal.dateComponents([.hour, .minute], from: s) - let compsEnd = cal.dateComponents([.hour, .minute], from: e) - - if let sh = compsStart.hour, let sm = compsStart.minute, - let eh = compsEnd.hour, let em = compsEnd.minute - { - cfg.nightStart = TimeOfDay(hour: sh, minute: sm) - cfg.dayStart = TimeOfDay(hour: eh, minute: em) - qStart.setNil(key: qStart.key) - qEnd.setNil(key: qEnd.key) - } - } - - // 9. Legacy “ignore zero BG” flag → ignoreZeroBG - move(UserDefaultsValue(key: "alertIgnoreZero", - default: cfg.ignoreZeroBG)) - { - $0.ignoreZeroBG = $1 - } - - // finally persist the whole struct - Storage.shared.alarmConfiguration.value = cfg - } - - // ── Dexcom Share -------------------------------------------------------- - move(UserDefaultsValue(key: "shareUserName", default: ""), - into: Storage.shared.shareUserName) - - move(UserDefaultsValue(key: "sharePassword", default: ""), - into: Storage.shared.sharePassword) - - move(UserDefaultsValue(key: "shareServer", default: "US"), - into: Storage.shared.shareServer) - - // ── Graph --------------------------------------------------------------- - moveFloatToDouble( - UserDefaultsValue(key: "chartScaleX", default: 18.0), - into: Storage.shared.chartScaleX - ) - - // ── Advanced settings --------------------------------------------------- - move(UserDefaultsValue(key: "downloadTreatments", default: true), - into: Storage.shared.downloadTreatments) - move(UserDefaultsValue(key: "downloadPrediction", default: true), - into: Storage.shared.downloadPrediction) - move(UserDefaultsValue(key: "graphOtherTreatments", default: true), - into: Storage.shared.graphOtherTreatments) - move(UserDefaultsValue(key: "graphBasal", default: true), - into: Storage.shared.graphBasal) - move(UserDefaultsValue(key: "graphBolus", default: true), - into: Storage.shared.graphBolus) - move(UserDefaultsValue(key: "graphCarbs", default: true), - into: Storage.shared.graphCarbs) - move(UserDefaultsValue(key: "bgUpdateDelay", default: 10), - into: Storage.shared.bgUpdateDelay) - - // ── Insert times -------------------------------------------------------- - move(UserDefaultsValue(key: "alertCageInsertTime", default: 0), - into: Storage.shared.cageInsertTime) - move(UserDefaultsValue(key: "alertSageInsertTime", default: 0), - into: Storage.shared.sageInsertTime) - - // ── Version-cache / notification bookkeeping --------------------------- - move(UserDefaultsValue(key: "cachedForVersion", default: nil), - into: Storage.shared.cachedForVersion) - move(UserDefaultsValue(key: "latestVersion", default: nil), - into: Storage.shared.latestVersion) - move(UserDefaultsValue(key: "latestVersionChecked", default: nil), - into: Storage.shared.latestVersionChecked) - move(UserDefaultsValue(key: "currentVersionBlackListed", default: false), - into: Storage.shared.currentVersionBlackListed) - move(UserDefaultsValue(key: "lastBlacklistNotificationShown", default: nil), - into: Storage.shared.lastBlacklistNotificationShown) - move(UserDefaultsValue(key: "lastVersionUpdateNotificationShown", default: nil), - into: Storage.shared.lastVersionUpdateNotificationShown) - move(UserDefaultsValue(key: "lastExpirationNotificationShown", default: nil), - into: Storage.shared.lastExpirationNotificationShown) - - move(UserDefaultsValue(key: "hideInfoTable", default: false), into: Storage.shared.hideInfoTable) - move(UserDefaultsValue(key: "token", default: ""), into: Storage.shared.token) - move(UserDefaultsValue(key: "units", default: "mg/dL"), into: Storage.shared.units) - - move(UserDefaultsValue<[Int]>(key: "infoSort", - default: InfoType.allCases.map { $0.sortOrder }), - into: Storage.shared.infoSort) - - move(UserDefaultsValue<[Bool]>(key: "infoVisible", - default: InfoType.allCases.map { $0.defaultVisible }), - into: Storage.shared.infoVisible) - - migrateUrgentLowAlarm() - migrateLowAlarm() - migrateHighAlarm() - migrateUrgentHighAlarm() - migrateFastDropAlarm() - migrateFastRiseAlarm() - migrateMissedReadingAlarm() - migrateNotLoopingAlarm() - migrateMissedBolusAlarm() - migrateSensorChangeAlarm() - migratePumpChangeAlarm() - migrateOverrideStartAlarm() - migrateOverrideEndAlarm() - migrateTempTargetStartAlarm() - migrateTempTargetEndAlarm() - migrateTemporaryBGAlarm() - migratePumpVolumeAlarm() - migrateIOBAlarm() - migrateCOBAlarm() - migrateBatteryAlarm() - migrateBatteryDropAlarm() - migrateRecBolusAlarm() - } - - // MARK: - One-off alarm migrations - - /// Reads *all* `alertUrgentLow*` keys, converts them into a single `Alarm`, - /// saves it to the modern `[Alarm]` store, then deletes the legacy keys. - private func migrateUrgentLowAlarm() { - // Did the user ever change that alert? (No key ⇒ nothing to do.) - guard UserDefaultsValue(key: "alertUrgentLowActive", default: false).exists else { return } - - /// Helper: fetch-then-delete a legacy value in one line. - func take(_ key: String, default def: V) -> V { - let box = UserDefaultsValue(key: key, default: def) - defer { box.setNil(key: key) } - return box.value - } - - // Build the new Alarm ------------------------------------------------ - var alarm = Alarm(type: .low) - alarm.name = "Urgent Low" - alarm.isEnabled = take("alertUrgentLowActive", default: false) - alarm.belowBG = Double(take("alertUrgentLowBG", default: 55.0)) - alarm.predictiveMinutes = take("alertUrgentLowPredictiveMinutes", default: 0) - alarm.snoozeDuration = take("alertUrgentLowSnooze", default: 5) - alarm.snoozedUntil = take("alertUrgentLowSnoozedTime", default: nil as Date?) - alarm.soundFile = SoundFile( - rawValue: take("alertUrgentLowSound", - default: "Emergency_Alarm_Siren")) - ?? .emergencyAlarmSiren - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertUrgentLowAudible", - default: "Always").lowercased()) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertUrgentLowRepeat", - default: "Always").lowercased()) ?? .always - - // Day / Night active-window (“Pre-Snooze”) - let autoStr = take("alertUrgentLowAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertUrgentLowAutosnoozeDay", default: false) - let nightFlag = take("alertUrgentLowAutosnoozeNight", default: false) - - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always // “Never” → always active - } - }() - - // Persist ----------------------------------------------------------------- - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - Low-BG alarm ------------------------------------------------------- - - private func migrateLowAlarm() { - // Bail if the old keys were never written - guard UserDefaultsValue(key: "alertLowActive", default: false).exists else { return } - - // tiny helper: fetch value → erase key - func take(_ key: String, default def: V) -> V { - let box = UserDefaultsValue(key: key, default: def) - defer { box.setNil(key: key) } - return box.value - } - - // Build the new Alarm ---------------------------------------------------- - var alarm = Alarm(type: .low) - alarm.name = "Low" - alarm.isEnabled = take("alertLowActive", default: false) - alarm.belowBG = Double(take("alertLowBG", default: 70.0)) - - // “Persistent ≥ X min” → `persistentMinutes` - alarm.persistentMinutes = take("alertLowPersistent", default: 0) - - // “Persistence max BG drop” -- ignoring this for now - // let persistentLowTriggerImmediatelyBG = UserDefaultsRepository.alertLowBG.value - UserDefaultsRepository.alertLowPersistenceMax.value - // (Float(persistentLowBG) <= UserDefaultsRepository.alertLowBG.value || Float(currentBG) <= persistentLowTriggerImmediatelyBG) - _ = Double(take("alertLowPersistenceMax", default: 5.0)) - - alarm.snoozeDuration = take("alertLowSnooze", default: 5) - alarm.snoozedUntil = take("alertLowSnoozedTime", default: nil as Date?) - alarm.soundFile = SoundFile( - rawValue: take("alertLowSound", - default: "Indeed")) ?? .indeed - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertLowAudible", - default: "Always").lowercased()) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertLowRepeat", - default: "Always").lowercased()) ?? .always - - // activeOption ← legacy “Pre-Snooze” flags / picker - let autoStr = take("alertLowAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertLowAutosnoozeDay", default: false) - let nightFlag = take("alertLowAutosnoozeNight", default: false) - - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always - } - }() - - // Done → append to the modern store - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - High-BG alarm ----------------------------------------------------- - - private func migrateHighAlarm() { - // Only run if the legacy key ever existed - guard UserDefaultsValue(key: "alertHighActive", default: false).exists else { return } - - /// Fetch → erase helper - func take(_ key: String, default def: V) -> V { - let box = UserDefaultsValue(key: key, default: def) - defer { box.setNil(key: key) } // remove legacy value - return box.value - } - - // ---------- Build Alarm ----------------------------------------------- - var alarm = Alarm(type: .high) - alarm.name = "High" - alarm.isEnabled = take("alertHighActive", default: false) - alarm.aboveBG = Double(take("alertHighBG", default: 180.0)) - - alarm.persistentMinutes = take("alertHighPersistent", default: 60) - alarm.snoozeDuration = take("alertHighSnooze", default: 60) - alarm.snoozedUntil = take("alertHighSnoozedTime", default: nil as Date?) - - alarm.soundFile = SoundFile( - rawValue: take("alertHighSound", - default: "Time_Has_Come")) ?? .timeHasCome - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertHighAudible", - default: "Always").lowercased()) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertHighRepeat", - default: "Always").lowercased()) ?? .always - - // ── activeOption derived from “Pre-Snooze” picker & flags - let autoStr = take("alertHighAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertHighAutosnoozeDay", default: false) - let nightFlag = take("alertHighAutosnoozeNight", default: false) - - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always - } - }() - - // ---------- Persist & we’re done -------------------------------------- - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - Urgent-High alarm -------------------------------------------------- - - private func migrateUrgentHighAlarm() { - // run only once, only if the user ever changed that toggle - guard UserDefaultsValue(key: "alertUrgentHighActive", default: false).exists else { return } - - // helper: read-then-erase a legacy value - func take(_ key: String, default def: V) -> V { - let box = UserDefaultsValue(key: key, default: def) - defer { box.setNil(key: key) } // wipe legacy key - return box.value - } - - // ───────── Build the Alarm ──────────────────────────────────────────── - var alarm = Alarm(type: .high) // we map to the existing `.high` type - alarm.name = "Urgent High" - alarm.isEnabled = take("alertUrgentHighActive", default: false) - alarm.aboveBG = Double(take("alertUrgentHighBG", default: 250.0)) - - alarm.snoozeDuration = take("alertUrgentHighSnooze", default: 30) - alarm.snoozedUntil = take("alertUrgentHighSnoozedTime", default: nil as Date?) - - alarm.soundFile = SoundFile( - rawValue: take("alertUrgentHighSound", - default: "Pager_Beeps")) ?? .pagerBeeps - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertUrgentHighAudible", - default: "Always").lowercased()) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertUrgentHighRepeat", - default: "Always").lowercased()) ?? .always - - // activeOption comes from the old “Pre-Snooze” picker + its day/night flags - let autoStr = take("alertUrgentHighAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertUrgentHighAutosnoozeDay", default: false) - let nightFlag = take("alertUrgentHighAutosnoozeNight", default: false) - - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { // fall back to picker value - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always - } - }() - - // ───────── Persist in new storage ───────────────────────────────────── - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - Fast-Drop alarm ---------------------------------------------------- - - private func migrateFastDropAlarm() { - guard UserDefaultsValue(key: "alertFastDropDeltaActive", - default: false).exists else { return } - - // helper: read-then-erase - func take(_ k: String, default d: V) -> V { - let box = UserDefaultsValue(key: k, default: d) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .fastDrop) - alarm.name = "Fast Drop" - alarm.isEnabled = take("alertFastDropDeltaActive", default: false) - - // core trigger parameters - alarm.delta = Double(take("alertFastDropDelta", default: 10.0)) - alarm.monitoringWindow = take("alertFastDropReadings", default: 3) - 1 // store #readings - if take("alertFastDropUseLimit", default: false) { - alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0)) - } - - // snoozing - alarm.snoozeDuration = take("alertFastDropDeltaSnooze", default: 10) - alarm.snoozedUntil = take("alertFastDropSnoozedTime", default: nil as Date?) - - // sound + options - alarm.soundFile = SoundFile( - rawValue: take("alertFastDropSound", default: "Big_Clock_Ticking") - ) ?? .bigClockTicking - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertFastDropAudible", default: "Always").lowercased() - ) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertFastDropRepeat", default: "Never").lowercased() - ) ?? .never - - // activeOption from old “Pre-Snooze” picker + day/night flags - let autoStr = take("alertFastDropAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertFastDropAutosnoozeDay", default: false) - let nightFlag = take("alertFastDropAutosnoozeNight", default: false) - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always - } - }() - - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - Fast-Rise alarm ---------------------------------------------------- - - private func migrateFastRiseAlarm() { - guard UserDefaultsValue(key: "alertFastRiseDeltaActive", - default: false).exists else { return } - - func take(_ k: String, default d: V) -> V { - let box = UserDefaultsValue(key: k, default: d) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .fastRise) - alarm.name = "Fast Rise" - alarm.isEnabled = take("alertFastRiseDeltaActive", default: false) - - alarm.delta = Double(take("alertFastRiseDelta", default: 10.0)) - alarm.monitoringWindow = take("alertFastRiseReadings", default: 3) - if take("alertFastRiseUseLimit", default: false) { - alarm.aboveBG = Double(take("alertFastRiseAboveBG", default: 200.0)) - } - - alarm.snoozeDuration = take("alertFastRiseDeltaSnooze", default: 10) - alarm.snoozedUntil = take("alertFastRiseSnoozedTime", default: nil as Date?) - - alarm.soundFile = SoundFile( - rawValue: take("alertFastRiseSound", - default: "Cartoon_Fail_Strings_Trumpet") - ) ?? .cartoonFailStringsTrumpet - - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertFastRiseAudible", default: "Always").lowercased() - ) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertFastRiseRepeat", default: "Never").lowercased() - ) ?? .never - - let autoStr = take("alertFastRiseAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertFastRiseAutosnoozeDay", default: false) - let nightFlag = take("alertFastRiseAutosnoozeNight", default: false) - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always - } - }() - - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - Missed-Reading alarm --------------------------------------------- - - private func migrateMissedReadingAlarm() { - // Run only when the user has ever modified those settings - guard UserDefaultsValue(key: "alertMissedReadingActive", - default: false).exists else { return } - - // read-then-erase helper - func take(_ k: String, default d: V) -> V { - let b = UserDefaultsValue(key: k, default: d) - defer { b.setNil(key: k) } - return b.value - } - - var alarm = Alarm(type: .missedReading) - alarm.name = "Missed Reading" - alarm.isEnabled = take("alertMissedReadingActive", default: false) - - // “No CGM data for X minutes” - alarm.threshold = take("alertMissedReading", default: 31) - - // snoozing - alarm.snoozeDuration = take("alertMissedReadingSnooze", default: 30) - alarm.snoozedUntil = take("alertMissedReadingSnoozedTime", - default: nil as Date?) - // (legacy “is-snoozed” flag is implicit in snoozedUntil) - - // sound - alarm.soundFile = SoundFile( - rawValue: take("alertMissedReadingSound", - default: "Cartoon_Tip_Toe_Sneaky_Walk") - ) ?? .cartoonTipToeSneakyWalk - - // play / repeat options - alarm.playSoundOption = PlaySoundOption( - rawValue: take("alertMissedReadingAudible", - default: "Always").lowercased() - ) ?? .always - - alarm.repeatSoundOption = RepeatSoundOption( - rawValue: take("alertMissedReadingRepeat", - default: "Never").lowercased() - ) ?? .never - - // activeOption ← “Pre-Snooze” picker + day/night flags - let autoStr = take("alertMissedReadingAutosnooze", default: "Never") - .lowercased() - let dayFlag = take("alertMissedReadingAutosnoozeDay", default: false) - let nightFlag = take("alertMissedReadingAutosnoozeNight", default: false) - - alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } - switch autoStr { - case "day", "at day": return .day - case "night", "at night": return .night - default: return .always // “Never” → always on - } - }() - - // store in the new world - Storage.shared.alarms.value.append(alarm) - } - - // MARK: - “Not Looping” (legacy → Storage.shared.alarms) - - private func migrateNotLoopingAlarm() { - // Check if the user ever configured this alarm - let activeFlag = UserDefaultsValue(key: "alertNotLoopingActive", default: false) - guard activeFlag.exists else { return } // nothing to migrate - - // Convenience: read-then-erase - func take(_ key: String, default def: V) -> V { - let box = UserDefaultsValue(key: key, default: def) - defer { box.setNil(key: key) } // wipe after reading - return box.value - } - - // Build the new Alarm --------------------------------------------------- - var alarm = Alarm(type: .notLooping) - alarm.name = "Not Looping Alert" - alarm.isEnabled = take("alertNotLoopingActive", default: false) - alarm.threshold = Double(take("alertNotLooping", default: 31)) // minutes - alarm.snoozeDuration = take("alertNotLoopingSnooze", default: 30) - alarm.snoozedUntil = take("alertNotLoopingSnoozedTime", default: nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertNotLoopingSound", - default: "Sci-Fi_Engine_Shut_Down")) ?? .sciFiEngineShutDown - - // ── ACTIVE-DURING (day/night) ← old **Pre-Snooze** flags -------------- - let actDay = take("alertNotLoopingAutosnoozeDay", default: false) - let actNight = take("alertNotLoopingAutosnoozeNight", default: false) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always // “Never” in old UI - } - }() - - // ── PLAY-SOUND option --------------------------------------------------- - let playStr = take("alertNotLoopingAudible", default: "Always").lowercased() - let playDay = take("alertNotLoopingDayTimeAudible", default: true) - let playNight = take("alertNotLoopingNightTimeAudible", default: true) - alarm.playSoundOption = { - if !playDay, !playNight { return .never } - else if playDay, playNight { return .always } - else if playDay { return .day } - else { return .night } - }() - - // ── REPEAT-SOUND option ------------------------------------------------- - let repStr = take("alertNotLoopingRepeat", default: "Never").lowercased() - let repDay = take("alertNotLoopingDayTime", default: false) - let repNight = take("alertNotLoopingNightTime", default: false) - alarm.repeatSoundOption = { - if repDay, repNight { return .always } - else if repDay, !repNight { return .day } - else if repNight, !repDay { return .night } - else { return .never } - }() - - // ── BG-limit guard ------------------------------------------------------ - if take("alertNotLoopingUseLimits", default: false) { - alarm.belowBG = Double(take("alertNotLoopingLowerLimit", default: 100.0)) - alarm.aboveBG = Double(take("alertNotLoopingUpperLimit", default: 160.0)) - } - - // ── Per-alarm snooze state --------------------------------------------- - if !take("alertNotLoopingIsSnoozed", default: false) { - alarm.snoozedUntil = nil // ignore stored date if flag isn’t set - } - - // Persist & finish ------------------------------------------------------- - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // MARK: – Missed-Bolus alarm ------------------------------------------------- - - private func migrateMissedBolusAlarm() { - // Was the old alarm ever configured? - let legacyActive = UserDefaultsValue(key: "alertMissedBolusActive", - default: false) - guard legacyActive.exists else { return } // nothing to do - - // helper: read-then-delete ------------------------------------------------ - func take(_ k: String, - _ def: V) -> V - { - let box = UserDefaultsValue(key: k, default: def) - defer { box.setNil(key: k) } // wipe after reading - return box.value - } - - // ──────────────────────────────────────────────────────────────────────── - // Build the new Alarm - // ──────────────────────────────────────────────────────────────────────── - var alarm = Alarm(type: .missedBolus) - alarm.name = "Missed Bolus Alert" - alarm.isEnabled = take("alertMissedBolusActive", false) - - // core timings - alarm.monitoringWindow = take("alertMissedBolus", 10) // delay - alarm.predictiveMinutes = take("alertMissedBolusPrebolus", 20) // pre-bolus window - alarm.snoozeDuration = take("alertMissedBolusSnooze", 10) - - // snooze-state - if take("alertMissedBolusIsSnoozed", false) { - alarm.snoozedUntil = take("alertMissedBolusSnoozedTime", - nil as Date?) - } - - // carb / bolus filters - alarm.delta = take("alertMissedBolusIgnoreBolus", 0.5) - alarm.threshold = Double( - take("alertMissedBolusLowGrams", 10)) - alarm.aboveBG = Double( - take("alertMissedBolusLowGramsBG", 70.0)) - - // sound & tone - alarm.soundFile = SoundFile(rawValue: - take("alertMissedBolusSound", - "Dhol_Shuffleloop")) ?? .dholShuffleloop - - // ── ACTIVE-DURING ← old “Pre-Snooze” flags - let actDay = take("alertMissedBolusAutosnoozeDay", false) - let actNight = take("alertMissedBolusAutosnoozeNight", false) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always // “Never” → always - } - }() - - // ── PLAY-SOUND ← old “PlaySound” picker - let playDay = take("alertMissedBolusDayTimeAudible", true) - let playNight = take("alertMissedBolusNightTimeAudible", true) - alarm.playSoundOption = { - switch (playDay, playNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // ── REPEAT-SOUND ← old “Repeat Sound” picker - let repDay = take("alertMissedBolusDayTime", false) - let repNight = take("alertMissedBolusNightTime", false) - alarm.repeatSoundOption = { - switch (repDay, repNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // (The deprecated “alertMissedBolusQuiet” key is ignored.) - - // ── Store & finish ------------------------------------------------------ - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // ───────────────────────────────────────────────────────────────────────────── - - // MARK: SAGE → .sensorChange - - // ───────────────────────────────────────────────────────────────────────────── - private func migrateSensorChangeAlarm() { - // Was the old setting ever stored? - let flag = UserDefaultsValue(key: "alertSAGEActive", default: false) - guard flag.exists else { return } - - // tiny helper that *reads + wipes* a legacy key - func take(_ k: String, _ def: V) -> V { - let b = UserDefaultsValue(key: k, default: def) - defer { b.setNil(key: k) } - return b.value - } - - var alarm = Alarm(type: .sensorChange) - alarm.name = "Sensor Change Reminder" - alarm.isEnabled = take("alertSAGEActive", false) - alarm.threshold = Double(take("alertSAGE", 8)) // hours - alarm.snoozeDuration = take("alertSAGESnooze", 2) // hours - if take("alertSAGEIsSnoozed", false) { - alarm.snoozedUntil = take("alertSAGESnoozedTime", nil as Date?) - } - alarm.soundFile = SoundFile(rawValue: - take("alertSAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou - - // ACTIVE (day / night) - let actDay = take("alertSAGEAutosnoozeDay", false) - let actNight = take("alertSAGEAutosnoozeNight", true) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always - } - }() - - // PLAY sound - let playDay = take("alertSAGEDayTimeAudible", true) - let playNight = take("alertSAGENightTimeAudible", true) - alarm.playSoundOption = { - switch (playDay, playNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // REPEAT sound - let repDay = take("alertSAGEDayTime", false) - let repNight = take("alertSAGENightTime", false) - alarm.repeatSoundOption = { - switch (repDay, repNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // Persist - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // ───────────────────────────────────────────────────────────────────────────── - - // MARK: CAGE → .pumpChange - - // ───────────────────────────────────────────────────────────────────────────── - private func migratePumpChangeAlarm() { - let flag = UserDefaultsValue(key: "alertCAGEActive", default: false) - guard flag.exists else { return } - - func take(_ k: String, _ def: V) -> V { - let b = UserDefaultsValue(key: k, default: def) - defer { b.setNil(key: k) } - return b.value - } - - var alarm = Alarm(type: .pumpChange) - alarm.name = "Pump / Cannula Change" - alarm.isEnabled = take("alertCAGEActive", false) - alarm.threshold = Double(take("alertCAGE", 4)) // hours - alarm.snoozeDuration = take("alertCAGESnooze", 2) // hours - if take("alertCAGEIsSnoozed", false) { - alarm.snoozedUntil = take("alertCAGESnoozedTime", nil as Date?) - } - alarm.soundFile = SoundFile(rawValue: - take("alertCAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou - - // ACTIVE - let actDay = take("alertCAGEAutosnoozeDay", false) - let actNight = take("alertCAGEAutosnoozeNight", true) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always - } - }() - - // PLAY - let playDay = take("alertCAGEDayTimeAudible", true) - let playNight = take("alertCAGENightTimeAudible", true) - alarm.playSoundOption = { - switch (playDay, playNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // REPEAT - let repDay = take("alertCAGEDayTime", false) - let repNight = take("alertCAGENightTime", false) - alarm.repeatSoundOption = { - switch (repDay, repNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // ───────────────────────────────────────────────────────────────────────────── - - // MARK: Override-Start → .overrideStart - - // ───────────────────────────────────────────────────────────────────────────── - private func migrateOverrideStartAlarm() { - let exists = UserDefaultsValue(key: "alertOverrideStart", default: false) - guard exists.exists else { return } // user never touched it - - func take(_ k: String, _ def: V) -> V { - let box = UserDefaultsValue(key: k, default: def) - defer { box.setNil(key: k) } // wipe after reading - return box.value - } - - var alarm = Alarm(type: .overrideStart) - alarm.name = "Override Started" - - alarm.isEnabled = take("alertOverrideStart", false) - alarm.snoozeDuration = 5 // legacy UI had no stepper - if take("alertOverrideStartIsSnoozed", false) { - alarm.snoozedUntil = take("alertOverrideStartSnoozedTime", nil as Date?) - } - - alarm.soundFile = SoundFile( - rawValue: take("alertOverrideStartSound", "Ending_Reached") - ) ?? .endingReached - - // ── ACTIVE (legacy “Pre-Snooze” day/night flags) ────────────── - let actDay = take("alertOverrideStartAutosnoozeDay", false) - let actNight = take("alertOverrideStartAutosnoozeNight", false) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always - } - }() - - // ── PLAY (legacy “Play Sound” day/night flags) ─────────────── - let playDay = take("alertOverrideStartDayTimeAudible", true) - let playNight = take("alertOverrideStartNightTimeAudible", true) - alarm.playSoundOption = { - switch (playDay, playNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // ── REPEAT (legacy “Repeat Sound” day/night flags) ─────────── - let repDay = take("alertOverrideStartDayTime", false) - let repNight = take("alertOverrideStartNightTime", false) - alarm.repeatSoundOption = { - switch (repDay, repNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // ignore & wipe unused keys - _ = take("alertOverrideStartQuiet", false as Bool) - _ = take("alertOverrideStartRepeatAudible", "Always" as String) - - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // ───────────────────────────────────────────────────────────────────────────── - - // MARK: Override-End → .overrideEnd - - // ───────────────────────────────────────────────────────────────────────────── - private func migrateOverrideEndAlarm() { - let exists = UserDefaultsValue(key: "alertOverrideEnd", default: false) - guard exists.exists else { return } - - func take(_ k: String, _ def: V) -> V { - let box = UserDefaultsValue(key: k, default: def) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .overrideEnd) - alarm.name = "Override Ended" - - alarm.isEnabled = take("alertOverrideEnd", false) - alarm.snoozeDuration = 5 - if take("alertOverrideEndIsSnoozed", false) { - alarm.snoozedUntil = take("alertOverrideEndSnoozedTime", nil as Date?) - } - - alarm.soundFile = SoundFile( - rawValue: take("alertOverrideEndSound", "Alert_Tone_Busy") - ) ?? .alertToneBusy - - // ACTIVE - let actDay = take("alertOverrideEndAutosnoozeDay", false) - let actNight = take("alertOverrideEndAutosnoozeNight", false) - alarm.activeOption = { - switch (actDay, actNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always - } - }() - - // PLAY - let playDay = take("alertOverrideEndDayTimeAudible", true) - let playNight = take("alertOverrideEndNightTimeAudible", true) - alarm.playSoundOption = { - switch (playDay, playNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // REPEAT - let repDay = take("alertOverrideEndDayTime", false) - let repNight = take("alertOverrideEndNightTime", false) - alarm.repeatSoundOption = { - switch (repDay, repNight) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // wipe unused keys - _ = take("alertOverrideEndQuiet", false as Bool) - _ = take("alertOverrideEndRepeatAudible", "Always" as String) - - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // MARK: ––––– Temp-Target START → .tempTargetStart ––––– - - private func migrateTempTargetStartAlarm() { - let touched = UserDefaultsValue(key: "alertTempTargetStart", default: false) - guard touched.exists else { return } - - func take(_ k: String, _ def: V) -> V { - let box = UserDefaultsValue(key: k, default: def) - defer { box.setNil(key: k) } // scrub after read - return box.value - } - - var alarm = Alarm(type: .tempTargetStart) - alarm.name = "Temp Target Started" - - alarm.isEnabled = take("alertTempTargetStart", false) - alarm.snoozeDuration = 5 - if take("alertTempTargetStartIsSnoozed", false) { - alarm.snoozedUntil = take("alertTempTargetStartSnoozedTime", nil as Date?) - } - - alarm.soundFile = SoundFile( - rawValue: take("alertTempTargetStartSound", "Ending_Reached") - ) ?? .endingReached - - // ACTIVE ← legacy Pre-Snooze day/night flags - alarm.activeOption = { - let d = take("alertTempTargetStartAutosnoozeDay", false) - let n = take("alertTempTargetStartAutosnoozeNight", false) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always } - }() - - // PLAY - alarm.playSoundOption = { - let d = take("alertTempTargetStartDayTimeAudible", true) - let n = take("alertTempTargetStartNightTimeAudible", true) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never } - }() - - // REPEAT - alarm.repeatSoundOption = { - let d = take("alertTempTargetStartDayTime", false) - let n = take("alertTempTargetStartNightTime", false) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never } - }() - - // wipe “quiet / RepeatAudible” extras - _ = take("alertTempTargetStartQuiet", false as Bool) - _ = take("alertTempTargetStartRepeatAudible", "Always" as String) - - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // MARK: ––––– Temp-Target END → .tempTargetEnd ––––– - - private func migrateTempTargetEndAlarm() { - let touched = UserDefaultsValue(key: "alertTempTargetEnd", default: false) - guard touched.exists else { return } - - func take(_ k: String, _ def: V) -> V { - let box = UserDefaultsValue(key: k, default: def) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .tempTargetEnd) - alarm.name = "Temp Target Ended" - - alarm.isEnabled = take("alertTempTargetEnd", false) - alarm.snoozeDuration = 5 - if take("alertTempTargetEndIsSnoozed", false) { - alarm.snoozedUntil = take("alertTempTargetEndSnoozedTime", nil as Date?) - } - - alarm.soundFile = SoundFile( - rawValue: take("alertTempTargetEndSound", "Alert_Tone_Busy") - ) ?? .alertToneBusy - - alarm.activeOption = { - let d = take("alertTempTargetEndAutosnoozeDay", false) - let n = take("alertTempTargetEndAutosnoozeNight", false) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always } - }() - - alarm.playSoundOption = { - let d = take("alertTempTargetEndDayTimeAudible", true) - let n = take("alertTempTargetEndNightTimeAudible", true) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never } - }() - - alarm.repeatSoundOption = { - let d = take("alertTempTargetEndDayTime", false) - let n = take("alertTempTargetEndNightTime", false) - switch (d, n) { case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never } - }() - - _ = take("alertTempTargetEndQuiet", false as Bool) - _ = take("alertTempTargetEndRepeatAudible", "Always" as String) - - var list = Storage.shared.alarms.value - list.append(alarm) - Storage.shared.alarms.value = list - } - - // MARK: ––––– TEMPORARY BG LIMIT → .temporary ––––– - - private func migrateTemporaryBGAlarm() { - let flag = UserDefaultsValue(key: "alertTemporaryActive", default: false) - guard flag.exists else { return } - - func take(_ k: String, _ d: V) -> V { - let box = UserDefaultsValue(key: k, default: d) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .temporary) - alarm.name = "Temporary BG Limit" - alarm.isEnabled = take("alertTemporaryActive", false) - - // limit direction ↓ / ↑ - let limit = Double(take("alertTemporaryBG", 90.0 as Float)) - if take("alertTemporaryBelow", true) { - alarm.belowBG = limit - } else { - alarm.aboveBG = limit - } - - // audio & repeat - alarm.soundFile = SoundFile(rawValue: - take("alertTemporarySound", "Indeed")) ?? .indeed - - alarm.playSoundOption = take("alertTemporaryBGAudible", true) ? .always : .never - alarm.repeatSoundOption = take("alertTemporaryBGRepeat", false) ? .always : .never - - Storage.shared.alarms.value.append(alarm) - } - - // MARK: ––––– PUMP RESERVOIR LEVEL → .pump ––––– - - private func migratePumpVolumeAlarm() { - let flag = UserDefaultsValue(key: "alertPump", default: false) - guard flag.exists else { return } - - func take(_ k: String, _ d: V) -> V { - let box = UserDefaultsValue(key: k, default: d) - defer { box.setNil(key: k) } - return box.value - } - - var alarm = Alarm(type: .pump) - alarm.name = "Pump Reservoir" - alarm.isEnabled = take("alertPump", false) - alarm.threshold = Double(take("alertPumpAt", 10)) // units left - - // Snooze — stored in hours, so keep as-is. - alarm.snoozeDuration = take("alertPumpSnoozeHours", 5) - - if take("alertPumpIsSnoozed", false) { - alarm.snoozedUntil = take("alertPumpSnoozedTime", nil as Date?) - } - - alarm.soundFile = SoundFile(rawValue: - take("alertPumpSound", "Marimba_Descend")) ?? .marimbaDescend - - // PLAY-sound option (day / night / never) - alarm.playSoundOption = { - let d = take("alertPumpDayTimeAudible", true) - let n = take("alertPumpNightTimeAudible", true) - switch (d, n) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // REPEAT-sound option – derived from legacy picker flags - alarm.repeatSoundOption = { - let d = take("alertPumpDayTime", false) - let n = take("alertPumpNightTime", false) - switch (d, n) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - }() - - // ACTIVE day/night ← old “Pre-Snooze” flags - alarm.activeOption = { - let d = take("alertPumpAutosnoozeDay", false) - let n = take("alertPumpAutosnoozeNight", false) - switch (d, n) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always - } - }() - - // Discard no-longer-needed extras - _ = take("alertPumpQuiet", false as Bool) - _ = take("alertPumpRepeat", "Never" as String) - _ = take("alertPumpAudible", "Always" as String) - _ = take("alertPumpAutosnooze", "Never" as String) - - Storage.shared.alarms.value.append(alarm) - } - - // ----------------------------------------------------------------------------- - - // MARK: - Helpers (place them once, near the top of the migrate() file) - - // ----------------------------------------------------------------------------- - - /// Legacy picker + day/night flags → PlaySoundOption - private func playOption(from picker: String, - dayFlag: Bool, - nightFlag: Bool) -> PlaySoundOption - { - let p = picker.lowercased() - if p == "never" { return .never } - if p == "always" { return .always } - - switch (dayFlag, nightFlag) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - } - - /// Legacy picker + day/night flags → RepeatSoundOption - private func repeatOption(from picker: String, - dayFlag: Bool, - nightFlag: Bool) -> RepeatSoundOption - { - let p = picker.lowercased() - if p == "never" { return .never } - if p == "always" { return .always } - - switch (dayFlag, nightFlag) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never - } - } - - /// Convenience: load-then-erase a legacy `UserDefaultsValue` - private func take( - _ key: String, - _ defaultValue: V - ) -> V { - let box = UserDefaultsValue(key: key, default: defaultValue) - defer { box.setNil(key: key) } - return box.value - } - - // ----------------------------------------------------------------------------- - - // MARK: - IOB Alarm migration - - // ----------------------------------------------------------------------------- - - private func migrateIOBAlarm() { - // Only migrate if the user ever touched IOB alarm settings - guard UserDefaultsValue(key: "alertIOB", default: false).exists else { - return - } - - var alarm = Alarm(type: .iob) - alarm.name = "High IOB" - alarm.isEnabled = take("alertIOB", false) - alarm.aboveBG = nil // BG band not used - alarm.threshold = Double(take("alertIOBAt", 1.5)) // units threshold - alarm.bolusCountThreshold = take("alertIOBNumber", 3) - alarm.bolusWindowMinutes = take("alertIOBBolusesWithin", 60) - alarm.delta = Double(take("alertIOBMaxBoluses", 10)) // max total bolus - alarm.snoozeDuration = take("alertIOBSnoozeHours", 1) * 60 // hours → minutes - alarm.snoozedUntil = take("alertIOBSnoozedTime", nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertIOBSound", "Alert_Tone_Ringtone_1")) - ?? .alertToneRingtone1 - - // Audio options - alarm.playSoundOption = playOption( - from: take("alertIOBAudible", "Always"), - dayFlag: take("alertIOBDayTimeAudible", true), - nightFlag: take("alertIOBNightTimeAudible", true) - ) - alarm.repeatSoundOption = repeatOption( - from: take("alertIOBRepeat", "Always"), - dayFlag: take("alertIOBDayTime", true), - nightFlag: take("alertIOBNightTime", true) - ) - - // Active (day/night) option comes from the legacy “Pre-Snooze” picker, - // but the IOB alarm never had one, so treat it as always-on. - alarm.activeOption = .always - - // Persist - Storage.shared.alarms.value.append(alarm) - } - - // ----------------------------------------------------------------------------- - - // MARK: - COB Alarm migration - - // ----------------------------------------------------------------------------- - - private func migrateCOBAlarm() { - // Only migrate if the user ever touched COB alarm settings - guard UserDefaultsValue(key: "alertCOB", default: false).exists else { - return - } - - var alarm = Alarm(type: .cob) - alarm.name = "High COB" - alarm.isEnabled = take("alertCOB", false) - alarm.threshold = Double(take("alertCOBAt", 50)) // grams threshold - alarm.snoozeDuration = take("alertCOBSnoozeHours", 1) * 60 // hours → minutes - alarm.snoozedUntil = take("alertCOBSnoozedTime", nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertCOBSound", "Alert_Tone_Ringtone_2")) - ?? .alertToneRingtone2 - - // Audio options - alarm.playSoundOption = playOption( - from: take("alertCOBAudible", "Always"), - dayFlag: take("alertCOBDayTimeAudible", true), - nightFlag: take("alertCOBNightTimeAudible", true) - ) - alarm.repeatSoundOption = repeatOption( - from: take("alertCOBRepeat", "Always"), - dayFlag: take("alertCOBDayTime", true), - nightFlag: take("alertCOBNightTime", true) - ) - - alarm.activeOption = .always // same reason as above - - // Persist - Storage.shared.alarms.value.append(alarm) - } - - // ============================================================================= - // BATTERY-LEVEL alarm (old keys → .battery) - // ============================================================================= - private func migrateBatteryAlarm() { - guard UserDefaultsValue(key: "alertBatteryActive", - default: false).exists else { return } - - var alarm = Alarm(type: .battery) - alarm.name = "Low Battery" - alarm.isEnabled = take("alertBatteryActive", false) - alarm.threshold = Double(take("alertBatteryLevel", 25)) // % - alarm.snoozeDuration = take("alertBatterySnoozeHours", 1) * 60 - alarm.snoozedUntil = take("alertBatterySnoozedTime", nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertBatterySound", "Machine_Charge")) - ?? .machineCharge - - // ── audio – legacy had a simple Bool “repeat / no-repeat” - let rpt = take("alertBatteryRepeat", true) - alarm.playSoundOption = .always // no day/night picker in legacy UI - alarm.repeatSoundOption = rpt ? .always : .never - - alarm.activeOption = .always // no day/night activation picker - Storage.shared.alarms.value.append(alarm) - } - - // ============================================================================= - // BATTERY-DROP alarm (old keys → .batteryDrop) - // ============================================================================= - private func migrateBatteryDropAlarm() { - guard UserDefaultsValue(key: "alertBatteryDropActive", - default: false).exists else { return } - - var alarm = Alarm(type: .batteryDrop) - alarm.name = "Battery Drop" - alarm.isEnabled = take("alertBatteryDropActive", false) - alarm.delta = Double(take("alertBatteryDropPercentage", 5)) // % drop - alarm.monitoringWindow = take("alertBatteryDropPeriod", 15) // min - alarm.snoozeDuration = take("alertBatteryDropSnoozeHours", 1) * 60 - alarm.snoozedUntil = take("alertBatteryDropSnoozedTime", nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertBatteryDropSound", "Machine_Charge")) - ?? .machineCharge - - let rpt = take("alertBatteryDropRepeat", true) - alarm.playSoundOption = .always - alarm.repeatSoundOption = rpt ? .always : .never - alarm.activeOption = .always - - Storage.shared.alarms.value.append(alarm) - } - - // ============================================================================= - // REC-BOLUS alarm (old keys → .recBolus) - // ============================================================================= - private func migrateRecBolusAlarm() { - guard UserDefaultsValue(key: "alertRecBolusActive", - default: false).exists else { return } - - var alarm = Alarm(type: .recBolus) - alarm.name = "Recommended Bolus" - alarm.isEnabled = take("alertRecBolusActive", false) - alarm.delta = take("alertRecBolusLevel", 1.0) // units - alarm.snoozeDuration = take("alertRecBolusSnooze", 5) // min - alarm.snoozedUntil = take("alertRecBolusSnoozedTime", nil as Date?) - alarm.soundFile = SoundFile(rawValue: - take("alertRecBolusSound", "Dhol_Shuffleloop")) - ?? .dholShuffleloop - - let rpt = take("alertRecBolusRepeat", false) - alarm.playSoundOption = .always - alarm.repeatSoundOption = rpt ? .always : .never - alarm.activeOption = .always - - Storage.shared.alarms.value.append(alarm) - } - // MARK: - Migration helpers (can be removed when step 5 is removed) /// Check whether a tab item's position was explicitly stored in UserDefaults diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 105873f1c..7be80a53b 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -646,12 +646,6 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio // 2. Update any other StorageValue defaults in Storage.swift that this new step // mutates, so a fresh install ends up in the same state as a migrated user. - // Step 1: Released in v3.0.0 (2025-07-07). Can be removed after 2026-07-07. - if Storage.shared.migrationStep.value < 1 { - Storage.shared.migrateStep1() - Storage.shared.migrationStep.value = 1 - } - // Step 2: Released in v3.1.0 (2025-07-21). Can be removed after 2026-07-21. if Storage.shared.migrationStep.value < 2 { Storage.shared.migrateStep2()