Storage+Migrate.swift 77 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765
  1. // LoopFollow
  2. // Storage+Migrate.swift
  3. import Foundation
  4. extension Storage {
  5. func migrateStep5() {
  6. // Users upgrading from main had snoozer hardcoded at position 3, but that
  7. // key never existed in UserDefaults. On dev the default for snoozerPosition
  8. // is .menu, so an unstored value silently becomes .menu and snoozer
  9. // disappears from the tab bar.
  10. //
  11. // Detect this by checking .exists: if snoozerPosition was never stored,
  12. // the user expects snoozer at position 3 (where it always was on main).
  13. guard !snoozerPosition.exists else { return }
  14. LogManager.shared.log(category: .general, message: "migrateStep5: snoozerPosition not stored, restoring snoozer to position 3")
  15. // If position 3 is occupied by another item whose position also wasn't
  16. // stored (e.g. nightscout landing there from a changed default), move
  17. // that item out first.
  18. for item in TabItem.allCases where item != .snoozer {
  19. if position(for: item).normalized == .position3,
  20. !migratePositionExists(for: item)
  21. {
  22. setPosition(.menu, for: item)
  23. }
  24. }
  25. if tabItem(at: .position3) == nil {
  26. snoozerPosition.value = .position3
  27. }
  28. }
  29. func migrateStep3() {
  30. LogManager.shared.log(category: .general, message: "Running migrateStep3 - this should only happen once!")
  31. let legacyForceDarkMode = StorageValue<Bool>(key: "forceDarkMode", defaultValue: true)
  32. if legacyForceDarkMode.exists {
  33. Storage.shared.appearanceMode.value = legacyForceDarkMode.value ? .dark : .system
  34. legacyForceDarkMode.remove()
  35. }
  36. if !TabPosition.customizablePositions.contains(homePosition.value.normalized) {
  37. LogManager.shared.log(category: .general, message: "migrateStep3: Setting home to position1 (was \(homePosition.value.rawValue))")
  38. homePosition.value = .position1
  39. }
  40. if !TabPosition.customizablePositions.contains(snoozerPosition.value.normalized) {
  41. // Move any unstored occupant at position 3 to menu before placing snoozer,
  42. // to avoid a collision when dev defaults differ from main.
  43. for item in TabItem.allCases where item != .snoozer {
  44. if position(for: item).normalized == .position3,
  45. !migratePositionExists(for: item)
  46. {
  47. setPosition(.menu, for: item)
  48. }
  49. }
  50. if tabItem(at: .position3) == nil {
  51. snoozerPosition.value = .position3
  52. }
  53. }
  54. if alarmsPosition.value == .more {
  55. alarmsPosition.value = .menu
  56. }
  57. if remotePosition.value == .more {
  58. remotePosition.value = .menu
  59. }
  60. if nightscoutPosition.value == .more {
  61. nightscoutPosition.value = .menu
  62. }
  63. }
  64. func migrateStep2() {
  65. // Migrate from old system to new position-based system
  66. if remoteType.value != .none {
  67. remotePosition.value = .position2
  68. alarmsPosition.value = .menu
  69. } else {
  70. alarmsPosition.value = .position2
  71. remotePosition.value = .menu
  72. }
  73. nightscoutPosition.value = .position4
  74. }
  75. func migrateStep1() {
  76. Storage.shared.url.value = ObservableUserDefaults.shared.old_url.value
  77. Storage.shared.device.value = ObservableUserDefaults.shared.old_device.value
  78. Storage.shared.nsWriteAuth.value = ObservableUserDefaults.shared.old_nsWriteAuth.value
  79. Storage.shared.nsAdminAuth.value = ObservableUserDefaults.shared.old_nsAdminAuth.value
  80. // Helper: 1-to-1 type -----------------------------------------------------------------
  81. func move<T: AnyConvertible & Equatable>(
  82. _ legacy: @autoclosure () -> UserDefaultsValue<T>,
  83. into newValue: StorageValue<T>
  84. ) {
  85. let item = legacy()
  86. guard item.exists else { return }
  87. newValue.value = item.value
  88. item.setNil(key: item.key)
  89. }
  90. // Helper: Float → Double ------------------------------------------------------------
  91. func moveFloatToDouble(
  92. _ legacy: @autoclosure () -> UserDefaultsValue<Float>,
  93. into newValue: StorageValue<Double>
  94. ) {
  95. let item = legacy()
  96. guard item.exists else { return }
  97. newValue.value = Double(item.value)
  98. item.setNil(key: item.key)
  99. }
  100. // Remove this in a year later than the release of the new Alarms [BEGIN]
  101. let legacyColorBGText = UserDefaultsValue<Bool>(key: "colorBGText", default: true)
  102. if legacyColorBGText.exists {
  103. Storage.shared.colorBGText.value = legacyColorBGText.value
  104. legacyColorBGText.setNil(key: "colorBGText")
  105. }
  106. let legacyAppBadge = UserDefaultsValue<Bool>(key: "appBadge", default: true)
  107. if legacyAppBadge.exists {
  108. Storage.shared.appBadge.value = legacyAppBadge.value
  109. legacyAppBadge.setNil(key: "appBadge")
  110. }
  111. let legacyForceDarkMode = UserDefaultsValue<Bool>(key: "forceDarkMode", default: true)
  112. if legacyForceDarkMode.exists {
  113. // Migrate from Bool to AppearanceMode: true -> .dark, false -> .system
  114. Storage.shared.appearanceMode.value = legacyForceDarkMode.value ? .dark : .system
  115. legacyForceDarkMode.setNil(key: "forceDarkMode")
  116. }
  117. let legacyShowStats = UserDefaultsValue<Bool>(key: "showStats", default: true)
  118. if legacyShowStats.exists {
  119. Storage.shared.showStats.value = legacyShowStats.value
  120. legacyShowStats.setNil(key: "showStats")
  121. }
  122. let legacyUseIFCC = UserDefaultsValue<Bool>(key: "useIFCC", default: false)
  123. if legacyUseIFCC.exists {
  124. Storage.shared.useIFCC.value = legacyUseIFCC.value
  125. legacyUseIFCC.setNil(key: "useIFCC")
  126. }
  127. let legacyShowSmallGraph = UserDefaultsValue<Bool>(key: "showSmallGraph", default: true)
  128. if legacyShowSmallGraph.exists {
  129. Storage.shared.showSmallGraph.value = legacyShowSmallGraph.value
  130. legacyShowSmallGraph.setNil(key: "showSmallGraph")
  131. }
  132. let legacyScreenlockSwitchState = UserDefaultsValue<Bool>(key: "screenlockSwitchState", default: true)
  133. if legacyScreenlockSwitchState.exists {
  134. Storage.shared.screenlockSwitchState.value = legacyScreenlockSwitchState.value
  135. legacyScreenlockSwitchState.setNil(key: "screenlockSwitchState")
  136. }
  137. let legacyShowDisplayName = UserDefaultsValue<Bool>(key: "showDisplayName", default: false)
  138. if legacyShowDisplayName.exists {
  139. Storage.shared.showDisplayName.value = legacyShowDisplayName.value
  140. legacyShowDisplayName.setNil(key: "showDisplayName")
  141. }
  142. let legacySpeakBG = UserDefaultsValue<Bool>(key: "speakBG", default: false)
  143. if legacySpeakBG.exists {
  144. Storage.shared.speakBG.value = legacySpeakBG.value
  145. legacySpeakBG.setNil(key: "speakBG")
  146. }
  147. let legacySpeakBGAlways = UserDefaultsValue<Bool>(key: "speakBGAlways", default: true)
  148. if legacySpeakBGAlways.exists {
  149. Storage.shared.speakBGAlways.value = legacySpeakBGAlways.value
  150. legacySpeakBGAlways.setNil(key: "speakBGAlways")
  151. }
  152. let legacySpeakLowBG = UserDefaultsValue<Bool>(key: "speakLowBG", default: false)
  153. if legacySpeakLowBG.exists {
  154. Storage.shared.speakLowBG.value = legacySpeakLowBG.value
  155. legacySpeakLowBG.setNil(key: "speakLowBG")
  156. }
  157. let legacySpeakProactiveLowBG = UserDefaultsValue<Bool>(key: "speakProactiveLowBG", default: false)
  158. if legacySpeakProactiveLowBG.exists {
  159. Storage.shared.speakProactiveLowBG.value = legacySpeakProactiveLowBG.value
  160. legacySpeakProactiveLowBG.setNil(key: "speakProactiveLowBG")
  161. }
  162. let legacySpeakFastDropDelta = UserDefaultsValue<Float>(key: "speakFastDropDelta", default: 10.0)
  163. if legacySpeakFastDropDelta.exists {
  164. Storage.shared.speakFastDropDelta.value = Double(legacySpeakFastDropDelta.value)
  165. legacySpeakFastDropDelta.setNil(key: "speakFastDropDelta")
  166. }
  167. let legacySpeakLowBGLimit = UserDefaultsValue<Float>(key: "speakLowBGLimit", default: 72.0)
  168. if legacySpeakLowBGLimit.exists {
  169. Storage.shared.speakLowBGLimit.value = Double(legacySpeakLowBGLimit.value)
  170. legacySpeakLowBGLimit.setNil(key: "speakLowBGLimit")
  171. }
  172. let legacySpeakHighBGLimit = UserDefaultsValue<Float>(key: "speakHighBGLimit", default: 180.0)
  173. if legacySpeakHighBGLimit.exists {
  174. Storage.shared.speakHighBGLimit.value = Double(legacySpeakHighBGLimit.value)
  175. legacySpeakHighBGLimit.setNil(key: "speakHighBGLimit")
  176. }
  177. let legacySpeakHighBG = UserDefaultsValue<Bool>(key: "speakHighBG", default: false)
  178. if legacySpeakHighBG.exists {
  179. Storage.shared.speakHighBG.value = legacySpeakHighBG.value
  180. legacySpeakHighBG.setNil(key: "speakHighBG")
  181. }
  182. let legacySpeakLanguage = UserDefaultsValue<String>(key: "speakLanguage", default: "en")
  183. if legacySpeakLanguage.exists {
  184. Storage.shared.speakLanguage.value = legacySpeakLanguage.value
  185. legacySpeakLanguage.setNil(key: "speakLanguage")
  186. }
  187. move(UserDefaultsValue<Bool>(key: "persistentNotification", default: true), into: Storage.shared.persistentNotification)
  188. // ── General (done earlier, but safe to repeat) ──
  189. move(UserDefaultsValue<Bool>(key: "colorBGText", default: true), into: Storage.shared.colorBGText)
  190. move(UserDefaultsValue<Bool>(key: "appBadge", default: true), into: appBadge)
  191. // Note: forceDarkMode migration to appearanceMode is handled earlier in migrateGeneralSettings()
  192. move(UserDefaultsValue<Bool>(key: "showStats", default: true), into: showStats)
  193. move(UserDefaultsValue<Bool>(key: "useIFCC", default: false), into: useIFCC)
  194. move(UserDefaultsValue<Bool>(key: "showSmallGraph", default: true), into: showSmallGraph)
  195. move(UserDefaultsValue<Bool>(key: "screenlockSwitchState", default: false), into: screenlockSwitchState)
  196. move(UserDefaultsValue<Bool>(key: "showDisplayName", default: false), into: showDisplayName)
  197. // ── Speak-BG ──
  198. move(UserDefaultsValue<Bool>(key: "speakBG", default: false), into: speakBG)
  199. move(UserDefaultsValue<Bool>(key: "speakBGAlways", default: true), into: speakBGAlways)
  200. move(UserDefaultsValue<Bool>(key: "speakLowBG", default: false), into: speakLowBG)
  201. move(UserDefaultsValue<Bool>(key: "speakProactiveLowBG", default: false), into: speakProactiveLowBG)
  202. move(UserDefaultsValue<Bool>(key: "speakHighBG", default: false), into: speakHighBG)
  203. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakLowBGLimit", default: 72.0), into: speakLowBGLimit)
  204. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakHighBGLimit", default: 180.0), into: speakHighBGLimit)
  205. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakFastDropDelta", default: 10.0), into: speakFastDropDelta)
  206. move(UserDefaultsValue<String>(key: "speakLanguage", default: "en"), into: speakLanguage)
  207. // ── Graph ──
  208. move(UserDefaultsValue<Bool>(key: "showDots", default: true), into: showDots)
  209. move(UserDefaultsValue<Bool>(key: "showLines", default: true), into: showLines)
  210. move(UserDefaultsValue<Bool>(key: "showValues", default: true), into: showValues)
  211. move(UserDefaultsValue<Bool>(key: "showAbsorption", default: true), into: showAbsorption)
  212. move(UserDefaultsValue<Bool>(key: "showDIAMarkers", default: true), into: showDIALines)
  213. move(UserDefaultsValue<Bool>(key: "show30MinLine", default: false), into: show30MinLine)
  214. move(UserDefaultsValue<Bool>(key: "show90MinLine", default: false), into: show90MinLine)
  215. move(UserDefaultsValue<Bool>(key: "showMidnightMarkers", default: false), into: showMidnightLines)
  216. move(UserDefaultsValue<Bool>(key: "smallGraphTreatments", default: true), into: smallGraphTreatments)
  217. move(UserDefaultsValue<Int>(key: "smallGraphHeight", default: 40), into: smallGraphHeight)
  218. move(UserDefaultsValue<Double>(key: "predictionToLoad", default: 1.0), into: predictionToLoad)
  219. move(UserDefaultsValue<Double>(key: "minBasalScale", default: 5.0), into: minBasalScale)
  220. moveFloatToDouble(UserDefaultsValue<Float>(key: "minBGScale", default: 250.0), into: minBGScale)
  221. moveFloatToDouble(UserDefaultsValue<Float>(key: "lowLine", default: 70.0), into: lowLine)
  222. moveFloatToDouble(UserDefaultsValue<Float>(key: "highLine", default: 180.0), into: highLine)
  223. move(UserDefaultsValue<Int>(key: "downloadDays", default: 1), into: downloadDays)
  224. // Remove this in a year later than the release of the new Alarms [END]
  225. // ── Watch / Calendar ────────────────────────────────────────────────
  226. move(UserDefaultsValue<Bool>(key: "writeCalendarEvent", default: false), into: writeCalendarEvent)
  227. move(UserDefaultsValue<String>(key: "calendarIdentifier", default: ""), into: calendarIdentifier)
  228. move(UserDefaultsValue<String>(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%"), into: watchLine1)
  229. move(UserDefaultsValue<String>(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%"), into: watchLine2)
  230. // Migration of generic alarm settings
  231. // ── AlarmConfiguration migration ─────────────────────────────────────────
  232. do {
  233. // Work on a mutable copy, then write the whole thing back once.
  234. var cfg = Storage.shared.alarmConfiguration.value
  235. let cal = Calendar.current
  236. /// Copy *one* legacy value → struct field → delete old key
  237. func move<T: AnyConvertible & Equatable>(
  238. _ legacy: @autoclosure () -> UserDefaultsValue<T>,
  239. write: (inout AlarmConfiguration, T) -> Void
  240. ) {
  241. let item = legacy()
  242. guard item.exists else { return }
  243. write(&cfg, item.value)
  244. item.setNil(key: item.key)
  245. }
  246. // 1. Override-volume toggle
  247. move(UserDefaultsValue<Bool>(key: "overrideSystemOutputVolume",
  248. default: cfg.overrideSystemOutputVolume))
  249. {
  250. $0.overrideSystemOutputVolume = $1
  251. }
  252. // 2. Forced output volume itself.
  253. // Prefer newer key (“forcedOutputVolume”); otherwise fall back.
  254. if UserDefaultsValue<Float>(key: "forcedOutputVolume",
  255. default: cfg.forcedOutputVolume).exists
  256. {
  257. move(UserDefaultsValue<Float>(key: "forcedOutputVolume",
  258. default: cfg.forcedOutputVolume))
  259. {
  260. $0.forcedOutputVolume = $1
  261. }
  262. } else {
  263. move(UserDefaultsValue<Float>(key: "systemOutputVolume",
  264. default: cfg.forcedOutputVolume))
  265. {
  266. $0.forcedOutputVolume = $1
  267. }
  268. }
  269. // 3. Play audio during phone calls
  270. move(UserDefaultsValue<Bool>(key: "alertAudioDuringPhone",
  271. default: cfg.audioDuringCalls))
  272. {
  273. $0.audioDuringCalls = $1
  274. }
  275. // 4. Auto-snooze CGM-start alarm
  276. move(UserDefaultsValue<Bool>(key: "alertAutoSnoozeCGMStart",
  277. default: cfg.autoSnoozeCGMStart))
  278. {
  279. $0.autoSnoozeCGMStart = $1
  280. }
  281. // 5. Global “Snooze all” → snoozeUntil
  282. move(UserDefaultsValue<Date?>(key: "alertSnoozeAllTime",
  283. default: cfg.snoozeUntil))
  284. {
  285. $0.snoozeUntil = $1
  286. }
  287. // 6. Global “Mute all” → muteUntil
  288. move(UserDefaultsValue<Date?>(key: "alertMuteAllTime",
  289. default: cfg.muteUntil))
  290. {
  291. $0.muteUntil = $1
  292. }
  293. // 7 & 8. Legacy quiet-hours → day/night start
  294. // (only if both dates exist and are on the same “reference” day)
  295. let qStart = UserDefaultsValue<Date?>(key: "quietHourStart", default: nil)
  296. let qEnd = UserDefaultsValue<Date?>(key: "quietHourEnd", default: nil)
  297. if let s = qStart.value, let e = qEnd.value {
  298. let compsStart = cal.dateComponents([.hour, .minute], from: s)
  299. let compsEnd = cal.dateComponents([.hour, .minute], from: e)
  300. if let sh = compsStart.hour, let sm = compsStart.minute,
  301. let eh = compsEnd.hour, let em = compsEnd.minute
  302. {
  303. cfg.nightStart = TimeOfDay(hour: sh, minute: sm)
  304. cfg.dayStart = TimeOfDay(hour: eh, minute: em)
  305. qStart.setNil(key: qStart.key)
  306. qEnd.setNil(key: qEnd.key)
  307. }
  308. }
  309. // 9. Legacy “ignore zero BG” flag → ignoreZeroBG
  310. move(UserDefaultsValue<Bool>(key: "alertIgnoreZero",
  311. default: cfg.ignoreZeroBG))
  312. {
  313. $0.ignoreZeroBG = $1
  314. }
  315. // finally persist the whole struct
  316. Storage.shared.alarmConfiguration.value = cfg
  317. }
  318. // ── Dexcom Share --------------------------------------------------------
  319. move(UserDefaultsValue<String>(key: "shareUserName", default: ""),
  320. into: Storage.shared.shareUserName)
  321. move(UserDefaultsValue<String>(key: "sharePassword", default: ""),
  322. into: Storage.shared.sharePassword)
  323. move(UserDefaultsValue<String>(key: "shareServer", default: "US"),
  324. into: Storage.shared.shareServer)
  325. // ── Graph ---------------------------------------------------------------
  326. moveFloatToDouble(
  327. UserDefaultsValue<Float>(key: "chartScaleX", default: 18.0),
  328. into: Storage.shared.chartScaleX
  329. )
  330. // ── Advanced settings ---------------------------------------------------
  331. move(UserDefaultsValue<Bool>(key: "downloadTreatments", default: true),
  332. into: Storage.shared.downloadTreatments)
  333. move(UserDefaultsValue<Bool>(key: "downloadPrediction", default: true),
  334. into: Storage.shared.downloadPrediction)
  335. move(UserDefaultsValue<Bool>(key: "graphOtherTreatments", default: true),
  336. into: Storage.shared.graphOtherTreatments)
  337. move(UserDefaultsValue<Bool>(key: "graphBasal", default: true),
  338. into: Storage.shared.graphBasal)
  339. move(UserDefaultsValue<Bool>(key: "graphBolus", default: true),
  340. into: Storage.shared.graphBolus)
  341. move(UserDefaultsValue<Bool>(key: "graphCarbs", default: true),
  342. into: Storage.shared.graphCarbs)
  343. move(UserDefaultsValue<Int>(key: "bgUpdateDelay", default: 10),
  344. into: Storage.shared.bgUpdateDelay)
  345. // ── Insert times --------------------------------------------------------
  346. move(UserDefaultsValue<TimeInterval>(key: "alertCageInsertTime", default: 0),
  347. into: Storage.shared.cageInsertTime)
  348. move(UserDefaultsValue<TimeInterval>(key: "alertSageInsertTime", default: 0),
  349. into: Storage.shared.sageInsertTime)
  350. // ── Version-cache / notification bookkeeping ---------------------------
  351. move(UserDefaultsValue<String?>(key: "cachedForVersion", default: nil),
  352. into: Storage.shared.cachedForVersion)
  353. move(UserDefaultsValue<String?>(key: "latestVersion", default: nil),
  354. into: Storage.shared.latestVersion)
  355. move(UserDefaultsValue<Date?>(key: "latestVersionChecked", default: nil),
  356. into: Storage.shared.latestVersionChecked)
  357. move(UserDefaultsValue<Bool>(key: "currentVersionBlackListed", default: false),
  358. into: Storage.shared.currentVersionBlackListed)
  359. move(UserDefaultsValue<Date?>(key: "lastBlacklistNotificationShown", default: nil),
  360. into: Storage.shared.lastBlacklistNotificationShown)
  361. move(UserDefaultsValue<Date?>(key: "lastVersionUpdateNotificationShown", default: nil),
  362. into: Storage.shared.lastVersionUpdateNotificationShown)
  363. move(UserDefaultsValue<Date?>(key: "lastExpirationNotificationShown", default: nil),
  364. into: Storage.shared.lastExpirationNotificationShown)
  365. move(UserDefaultsValue<Bool>(key: "hideInfoTable", default: false), into: Storage.shared.hideInfoTable)
  366. move(UserDefaultsValue<String>(key: "token", default: ""), into: Storage.shared.token)
  367. move(UserDefaultsValue<String>(key: "units", default: "mg/dL"), into: Storage.shared.units)
  368. move(UserDefaultsValue<[Int]>(key: "infoSort",
  369. default: InfoType.allCases.map { $0.sortOrder }),
  370. into: Storage.shared.infoSort)
  371. move(UserDefaultsValue<[Bool]>(key: "infoVisible",
  372. default: InfoType.allCases.map { $0.defaultVisible }),
  373. into: Storage.shared.infoVisible)
  374. migrateUrgentLowAlarm()
  375. migrateLowAlarm()
  376. migrateHighAlarm()
  377. migrateUrgentHighAlarm()
  378. migrateFastDropAlarm()
  379. migrateFastRiseAlarm()
  380. migrateMissedReadingAlarm()
  381. migrateNotLoopingAlarm()
  382. migrateMissedBolusAlarm()
  383. migrateSensorChangeAlarm()
  384. migratePumpChangeAlarm()
  385. migrateOverrideStartAlarm()
  386. migrateOverrideEndAlarm()
  387. migrateTempTargetStartAlarm()
  388. migrateTempTargetEndAlarm()
  389. migrateTemporaryBGAlarm()
  390. migratePumpVolumeAlarm()
  391. migrateIOBAlarm()
  392. migrateCOBAlarm()
  393. migrateBatteryAlarm()
  394. migrateBatteryDropAlarm()
  395. migrateRecBolusAlarm()
  396. }
  397. // MARK: - One-off alarm migrations
  398. /// Reads *all* `alertUrgentLow*` keys, converts them into a single `Alarm`,
  399. /// saves it to the modern `[Alarm]` store, then deletes the legacy keys.
  400. private func migrateUrgentLowAlarm() {
  401. // Did the user ever change that alert? (No key ⇒ nothing to do.)
  402. guard UserDefaultsValue<Bool>(key: "alertUrgentLowActive", default: false).exists else { return }
  403. /// Helper: fetch-then-delete a legacy value in one line.
  404. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  405. let box = UserDefaultsValue<V>(key: key, default: def)
  406. defer { box.setNil(key: key) }
  407. return box.value
  408. }
  409. // Build the new Alarm ------------------------------------------------
  410. var alarm = Alarm(type: .low)
  411. alarm.name = "Urgent Low"
  412. alarm.isEnabled = take("alertUrgentLowActive", default: false)
  413. alarm.belowBG = Double(take("alertUrgentLowBG", default: 55.0))
  414. alarm.predictiveMinutes = take("alertUrgentLowPredictiveMinutes", default: 0)
  415. alarm.snoozeDuration = take("alertUrgentLowSnooze", default: 5)
  416. alarm.snoozedUntil = take("alertUrgentLowSnoozedTime", default: nil as Date?)
  417. alarm.soundFile = SoundFile(
  418. rawValue: take("alertUrgentLowSound",
  419. default: "Emergency_Alarm_Siren"))
  420. ?? .emergencyAlarmSiren
  421. alarm.playSoundOption = PlaySoundOption(
  422. rawValue: take("alertUrgentLowAudible",
  423. default: "Always").lowercased()) ?? .always
  424. alarm.repeatSoundOption = RepeatSoundOption(
  425. rawValue: take("alertUrgentLowRepeat",
  426. default: "Always").lowercased()) ?? .always
  427. // Day / Night active-window (“Pre-Snooze”)
  428. let autoStr = take("alertUrgentLowAutosnooze", default: "Never").lowercased()
  429. let dayFlag = take("alertUrgentLowAutosnoozeDay", default: false)
  430. let nightFlag = take("alertUrgentLowAutosnoozeNight", default: false)
  431. alarm.activeOption = {
  432. if dayFlag, !nightFlag { return .day }
  433. if !dayFlag, nightFlag { return .night }
  434. switch autoStr {
  435. case "day", "at day": return .day
  436. case "night", "at night": return .night
  437. default: return .always // “Never” → always active
  438. }
  439. }()
  440. // Persist -----------------------------------------------------------------
  441. Storage.shared.alarms.value.append(alarm)
  442. }
  443. // MARK: - Low-BG alarm -------------------------------------------------------
  444. private func migrateLowAlarm() {
  445. // Bail if the old keys were never written
  446. guard UserDefaultsValue<Bool>(key: "alertLowActive", default: false).exists else { return }
  447. // tiny helper: fetch value → erase key
  448. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  449. let box = UserDefaultsValue<V>(key: key, default: def)
  450. defer { box.setNil(key: key) }
  451. return box.value
  452. }
  453. // Build the new Alarm ----------------------------------------------------
  454. var alarm = Alarm(type: .low)
  455. alarm.name = "Low"
  456. alarm.isEnabled = take("alertLowActive", default: false)
  457. alarm.belowBG = Double(take("alertLowBG", default: 70.0))
  458. // “Persistent ≥ X min” → `persistentMinutes`
  459. alarm.persistentMinutes = take("alertLowPersistent", default: 0)
  460. // “Persistence max BG drop” -- ignoring this for now
  461. // let persistentLowTriggerImmediatelyBG = UserDefaultsRepository.alertLowBG.value - UserDefaultsRepository.alertLowPersistenceMax.value
  462. // (Float(persistentLowBG) <= UserDefaultsRepository.alertLowBG.value || Float(currentBG) <= persistentLowTriggerImmediatelyBG)
  463. _ = Double(take("alertLowPersistenceMax", default: 5.0))
  464. alarm.snoozeDuration = take("alertLowSnooze", default: 5)
  465. alarm.snoozedUntil = take("alertLowSnoozedTime", default: nil as Date?)
  466. alarm.soundFile = SoundFile(
  467. rawValue: take("alertLowSound",
  468. default: "Indeed")) ?? .indeed
  469. alarm.playSoundOption = PlaySoundOption(
  470. rawValue: take("alertLowAudible",
  471. default: "Always").lowercased()) ?? .always
  472. alarm.repeatSoundOption = RepeatSoundOption(
  473. rawValue: take("alertLowRepeat",
  474. default: "Always").lowercased()) ?? .always
  475. // activeOption ← legacy “Pre-Snooze” flags / picker
  476. let autoStr = take("alertLowAutosnooze", default: "Never").lowercased()
  477. let dayFlag = take("alertLowAutosnoozeDay", default: false)
  478. let nightFlag = take("alertLowAutosnoozeNight", default: false)
  479. alarm.activeOption = {
  480. if dayFlag, !nightFlag { return .day }
  481. if !dayFlag, nightFlag { return .night }
  482. switch autoStr {
  483. case "day", "at day": return .day
  484. case "night", "at night": return .night
  485. default: return .always
  486. }
  487. }()
  488. // Done → append to the modern store
  489. Storage.shared.alarms.value.append(alarm)
  490. }
  491. // MARK: - High-BG alarm -----------------------------------------------------
  492. private func migrateHighAlarm() {
  493. // Only run if the legacy key ever existed
  494. guard UserDefaultsValue<Bool>(key: "alertHighActive", default: false).exists else { return }
  495. /// Fetch → erase helper
  496. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  497. let box = UserDefaultsValue<V>(key: key, default: def)
  498. defer { box.setNil(key: key) } // remove legacy value
  499. return box.value
  500. }
  501. // ---------- Build Alarm -----------------------------------------------
  502. var alarm = Alarm(type: .high)
  503. alarm.name = "High"
  504. alarm.isEnabled = take("alertHighActive", default: false)
  505. alarm.aboveBG = Double(take("alertHighBG", default: 180.0))
  506. alarm.persistentMinutes = take("alertHighPersistent", default: 60)
  507. alarm.snoozeDuration = take("alertHighSnooze", default: 60)
  508. alarm.snoozedUntil = take("alertHighSnoozedTime", default: nil as Date?)
  509. alarm.soundFile = SoundFile(
  510. rawValue: take("alertHighSound",
  511. default: "Time_Has_Come")) ?? .timeHasCome
  512. alarm.playSoundOption = PlaySoundOption(
  513. rawValue: take("alertHighAudible",
  514. default: "Always").lowercased()) ?? .always
  515. alarm.repeatSoundOption = RepeatSoundOption(
  516. rawValue: take("alertHighRepeat",
  517. default: "Always").lowercased()) ?? .always
  518. // ── activeOption derived from “Pre-Snooze” picker & flags
  519. let autoStr = take("alertHighAutosnooze", default: "Never").lowercased()
  520. let dayFlag = take("alertHighAutosnoozeDay", default: false)
  521. let nightFlag = take("alertHighAutosnoozeNight", default: false)
  522. alarm.activeOption = {
  523. if dayFlag, !nightFlag { return .day }
  524. if !dayFlag, nightFlag { return .night }
  525. switch autoStr {
  526. case "day", "at day": return .day
  527. case "night", "at night": return .night
  528. default: return .always
  529. }
  530. }()
  531. // ---------- Persist & we’re done --------------------------------------
  532. Storage.shared.alarms.value.append(alarm)
  533. }
  534. // MARK: - Urgent-High alarm --------------------------------------------------
  535. private func migrateUrgentHighAlarm() {
  536. // run only once, only if the user ever changed that toggle
  537. guard UserDefaultsValue<Bool>(key: "alertUrgentHighActive", default: false).exists else { return }
  538. // helper: read-then-erase a legacy value
  539. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  540. let box = UserDefaultsValue<V>(key: key, default: def)
  541. defer { box.setNil(key: key) } // wipe legacy key
  542. return box.value
  543. }
  544. // ───────── Build the Alarm ────────────────────────────────────────────
  545. var alarm = Alarm(type: .high) // we map to the existing `.high` type
  546. alarm.name = "Urgent High"
  547. alarm.isEnabled = take("alertUrgentHighActive", default: false)
  548. alarm.aboveBG = Double(take("alertUrgentHighBG", default: 250.0))
  549. alarm.snoozeDuration = take("alertUrgentHighSnooze", default: 30)
  550. alarm.snoozedUntil = take("alertUrgentHighSnoozedTime", default: nil as Date?)
  551. alarm.soundFile = SoundFile(
  552. rawValue: take("alertUrgentHighSound",
  553. default: "Pager_Beeps")) ?? .pagerBeeps
  554. alarm.playSoundOption = PlaySoundOption(
  555. rawValue: take("alertUrgentHighAudible",
  556. default: "Always").lowercased()) ?? .always
  557. alarm.repeatSoundOption = RepeatSoundOption(
  558. rawValue: take("alertUrgentHighRepeat",
  559. default: "Always").lowercased()) ?? .always
  560. // activeOption comes from the old “Pre-Snooze” picker + its day/night flags
  561. let autoStr = take("alertUrgentHighAutosnooze", default: "Never").lowercased()
  562. let dayFlag = take("alertUrgentHighAutosnoozeDay", default: false)
  563. let nightFlag = take("alertUrgentHighAutosnoozeNight", default: false)
  564. alarm.activeOption = {
  565. if dayFlag, !nightFlag { return .day }
  566. if !dayFlag, nightFlag { return .night }
  567. switch autoStr { // fall back to picker value
  568. case "day", "at day": return .day
  569. case "night", "at night": return .night
  570. default: return .always
  571. }
  572. }()
  573. // ───────── Persist in new storage ─────────────────────────────────────
  574. Storage.shared.alarms.value.append(alarm)
  575. }
  576. // MARK: - Fast-Drop alarm ----------------------------------------------------
  577. private func migrateFastDropAlarm() {
  578. guard UserDefaultsValue<Bool>(key: "alertFastDropDeltaActive",
  579. default: false).exists else { return }
  580. // helper: read-then-erase
  581. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  582. let box = UserDefaultsValue<V>(key: k, default: d)
  583. defer { box.setNil(key: k) }
  584. return box.value
  585. }
  586. var alarm = Alarm(type: .fastDrop)
  587. alarm.name = "Fast Drop"
  588. alarm.isEnabled = take("alertFastDropDeltaActive", default: false)
  589. // core trigger parameters
  590. alarm.delta = Double(take("alertFastDropDelta", default: 10.0))
  591. alarm.monitoringWindow = take("alertFastDropReadings", default: 3) - 1 // store #readings
  592. if take("alertFastDropUseLimit", default: false) {
  593. alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0))
  594. }
  595. // snoozing
  596. alarm.snoozeDuration = take("alertFastDropDeltaSnooze", default: 10)
  597. alarm.snoozedUntil = take("alertFastDropSnoozedTime", default: nil as Date?)
  598. // sound + options
  599. alarm.soundFile = SoundFile(
  600. rawValue: take("alertFastDropSound", default: "Big_Clock_Ticking")
  601. ) ?? .bigClockTicking
  602. alarm.playSoundOption = PlaySoundOption(
  603. rawValue: take("alertFastDropAudible", default: "Always").lowercased()
  604. ) ?? .always
  605. alarm.repeatSoundOption = RepeatSoundOption(
  606. rawValue: take("alertFastDropRepeat", default: "Never").lowercased()
  607. ) ?? .never
  608. // activeOption from old “Pre-Snooze” picker + day/night flags
  609. let autoStr = take("alertFastDropAutosnooze", default: "Never").lowercased()
  610. let dayFlag = take("alertFastDropAutosnoozeDay", default: false)
  611. let nightFlag = take("alertFastDropAutosnoozeNight", default: false)
  612. alarm.activeOption = {
  613. if dayFlag, !nightFlag { return .day }
  614. if !dayFlag, nightFlag { return .night }
  615. switch autoStr {
  616. case "day", "at day": return .day
  617. case "night", "at night": return .night
  618. default: return .always
  619. }
  620. }()
  621. Storage.shared.alarms.value.append(alarm)
  622. }
  623. // MARK: - Fast-Rise alarm ----------------------------------------------------
  624. private func migrateFastRiseAlarm() {
  625. guard UserDefaultsValue<Bool>(key: "alertFastRiseDeltaActive",
  626. default: false).exists else { return }
  627. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  628. let box = UserDefaultsValue<V>(key: k, default: d)
  629. defer { box.setNil(key: k) }
  630. return box.value
  631. }
  632. var alarm = Alarm(type: .fastRise)
  633. alarm.name = "Fast Rise"
  634. alarm.isEnabled = take("alertFastRiseDeltaActive", default: false)
  635. alarm.delta = Double(take("alertFastRiseDelta", default: 10.0))
  636. alarm.monitoringWindow = take("alertFastRiseReadings", default: 3)
  637. if take("alertFastRiseUseLimit", default: false) {
  638. alarm.aboveBG = Double(take("alertFastRiseAboveBG", default: 200.0))
  639. }
  640. alarm.snoozeDuration = take("alertFastRiseDeltaSnooze", default: 10)
  641. alarm.snoozedUntil = take("alertFastRiseSnoozedTime", default: nil as Date?)
  642. alarm.soundFile = SoundFile(
  643. rawValue: take("alertFastRiseSound",
  644. default: "Cartoon_Fail_Strings_Trumpet")
  645. ) ?? .cartoonFailStringsTrumpet
  646. alarm.playSoundOption = PlaySoundOption(
  647. rawValue: take("alertFastRiseAudible", default: "Always").lowercased()
  648. ) ?? .always
  649. alarm.repeatSoundOption = RepeatSoundOption(
  650. rawValue: take("alertFastRiseRepeat", default: "Never").lowercased()
  651. ) ?? .never
  652. let autoStr = take("alertFastRiseAutosnooze", default: "Never").lowercased()
  653. let dayFlag = take("alertFastRiseAutosnoozeDay", default: false)
  654. let nightFlag = take("alertFastRiseAutosnoozeNight", default: false)
  655. alarm.activeOption = {
  656. if dayFlag, !nightFlag { return .day }
  657. if !dayFlag, nightFlag { return .night }
  658. switch autoStr {
  659. case "day", "at day": return .day
  660. case "night", "at night": return .night
  661. default: return .always
  662. }
  663. }()
  664. Storage.shared.alarms.value.append(alarm)
  665. }
  666. // MARK: - Missed-Reading alarm ---------------------------------------------
  667. private func migrateMissedReadingAlarm() {
  668. // Run only when the user has ever modified those settings
  669. guard UserDefaultsValue<Bool>(key: "alertMissedReadingActive",
  670. default: false).exists else { return }
  671. // read-then-erase helper
  672. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  673. let b = UserDefaultsValue<V>(key: k, default: d)
  674. defer { b.setNil(key: k) }
  675. return b.value
  676. }
  677. var alarm = Alarm(type: .missedReading)
  678. alarm.name = "Missed Reading"
  679. alarm.isEnabled = take("alertMissedReadingActive", default: false)
  680. // “No CGM data for X minutes”
  681. alarm.threshold = take("alertMissedReading", default: 31)
  682. // snoozing
  683. alarm.snoozeDuration = take("alertMissedReadingSnooze", default: 30)
  684. alarm.snoozedUntil = take("alertMissedReadingSnoozedTime",
  685. default: nil as Date?)
  686. // (legacy “is-snoozed” flag is implicit in snoozedUntil)
  687. // sound
  688. alarm.soundFile = SoundFile(
  689. rawValue: take("alertMissedReadingSound",
  690. default: "Cartoon_Tip_Toe_Sneaky_Walk")
  691. ) ?? .cartoonTipToeSneakyWalk
  692. // play / repeat options
  693. alarm.playSoundOption = PlaySoundOption(
  694. rawValue: take("alertMissedReadingAudible",
  695. default: "Always").lowercased()
  696. ) ?? .always
  697. alarm.repeatSoundOption = RepeatSoundOption(
  698. rawValue: take("alertMissedReadingRepeat",
  699. default: "Never").lowercased()
  700. ) ?? .never
  701. // activeOption ← “Pre-Snooze” picker + day/night flags
  702. let autoStr = take("alertMissedReadingAutosnooze", default: "Never")
  703. .lowercased()
  704. let dayFlag = take("alertMissedReadingAutosnoozeDay", default: false)
  705. let nightFlag = take("alertMissedReadingAutosnoozeNight", default: false)
  706. alarm.activeOption = {
  707. if dayFlag, !nightFlag { return .day }
  708. if !dayFlag, nightFlag { return .night }
  709. switch autoStr {
  710. case "day", "at day": return .day
  711. case "night", "at night": return .night
  712. default: return .always // “Never” → always on
  713. }
  714. }()
  715. // store in the new world
  716. Storage.shared.alarms.value.append(alarm)
  717. }
  718. // MARK: - “Not Looping” (legacy → Storage.shared.alarms)
  719. private func migrateNotLoopingAlarm() {
  720. // Check if the user ever configured this alarm
  721. let activeFlag = UserDefaultsValue<Bool>(key: "alertNotLoopingActive", default: false)
  722. guard activeFlag.exists else { return } // nothing to migrate
  723. // Convenience: read-then-erase
  724. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  725. let box = UserDefaultsValue<V>(key: key, default: def)
  726. defer { box.setNil(key: key) } // wipe after reading
  727. return box.value
  728. }
  729. // Build the new Alarm ---------------------------------------------------
  730. var alarm = Alarm(type: .notLooping)
  731. alarm.name = "Not Looping Alert"
  732. alarm.isEnabled = take("alertNotLoopingActive", default: false)
  733. alarm.threshold = Double(take("alertNotLooping", default: 31)) // minutes
  734. alarm.snoozeDuration = take("alertNotLoopingSnooze", default: 30)
  735. alarm.snoozedUntil = take("alertNotLoopingSnoozedTime", default: nil as Date?)
  736. alarm.soundFile = SoundFile(rawValue:
  737. take("alertNotLoopingSound",
  738. default: "Sci-Fi_Engine_Shut_Down")) ?? .sciFiEngineShutDown
  739. // ── ACTIVE-DURING (day/night) ← old **Pre-Snooze** flags --------------
  740. let actDay = take("alertNotLoopingAutosnoozeDay", default: false)
  741. let actNight = take("alertNotLoopingAutosnoozeNight", default: false)
  742. alarm.activeOption = {
  743. switch (actDay, actNight) {
  744. case (true, true): return .always
  745. case (true, false): return .day
  746. case (false, true): return .night
  747. default: return .always // “Never” in old UI
  748. }
  749. }()
  750. // ── PLAY-SOUND option ---------------------------------------------------
  751. let playStr = take("alertNotLoopingAudible", default: "Always").lowercased()
  752. let playDay = take("alertNotLoopingDayTimeAudible", default: true)
  753. let playNight = take("alertNotLoopingNightTimeAudible", default: true)
  754. alarm.playSoundOption = {
  755. if !playDay, !playNight { return .never }
  756. else if playDay, playNight { return .always }
  757. else if playDay { return .day }
  758. else { return .night }
  759. }()
  760. // ── REPEAT-SOUND option -------------------------------------------------
  761. let repStr = take("alertNotLoopingRepeat", default: "Never").lowercased()
  762. let repDay = take("alertNotLoopingDayTime", default: false)
  763. let repNight = take("alertNotLoopingNightTime", default: false)
  764. alarm.repeatSoundOption = {
  765. if repDay, repNight { return .always }
  766. else if repDay, !repNight { return .day }
  767. else if repNight, !repDay { return .night }
  768. else { return .never }
  769. }()
  770. // ── BG-limit guard ------------------------------------------------------
  771. if take("alertNotLoopingUseLimits", default: false) {
  772. alarm.belowBG = Double(take("alertNotLoopingLowerLimit", default: 100.0))
  773. alarm.aboveBG = Double(take("alertNotLoopingUpperLimit", default: 160.0))
  774. }
  775. // ── Per-alarm snooze state ---------------------------------------------
  776. if !take("alertNotLoopingIsSnoozed", default: false) {
  777. alarm.snoozedUntil = nil // ignore stored date if flag isn’t set
  778. }
  779. // Persist & finish -------------------------------------------------------
  780. var list = Storage.shared.alarms.value
  781. list.append(alarm)
  782. Storage.shared.alarms.value = list
  783. }
  784. // MARK: – Missed-Bolus alarm -------------------------------------------------
  785. private func migrateMissedBolusAlarm() {
  786. // Was the old alarm ever configured?
  787. let legacyActive = UserDefaultsValue<Bool>(key: "alertMissedBolusActive",
  788. default: false)
  789. guard legacyActive.exists else { return } // nothing to do
  790. // helper: read-then-delete ------------------------------------------------
  791. func take<V: AnyConvertible & Equatable>(_ k: String,
  792. _ def: V) -> V
  793. {
  794. let box = UserDefaultsValue<V>(key: k, default: def)
  795. defer { box.setNil(key: k) } // wipe after reading
  796. return box.value
  797. }
  798. // ────────────────────────────────────────────────────────────────────────
  799. // Build the new Alarm
  800. // ────────────────────────────────────────────────────────────────────────
  801. var alarm = Alarm(type: .missedBolus)
  802. alarm.name = "Missed Bolus Alert"
  803. alarm.isEnabled = take("alertMissedBolusActive", false)
  804. // core timings
  805. alarm.monitoringWindow = take("alertMissedBolus", 10) // delay
  806. alarm.predictiveMinutes = take("alertMissedBolusPrebolus", 20) // pre-bolus window
  807. alarm.snoozeDuration = take("alertMissedBolusSnooze", 10)
  808. // snooze-state
  809. if take("alertMissedBolusIsSnoozed", false) {
  810. alarm.snoozedUntil = take("alertMissedBolusSnoozedTime",
  811. nil as Date?)
  812. }
  813. // carb / bolus filters
  814. alarm.delta = take("alertMissedBolusIgnoreBolus", 0.5)
  815. alarm.threshold = Double(
  816. take("alertMissedBolusLowGrams", 10))
  817. alarm.aboveBG = Double(
  818. take("alertMissedBolusLowGramsBG", 70.0))
  819. // sound & tone
  820. alarm.soundFile = SoundFile(rawValue:
  821. take("alertMissedBolusSound",
  822. "Dhol_Shuffleloop")) ?? .dholShuffleloop
  823. // ── ACTIVE-DURING ← old “Pre-Snooze” flags
  824. let actDay = take("alertMissedBolusAutosnoozeDay", false)
  825. let actNight = take("alertMissedBolusAutosnoozeNight", false)
  826. alarm.activeOption = {
  827. switch (actDay, actNight) {
  828. case (true, true): return .always
  829. case (true, false): return .day
  830. case (false, true): return .night
  831. default: return .always // “Never” → always
  832. }
  833. }()
  834. // ── PLAY-SOUND ← old “PlaySound” picker
  835. let playDay = take("alertMissedBolusDayTimeAudible", true)
  836. let playNight = take("alertMissedBolusNightTimeAudible", true)
  837. alarm.playSoundOption = {
  838. switch (playDay, playNight) {
  839. case (true, true): return .always
  840. case (true, false): return .day
  841. case (false, true): return .night
  842. default: return .never
  843. }
  844. }()
  845. // ── REPEAT-SOUND ← old “Repeat Sound” picker
  846. let repDay = take("alertMissedBolusDayTime", false)
  847. let repNight = take("alertMissedBolusNightTime", false)
  848. alarm.repeatSoundOption = {
  849. switch (repDay, repNight) {
  850. case (true, true): return .always
  851. case (true, false): return .day
  852. case (false, true): return .night
  853. default: return .never
  854. }
  855. }()
  856. // (The deprecated “alertMissedBolusQuiet” key is ignored.)
  857. // ── Store & finish ------------------------------------------------------
  858. var list = Storage.shared.alarms.value
  859. list.append(alarm)
  860. Storage.shared.alarms.value = list
  861. }
  862. // ─────────────────────────────────────────────────────────────────────────────
  863. // MARK: SAGE → .sensorChange
  864. // ─────────────────────────────────────────────────────────────────────────────
  865. private func migrateSensorChangeAlarm() {
  866. // Was the old setting ever stored?
  867. let flag = UserDefaultsValue<Bool>(key: "alertSAGEActive", default: false)
  868. guard flag.exists else { return }
  869. // tiny helper that *reads + wipes* a legacy key
  870. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  871. let b = UserDefaultsValue<V>(key: k, default: def)
  872. defer { b.setNil(key: k) }
  873. return b.value
  874. }
  875. var alarm = Alarm(type: .sensorChange)
  876. alarm.name = "Sensor Change Reminder"
  877. alarm.isEnabled = take("alertSAGEActive", false)
  878. alarm.threshold = Double(take("alertSAGE", 8)) // hours
  879. alarm.snoozeDuration = take("alertSAGESnooze", 2) // hours
  880. if take("alertSAGEIsSnoozed", false) {
  881. alarm.snoozedUntil = take("alertSAGESnoozedTime", nil as Date?)
  882. }
  883. alarm.soundFile = SoundFile(rawValue:
  884. take("alertSAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou
  885. // ACTIVE (day / night)
  886. let actDay = take("alertSAGEAutosnoozeDay", false)
  887. let actNight = take("alertSAGEAutosnoozeNight", true)
  888. alarm.activeOption = {
  889. switch (actDay, actNight) {
  890. case (true, true): return .always
  891. case (true, false): return .day
  892. case (false, true): return .night
  893. default: return .always
  894. }
  895. }()
  896. // PLAY sound
  897. let playDay = take("alertSAGEDayTimeAudible", true)
  898. let playNight = take("alertSAGENightTimeAudible", true)
  899. alarm.playSoundOption = {
  900. switch (playDay, playNight) {
  901. case (true, true): return .always
  902. case (true, false): return .day
  903. case (false, true): return .night
  904. default: return .never
  905. }
  906. }()
  907. // REPEAT sound
  908. let repDay = take("alertSAGEDayTime", false)
  909. let repNight = take("alertSAGENightTime", false)
  910. alarm.repeatSoundOption = {
  911. switch (repDay, repNight) {
  912. case (true, true): return .always
  913. case (true, false): return .day
  914. case (false, true): return .night
  915. default: return .never
  916. }
  917. }()
  918. // Persist
  919. var list = Storage.shared.alarms.value
  920. list.append(alarm)
  921. Storage.shared.alarms.value = list
  922. }
  923. // ─────────────────────────────────────────────────────────────────────────────
  924. // MARK: CAGE → .pumpChange
  925. // ─────────────────────────────────────────────────────────────────────────────
  926. private func migratePumpChangeAlarm() {
  927. let flag = UserDefaultsValue<Bool>(key: "alertCAGEActive", default: false)
  928. guard flag.exists else { return }
  929. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  930. let b = UserDefaultsValue<V>(key: k, default: def)
  931. defer { b.setNil(key: k) }
  932. return b.value
  933. }
  934. var alarm = Alarm(type: .pumpChange)
  935. alarm.name = "Pump / Cannula Change"
  936. alarm.isEnabled = take("alertCAGEActive", false)
  937. alarm.threshold = Double(take("alertCAGE", 4)) // hours
  938. alarm.snoozeDuration = take("alertCAGESnooze", 2) // hours
  939. if take("alertCAGEIsSnoozed", false) {
  940. alarm.snoozedUntil = take("alertCAGESnoozedTime", nil as Date?)
  941. }
  942. alarm.soundFile = SoundFile(rawValue:
  943. take("alertCAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou
  944. // ACTIVE
  945. let actDay = take("alertCAGEAutosnoozeDay", false)
  946. let actNight = take("alertCAGEAutosnoozeNight", true)
  947. alarm.activeOption = {
  948. switch (actDay, actNight) {
  949. case (true, true): return .always
  950. case (true, false): return .day
  951. case (false, true): return .night
  952. default: return .always
  953. }
  954. }()
  955. // PLAY
  956. let playDay = take("alertCAGEDayTimeAudible", true)
  957. let playNight = take("alertCAGENightTimeAudible", true)
  958. alarm.playSoundOption = {
  959. switch (playDay, playNight) {
  960. case (true, true): return .always
  961. case (true, false): return .day
  962. case (false, true): return .night
  963. default: return .never
  964. }
  965. }()
  966. // REPEAT
  967. let repDay = take("alertCAGEDayTime", false)
  968. let repNight = take("alertCAGENightTime", false)
  969. alarm.repeatSoundOption = {
  970. switch (repDay, repNight) {
  971. case (true, true): return .always
  972. case (true, false): return .day
  973. case (false, true): return .night
  974. default: return .never
  975. }
  976. }()
  977. var list = Storage.shared.alarms.value
  978. list.append(alarm)
  979. Storage.shared.alarms.value = list
  980. }
  981. // ─────────────────────────────────────────────────────────────────────────────
  982. // MARK: Override-Start → .overrideStart
  983. // ─────────────────────────────────────────────────────────────────────────────
  984. private func migrateOverrideStartAlarm() {
  985. let exists = UserDefaultsValue<Bool>(key: "alertOverrideStart", default: false)
  986. guard exists.exists else { return } // user never touched it
  987. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  988. let box = UserDefaultsValue<V>(key: k, default: def)
  989. defer { box.setNil(key: k) } // wipe after reading
  990. return box.value
  991. }
  992. var alarm = Alarm(type: .overrideStart)
  993. alarm.name = "Override Started"
  994. alarm.isEnabled = take("alertOverrideStart", false)
  995. alarm.snoozeDuration = 5 // legacy UI had no stepper
  996. if take("alertOverrideStartIsSnoozed", false) {
  997. alarm.snoozedUntil = take("alertOverrideStartSnoozedTime", nil as Date?)
  998. }
  999. alarm.soundFile = SoundFile(
  1000. rawValue: take("alertOverrideStartSound", "Ending_Reached")
  1001. ) ?? .endingReached
  1002. // ── ACTIVE (legacy “Pre-Snooze” day/night flags) ──────────────
  1003. let actDay = take("alertOverrideStartAutosnoozeDay", false)
  1004. let actNight = take("alertOverrideStartAutosnoozeNight", false)
  1005. alarm.activeOption = {
  1006. switch (actDay, actNight) {
  1007. case (true, true): return .always
  1008. case (true, false): return .day
  1009. case (false, true): return .night
  1010. default: return .always
  1011. }
  1012. }()
  1013. // ── PLAY (legacy “Play Sound” day/night flags) ───────────────
  1014. let playDay = take("alertOverrideStartDayTimeAudible", true)
  1015. let playNight = take("alertOverrideStartNightTimeAudible", true)
  1016. alarm.playSoundOption = {
  1017. switch (playDay, playNight) {
  1018. case (true, true): return .always
  1019. case (true, false): return .day
  1020. case (false, true): return .night
  1021. default: return .never
  1022. }
  1023. }()
  1024. // ── REPEAT (legacy “Repeat Sound” day/night flags) ───────────
  1025. let repDay = take("alertOverrideStartDayTime", false)
  1026. let repNight = take("alertOverrideStartNightTime", false)
  1027. alarm.repeatSoundOption = {
  1028. switch (repDay, repNight) {
  1029. case (true, true): return .always
  1030. case (true, false): return .day
  1031. case (false, true): return .night
  1032. default: return .never
  1033. }
  1034. }()
  1035. // ignore & wipe unused keys
  1036. _ = take("alertOverrideStartQuiet", false as Bool)
  1037. _ = take("alertOverrideStartRepeatAudible", "Always" as String)
  1038. var list = Storage.shared.alarms.value
  1039. list.append(alarm)
  1040. Storage.shared.alarms.value = list
  1041. }
  1042. // ─────────────────────────────────────────────────────────────────────────────
  1043. // MARK: Override-End → .overrideEnd
  1044. // ─────────────────────────────────────────────────────────────────────────────
  1045. private func migrateOverrideEndAlarm() {
  1046. let exists = UserDefaultsValue<Bool>(key: "alertOverrideEnd", default: false)
  1047. guard exists.exists else { return }
  1048. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  1049. let box = UserDefaultsValue<V>(key: k, default: def)
  1050. defer { box.setNil(key: k) }
  1051. return box.value
  1052. }
  1053. var alarm = Alarm(type: .overrideEnd)
  1054. alarm.name = "Override Ended"
  1055. alarm.isEnabled = take("alertOverrideEnd", false)
  1056. alarm.snoozeDuration = 5
  1057. if take("alertOverrideEndIsSnoozed", false) {
  1058. alarm.snoozedUntil = take("alertOverrideEndSnoozedTime", nil as Date?)
  1059. }
  1060. alarm.soundFile = SoundFile(
  1061. rawValue: take("alertOverrideEndSound", "Alert_Tone_Busy")
  1062. ) ?? .alertToneBusy
  1063. // ACTIVE
  1064. let actDay = take("alertOverrideEndAutosnoozeDay", false)
  1065. let actNight = take("alertOverrideEndAutosnoozeNight", false)
  1066. alarm.activeOption = {
  1067. switch (actDay, actNight) {
  1068. case (true, true): return .always
  1069. case (true, false): return .day
  1070. case (false, true): return .night
  1071. default: return .always
  1072. }
  1073. }()
  1074. // PLAY
  1075. let playDay = take("alertOverrideEndDayTimeAudible", true)
  1076. let playNight = take("alertOverrideEndNightTimeAudible", true)
  1077. alarm.playSoundOption = {
  1078. switch (playDay, playNight) {
  1079. case (true, true): return .always
  1080. case (true, false): return .day
  1081. case (false, true): return .night
  1082. default: return .never
  1083. }
  1084. }()
  1085. // REPEAT
  1086. let repDay = take("alertOverrideEndDayTime", false)
  1087. let repNight = take("alertOverrideEndNightTime", false)
  1088. alarm.repeatSoundOption = {
  1089. switch (repDay, repNight) {
  1090. case (true, true): return .always
  1091. case (true, false): return .day
  1092. case (false, true): return .night
  1093. default: return .never
  1094. }
  1095. }()
  1096. // wipe unused keys
  1097. _ = take("alertOverrideEndQuiet", false as Bool)
  1098. _ = take("alertOverrideEndRepeatAudible", "Always" as String)
  1099. var list = Storage.shared.alarms.value
  1100. list.append(alarm)
  1101. Storage.shared.alarms.value = list
  1102. }
  1103. // MARK: ––––– Temp-Target START → .tempTargetStart –––––
  1104. private func migrateTempTargetStartAlarm() {
  1105. let touched = UserDefaultsValue<Bool>(key: "alertTempTargetStart", default: false)
  1106. guard touched.exists else { return }
  1107. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  1108. let box = UserDefaultsValue<V>(key: k, default: def)
  1109. defer { box.setNil(key: k) } // scrub after read
  1110. return box.value
  1111. }
  1112. var alarm = Alarm(type: .tempTargetStart)
  1113. alarm.name = "Temp Target Started"
  1114. alarm.isEnabled = take("alertTempTargetStart", false)
  1115. alarm.snoozeDuration = 5
  1116. if take("alertTempTargetStartIsSnoozed", false) {
  1117. alarm.snoozedUntil = take("alertTempTargetStartSnoozedTime", nil as Date?)
  1118. }
  1119. alarm.soundFile = SoundFile(
  1120. rawValue: take("alertTempTargetStartSound", "Ending_Reached")
  1121. ) ?? .endingReached
  1122. // ACTIVE ← legacy Pre-Snooze day/night flags
  1123. alarm.activeOption = {
  1124. let d = take("alertTempTargetStartAutosnoozeDay", false)
  1125. let n = take("alertTempTargetStartAutosnoozeNight", false)
  1126. switch (d, n) { case (true, true): return .always
  1127. case (true, false): return .day
  1128. case (false, true): return .night
  1129. default: return .always }
  1130. }()
  1131. // PLAY
  1132. alarm.playSoundOption = {
  1133. let d = take("alertTempTargetStartDayTimeAudible", true)
  1134. let n = take("alertTempTargetStartNightTimeAudible", true)
  1135. switch (d, n) { case (true, true): return .always
  1136. case (true, false): return .day
  1137. case (false, true): return .night
  1138. default: return .never }
  1139. }()
  1140. // REPEAT
  1141. alarm.repeatSoundOption = {
  1142. let d = take("alertTempTargetStartDayTime", false)
  1143. let n = take("alertTempTargetStartNightTime", false)
  1144. switch (d, n) { case (true, true): return .always
  1145. case (true, false): return .day
  1146. case (false, true): return .night
  1147. default: return .never }
  1148. }()
  1149. // wipe “quiet / RepeatAudible” extras
  1150. _ = take("alertTempTargetStartQuiet", false as Bool)
  1151. _ = take("alertTempTargetStartRepeatAudible", "Always" as String)
  1152. var list = Storage.shared.alarms.value
  1153. list.append(alarm)
  1154. Storage.shared.alarms.value = list
  1155. }
  1156. // MARK: ––––– Temp-Target END → .tempTargetEnd –––––
  1157. private func migrateTempTargetEndAlarm() {
  1158. let touched = UserDefaultsValue<Bool>(key: "alertTempTargetEnd", default: false)
  1159. guard touched.exists else { return }
  1160. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  1161. let box = UserDefaultsValue<V>(key: k, default: def)
  1162. defer { box.setNil(key: k) }
  1163. return box.value
  1164. }
  1165. var alarm = Alarm(type: .tempTargetEnd)
  1166. alarm.name = "Temp Target Ended"
  1167. alarm.isEnabled = take("alertTempTargetEnd", false)
  1168. alarm.snoozeDuration = 5
  1169. if take("alertTempTargetEndIsSnoozed", false) {
  1170. alarm.snoozedUntil = take("alertTempTargetEndSnoozedTime", nil as Date?)
  1171. }
  1172. alarm.soundFile = SoundFile(
  1173. rawValue: take("alertTempTargetEndSound", "Alert_Tone_Busy")
  1174. ) ?? .alertToneBusy
  1175. alarm.activeOption = {
  1176. let d = take("alertTempTargetEndAutosnoozeDay", false)
  1177. let n = take("alertTempTargetEndAutosnoozeNight", false)
  1178. switch (d, n) { case (true, true): return .always
  1179. case (true, false): return .day
  1180. case (false, true): return .night
  1181. default: return .always }
  1182. }()
  1183. alarm.playSoundOption = {
  1184. let d = take("alertTempTargetEndDayTimeAudible", true)
  1185. let n = take("alertTempTargetEndNightTimeAudible", true)
  1186. switch (d, n) { case (true, true): return .always
  1187. case (true, false): return .day
  1188. case (false, true): return .night
  1189. default: return .never }
  1190. }()
  1191. alarm.repeatSoundOption = {
  1192. let d = take("alertTempTargetEndDayTime", false)
  1193. let n = take("alertTempTargetEndNightTime", false)
  1194. switch (d, n) { case (true, true): return .always
  1195. case (true, false): return .day
  1196. case (false, true): return .night
  1197. default: return .never }
  1198. }()
  1199. _ = take("alertTempTargetEndQuiet", false as Bool)
  1200. _ = take("alertTempTargetEndRepeatAudible", "Always" as String)
  1201. var list = Storage.shared.alarms.value
  1202. list.append(alarm)
  1203. Storage.shared.alarms.value = list
  1204. }
  1205. // MARK: ––––– TEMPORARY BG LIMIT → .temporary –––––
  1206. private func migrateTemporaryBGAlarm() {
  1207. let flag = UserDefaultsValue<Bool>(key: "alertTemporaryActive", default: false)
  1208. guard flag.exists else { return }
  1209. func take<V: AnyConvertible & Equatable>(_ k: String, _ d: V) -> V {
  1210. let box = UserDefaultsValue<V>(key: k, default: d)
  1211. defer { box.setNil(key: k) }
  1212. return box.value
  1213. }
  1214. var alarm = Alarm(type: .temporary)
  1215. alarm.name = "Temporary BG Limit"
  1216. alarm.isEnabled = take("alertTemporaryActive", false)
  1217. // limit direction ↓ / ↑
  1218. let limit = Double(take("alertTemporaryBG", 90.0 as Float))
  1219. if take("alertTemporaryBelow", true) {
  1220. alarm.belowBG = limit
  1221. } else {
  1222. alarm.aboveBG = limit
  1223. }
  1224. // audio & repeat
  1225. alarm.soundFile = SoundFile(rawValue:
  1226. take("alertTemporarySound", "Indeed")) ?? .indeed
  1227. alarm.playSoundOption = take("alertTemporaryBGAudible", true) ? .always : .never
  1228. alarm.repeatSoundOption = take("alertTemporaryBGRepeat", false) ? .always : .never
  1229. Storage.shared.alarms.value.append(alarm)
  1230. }
  1231. // MARK: ––––– PUMP RESERVOIR LEVEL → .pump –––––
  1232. private func migratePumpVolumeAlarm() {
  1233. let flag = UserDefaultsValue<Bool>(key: "alertPump", default: false)
  1234. guard flag.exists else { return }
  1235. func take<V: AnyConvertible & Equatable>(_ k: String, _ d: V) -> V {
  1236. let box = UserDefaultsValue<V>(key: k, default: d)
  1237. defer { box.setNil(key: k) }
  1238. return box.value
  1239. }
  1240. var alarm = Alarm(type: .pump)
  1241. alarm.name = "Pump Reservoir"
  1242. alarm.isEnabled = take("alertPump", false)
  1243. alarm.threshold = Double(take("alertPumpAt", 10)) // units left
  1244. // Snooze — stored in hours, so keep as-is.
  1245. alarm.snoozeDuration = take("alertPumpSnoozeHours", 5)
  1246. if take("alertPumpIsSnoozed", false) {
  1247. alarm.snoozedUntil = take("alertPumpSnoozedTime", nil as Date?)
  1248. }
  1249. alarm.soundFile = SoundFile(rawValue:
  1250. take("alertPumpSound", "Marimba_Descend")) ?? .marimbaDescend
  1251. // PLAY-sound option (day / night / never)
  1252. alarm.playSoundOption = {
  1253. let d = take("alertPumpDayTimeAudible", true)
  1254. let n = take("alertPumpNightTimeAudible", true)
  1255. switch (d, n) {
  1256. case (true, true): return .always
  1257. case (true, false): return .day
  1258. case (false, true): return .night
  1259. default: return .never
  1260. }
  1261. }()
  1262. // REPEAT-sound option – derived from legacy picker flags
  1263. alarm.repeatSoundOption = {
  1264. let d = take("alertPumpDayTime", false)
  1265. let n = take("alertPumpNightTime", false)
  1266. switch (d, n) {
  1267. case (true, true): return .always
  1268. case (true, false): return .day
  1269. case (false, true): return .night
  1270. default: return .never
  1271. }
  1272. }()
  1273. // ACTIVE day/night ← old “Pre-Snooze” flags
  1274. alarm.activeOption = {
  1275. let d = take("alertPumpAutosnoozeDay", false)
  1276. let n = take("alertPumpAutosnoozeNight", false)
  1277. switch (d, n) {
  1278. case (true, true): return .always
  1279. case (true, false): return .day
  1280. case (false, true): return .night
  1281. default: return .always
  1282. }
  1283. }()
  1284. // Discard no-longer-needed extras
  1285. _ = take("alertPumpQuiet", false as Bool)
  1286. _ = take("alertPumpRepeat", "Never" as String)
  1287. _ = take("alertPumpAudible", "Always" as String)
  1288. _ = take("alertPumpAutosnooze", "Never" as String)
  1289. Storage.shared.alarms.value.append(alarm)
  1290. }
  1291. // -----------------------------------------------------------------------------
  1292. // MARK: - Helpers (place them once, near the top of the migrate() file)
  1293. // -----------------------------------------------------------------------------
  1294. /// Legacy picker + day/night flags → PlaySoundOption
  1295. private func playOption(from picker: String,
  1296. dayFlag: Bool,
  1297. nightFlag: Bool) -> PlaySoundOption
  1298. {
  1299. let p = picker.lowercased()
  1300. if p == "never" { return .never }
  1301. if p == "always" { return .always }
  1302. switch (dayFlag, nightFlag) {
  1303. case (true, true): return .always
  1304. case (true, false): return .day
  1305. case (false, true): return .night
  1306. default: return .never
  1307. }
  1308. }
  1309. /// Legacy picker + day/night flags → RepeatSoundOption
  1310. private func repeatOption(from picker: String,
  1311. dayFlag: Bool,
  1312. nightFlag: Bool) -> RepeatSoundOption
  1313. {
  1314. let p = picker.lowercased()
  1315. if p == "never" { return .never }
  1316. if p == "always" { return .always }
  1317. switch (dayFlag, nightFlag) {
  1318. case (true, true): return .always
  1319. case (true, false): return .day
  1320. case (false, true): return .night
  1321. default: return .never
  1322. }
  1323. }
  1324. /// Convenience: load-then-erase a legacy `UserDefaultsValue`
  1325. private func take<V: AnyConvertible & Equatable>(
  1326. _ key: String,
  1327. _ defaultValue: V
  1328. ) -> V {
  1329. let box = UserDefaultsValue<V>(key: key, default: defaultValue)
  1330. defer { box.setNil(key: key) }
  1331. return box.value
  1332. }
  1333. // -----------------------------------------------------------------------------
  1334. // MARK: - IOB Alarm migration
  1335. // -----------------------------------------------------------------------------
  1336. private func migrateIOBAlarm() {
  1337. // Only migrate if the user ever touched IOB alarm settings
  1338. guard UserDefaultsValue<Bool>(key: "alertIOB", default: false).exists else {
  1339. return
  1340. }
  1341. var alarm = Alarm(type: .iob)
  1342. alarm.name = "High IOB"
  1343. alarm.isEnabled = take("alertIOB", false)
  1344. alarm.aboveBG = nil // BG band not used
  1345. alarm.threshold = Double(take("alertIOBAt", 1.5)) // units threshold
  1346. alarm.bolusCountThreshold = take("alertIOBNumber", 3)
  1347. alarm.bolusWindowMinutes = take("alertIOBBolusesWithin", 60)
  1348. alarm.delta = Double(take("alertIOBMaxBoluses", 10)) // max total bolus
  1349. alarm.snoozeDuration = take("alertIOBSnoozeHours", 1) * 60 // hours → minutes
  1350. alarm.snoozedUntil = take("alertIOBSnoozedTime", nil as Date?)
  1351. alarm.soundFile = SoundFile(rawValue:
  1352. take("alertIOBSound", "Alert_Tone_Ringtone_1"))
  1353. ?? .alertToneRingtone1
  1354. // Audio options
  1355. alarm.playSoundOption = playOption(
  1356. from: take("alertIOBAudible", "Always"),
  1357. dayFlag: take("alertIOBDayTimeAudible", true),
  1358. nightFlag: take("alertIOBNightTimeAudible", true)
  1359. )
  1360. alarm.repeatSoundOption = repeatOption(
  1361. from: take("alertIOBRepeat", "Always"),
  1362. dayFlag: take("alertIOBDayTime", true),
  1363. nightFlag: take("alertIOBNightTime", true)
  1364. )
  1365. // Active (day/night) option comes from the legacy “Pre-Snooze” picker,
  1366. // but the IOB alarm never had one, so treat it as always-on.
  1367. alarm.activeOption = .always
  1368. // Persist
  1369. Storage.shared.alarms.value.append(alarm)
  1370. }
  1371. // -----------------------------------------------------------------------------
  1372. // MARK: - COB Alarm migration
  1373. // -----------------------------------------------------------------------------
  1374. private func migrateCOBAlarm() {
  1375. // Only migrate if the user ever touched COB alarm settings
  1376. guard UserDefaultsValue<Bool>(key: "alertCOB", default: false).exists else {
  1377. return
  1378. }
  1379. var alarm = Alarm(type: .cob)
  1380. alarm.name = "High COB"
  1381. alarm.isEnabled = take("alertCOB", false)
  1382. alarm.threshold = Double(take("alertCOBAt", 50)) // grams threshold
  1383. alarm.snoozeDuration = take("alertCOBSnoozeHours", 1) * 60 // hours → minutes
  1384. alarm.snoozedUntil = take("alertCOBSnoozedTime", nil as Date?)
  1385. alarm.soundFile = SoundFile(rawValue:
  1386. take("alertCOBSound", "Alert_Tone_Ringtone_2"))
  1387. ?? .alertToneRingtone2
  1388. // Audio options
  1389. alarm.playSoundOption = playOption(
  1390. from: take("alertCOBAudible", "Always"),
  1391. dayFlag: take("alertCOBDayTimeAudible", true),
  1392. nightFlag: take("alertCOBNightTimeAudible", true)
  1393. )
  1394. alarm.repeatSoundOption = repeatOption(
  1395. from: take("alertCOBRepeat", "Always"),
  1396. dayFlag: take("alertCOBDayTime", true),
  1397. nightFlag: take("alertCOBNightTime", true)
  1398. )
  1399. alarm.activeOption = .always // same reason as above
  1400. // Persist
  1401. Storage.shared.alarms.value.append(alarm)
  1402. }
  1403. // =============================================================================
  1404. // BATTERY-LEVEL alarm (old keys → .battery)
  1405. // =============================================================================
  1406. private func migrateBatteryAlarm() {
  1407. guard UserDefaultsValue<Bool>(key: "alertBatteryActive",
  1408. default: false).exists else { return }
  1409. var alarm = Alarm(type: .battery)
  1410. alarm.name = "Low Battery"
  1411. alarm.isEnabled = take("alertBatteryActive", false)
  1412. alarm.threshold = Double(take("alertBatteryLevel", 25)) // %
  1413. alarm.snoozeDuration = take("alertBatterySnoozeHours", 1) * 60
  1414. alarm.snoozedUntil = take("alertBatterySnoozedTime", nil as Date?)
  1415. alarm.soundFile = SoundFile(rawValue:
  1416. take("alertBatterySound", "Machine_Charge"))
  1417. ?? .machineCharge
  1418. // ── audio – legacy had a simple Bool “repeat / no-repeat”
  1419. let rpt = take("alertBatteryRepeat", true)
  1420. alarm.playSoundOption = .always // no day/night picker in legacy UI
  1421. alarm.repeatSoundOption = rpt ? .always : .never
  1422. alarm.activeOption = .always // no day/night activation picker
  1423. Storage.shared.alarms.value.append(alarm)
  1424. }
  1425. // =============================================================================
  1426. // BATTERY-DROP alarm (old keys → .batteryDrop)
  1427. // =============================================================================
  1428. private func migrateBatteryDropAlarm() {
  1429. guard UserDefaultsValue<Bool>(key: "alertBatteryDropActive",
  1430. default: false).exists else { return }
  1431. var alarm = Alarm(type: .batteryDrop)
  1432. alarm.name = "Battery Drop"
  1433. alarm.isEnabled = take("alertBatteryDropActive", false)
  1434. alarm.delta = Double(take("alertBatteryDropPercentage", 5)) // % drop
  1435. alarm.monitoringWindow = take("alertBatteryDropPeriod", 15) // min
  1436. alarm.snoozeDuration = take("alertBatteryDropSnoozeHours", 1) * 60
  1437. alarm.snoozedUntil = take("alertBatteryDropSnoozedTime", nil as Date?)
  1438. alarm.soundFile = SoundFile(rawValue:
  1439. take("alertBatteryDropSound", "Machine_Charge"))
  1440. ?? .machineCharge
  1441. let rpt = take("alertBatteryDropRepeat", true)
  1442. alarm.playSoundOption = .always
  1443. alarm.repeatSoundOption = rpt ? .always : .never
  1444. alarm.activeOption = .always
  1445. Storage.shared.alarms.value.append(alarm)
  1446. }
  1447. // =============================================================================
  1448. // REC-BOLUS alarm (old keys → .recBolus)
  1449. // =============================================================================
  1450. private func migrateRecBolusAlarm() {
  1451. guard UserDefaultsValue<Bool>(key: "alertRecBolusActive",
  1452. default: false).exists else { return }
  1453. var alarm = Alarm(type: .recBolus)
  1454. alarm.name = "Recommended Bolus"
  1455. alarm.isEnabled = take("alertRecBolusActive", false)
  1456. alarm.delta = take("alertRecBolusLevel", 1.0) // units
  1457. alarm.snoozeDuration = take("alertRecBolusSnooze", 5) // min
  1458. alarm.snoozedUntil = take("alertRecBolusSnoozedTime", nil as Date?)
  1459. alarm.soundFile = SoundFile(rawValue:
  1460. take("alertRecBolusSound", "Dhol_Shuffleloop"))
  1461. ?? .dholShuffleloop
  1462. let rpt = take("alertRecBolusRepeat", false)
  1463. alarm.playSoundOption = .always
  1464. alarm.repeatSoundOption = rpt ? .always : .never
  1465. alarm.activeOption = .always
  1466. Storage.shared.alarms.value.append(alarm)
  1467. }
  1468. // MARK: - Migration helpers (can be removed when step 5 is removed)
  1469. /// Check whether a tab item's position was explicitly stored in UserDefaults
  1470. /// (as opposed to using the StorageValue default).
  1471. private func migratePositionExists(for item: TabItem) -> Bool {
  1472. switch item {
  1473. case .home: return homePosition.exists
  1474. case .alarms: return alarmsPosition.exists
  1475. case .remote: return remotePosition.exists
  1476. case .nightscout: return nightscoutPosition.exists
  1477. case .snoozer: return snoozerPosition.exists
  1478. case .stats: return statisticsPosition.exists
  1479. case .treatments: return treatmentsPosition.exists
  1480. }
  1481. }
  1482. }