Storage.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. // LoopFollow
  2. // Storage.swift
  3. import Foundation
  4. import HealthKit
  5. import UIKit
  6. /*
  7. Observable persistant storage
  8. */
  9. class Storage {
  10. var remoteType = StorageValue<RemoteType>(key: "remoteType", defaultValue: .none)
  11. var deviceToken = StorageValue<String>(key: "deviceToken", defaultValue: "")
  12. var expirationDate = StorageValue<Date?>(key: "expirationDate", defaultValue: nil)
  13. var sharedSecret = StorageValue<String>(key: "sharedSecret", defaultValue: "")
  14. var productionEnvironment = StorageValue<Bool>(key: "productionEnvironment", defaultValue: false)
  15. var remoteApnsKey = StorageValue<String>(key: "remoteApnsKey", defaultValue: "")
  16. var teamId = StorageValue<String?>(key: "teamId", defaultValue: nil)
  17. var remoteKeyId = StorageValue<String>(key: "remoteKeyId", defaultValue: "")
  18. var lfApnsKey = StorageValue<String>(key: "lfApnsKey", defaultValue: "")
  19. var lfKeyId = StorageValue<String>(key: "lfKeyId", defaultValue: "")
  20. var bundleId = StorageValue<String>(key: "bundleId", defaultValue: "")
  21. var user = StorageValue<String>(key: "user", defaultValue: "")
  22. var maxBolus = SecureStorageValue<HKQuantity>(key: "maxBolus", defaultValue: HKQuantity(unit: .internationalUnit(), doubleValue: 1.0))
  23. var maxCarbs = SecureStorageValue<HKQuantity>(key: "maxCarbs", defaultValue: HKQuantity(unit: .gram(), doubleValue: 30.0))
  24. var maxProtein = SecureStorageValue<HKQuantity>(key: "maxProtein", defaultValue: HKQuantity(unit: .gram(), doubleValue: 30.0))
  25. var maxFat = SecureStorageValue<HKQuantity>(key: "maxFat", defaultValue: HKQuantity(unit: .gram(), doubleValue: 30.0))
  26. var mealWithBolus = StorageValue<Bool>(key: "mealWithBolus", defaultValue: false)
  27. var mealWithFatProtein = StorageValue<Bool>(key: "mealWithFatProtein", defaultValue: false)
  28. // TODO: This flag can be deleted in March 2027. Check the commit for other places to cleanup.
  29. var hasSeenFatProteinOrderChange = StorageValue<Bool>(key: "hasSeenFatProteinOrderChange", defaultValue: false)
  30. var backgroundRefreshType = StorageValue<BackgroundRefreshType>(key: "backgroundRefreshType", defaultValue: .silentTune)
  31. var selectedBLEDevice = StorageValue<BLEDevice?>(key: "selectedBLEDevice", defaultValue: nil)
  32. var debugLogLevel = StorageValue<Bool>(key: "debugLogLevel", defaultValue: false)
  33. var contactTrend = StorageValue<ContactIncludeOption>(key: "contactTrend", defaultValue: .off)
  34. var contactDelta = StorageValue<ContactIncludeOption>(key: "contactDelta", defaultValue: .off)
  35. var contactIOB = StorageValue<ContactIncludeOption>(key: "contactIOB", defaultValue: .off)
  36. var contactTrendTarget = StorageValue<ContactType>(key: "contactTrendTarget", defaultValue: .BG)
  37. var contactDeltaTarget = StorageValue<ContactType>(key: "contactDeltaTarget", defaultValue: .BG)
  38. var contactIOBTarget = StorageValue<ContactType>(key: "contactIOBTarget", defaultValue: .BG)
  39. var contactEnabled = StorageValue<Bool>(key: "contactEnabled", defaultValue: false)
  40. var contactBackgroundColor = StorageValue<String>(key: "contactBackgroundColor", defaultValue: ContactColorOption.black.rawValue)
  41. var contactTextColor = StorageValue<String>(key: "contactTextColor", defaultValue: ContactColorOption.white.rawValue)
  42. var contactColorMode = StorageValue<ContactColorMode>(key: "contactColorMode", defaultValue: .staticColor)
  43. var sensorScheduleOffset = StorageValue<Double?>(key: "sensorScheduleOffset", defaultValue: nil)
  44. var alarms = StorageValue<[Alarm]>(key: "alarms", defaultValue: [])
  45. var alarmConfiguration = StorageValue<AlarmConfiguration>(key: "alarmConfiguration", defaultValue: .default)
  46. var lastOverrideStartNotified = StorageValue<TimeInterval?>(key: "lastOverrideStartNotified", defaultValue: nil)
  47. var lastOverrideEndNotified = StorageValue<TimeInterval?>(key: "lastOverrideEndNotified", defaultValue: nil)
  48. var lastTempTargetStartNotified = StorageValue<TimeInterval?>(key: "lastTempTargetStartNotified", defaultValue: nil)
  49. var lastTempTargetEndNotified = StorageValue<TimeInterval?>(key: "lastTempTargetEndNotified", defaultValue: nil)
  50. var lastRecBolusNotified = StorageValue<Double?>(key: "lastRecBolusNotified", defaultValue: nil)
  51. var lastCOBNotified = StorageValue<Double?>(key: "lastCOBNotified", defaultValue: nil)
  52. var lastMissedBolusNotified = StorageValue<Date?>(key: "lastMissedBolusNotified", defaultValue: nil)
  53. var pendingFutureCarbs = StorageValue<[PendingFutureCarb]>(key: "pendingFutureCarbs", defaultValue: [])
  54. // General Settings [BEGIN]
  55. var appBadge = StorageValue<Bool>(key: "appBadge", defaultValue: true)
  56. var colorBGText = StorageValue<Bool>(key: "colorBGText", defaultValue: true)
  57. var appearanceMode = StorageValue<AppearanceMode>(key: "appearanceMode", defaultValue: .dark)
  58. var showStats = StorageValue<Bool>(key: "showStats", defaultValue: true)
  59. var useIFCC = StorageValue<Bool>(key: "useIFCC", defaultValue: false)
  60. var showSmallGraph = StorageValue<Bool>(key: "showSmallGraph", defaultValue: true)
  61. var screenlockSwitchState = StorageValue<Bool>(key: "screenlockSwitchState", defaultValue: true)
  62. var showDisplayName = StorageValue<Bool>(key: "showDisplayName", defaultValue: false)
  63. var snoozerEmoji = StorageValue<Bool>(key: "snoozerEmoji", defaultValue: false)
  64. var forcePortraitMode = StorageValue<Bool>(key: "forcePortraitMode", defaultValue: false)
  65. var speakBG = StorageValue<Bool>(key: "speakBG", defaultValue: false)
  66. var speakBGAlways = StorageValue<Bool>(key: "speakBGAlways", defaultValue: true)
  67. var speakLowBG = StorageValue<Bool>(key: "speakLowBG", defaultValue: false)
  68. var speakProactiveLowBG = StorageValue<Bool>(key: "speakProactiveLowBG", defaultValue: false)
  69. var speakFastDropDelta = StorageValue<Double>(key: "speakFastDropDelta", defaultValue: 10.0)
  70. var speakLowBGLimit = StorageValue<Double>(key: "speakLowBGLimit", defaultValue: 72.0)
  71. var speakHighBGLimit = StorageValue<Double>(key: "speakHighBGLimit", defaultValue: 180.0)
  72. var speakHighBG = StorageValue<Bool>(key: "speakHighBG", defaultValue: false)
  73. var speakLanguage = StorageValue<String>(key: "speakLanguage", defaultValue: "en")
  74. // General Settings [END]
  75. // Live Activity glucose state
  76. var lastBgReadingTimeSeconds = StorageValue<TimeInterval?>(key: "lastBgReadingTimeSeconds", defaultValue: nil)
  77. var lastDeltaMgdl = StorageValue<Double?>(key: "lastDeltaMgdl", defaultValue: nil)
  78. var lastTrendCode = StorageValue<String?>(key: "lastTrendCode", defaultValue: nil)
  79. var lastIOB = StorageValue<Double?>(key: "lastIOB", defaultValue: nil)
  80. var lastCOB = StorageValue<Double?>(key: "lastCOB", defaultValue: nil)
  81. var projectedBgMgdl = StorageValue<Double?>(key: "projectedBgMgdl", defaultValue: nil)
  82. // Live Activity extended InfoType data
  83. var lastBasal = StorageValue<String>(key: "lastBasal", defaultValue: "")
  84. var lastPumpReservoirU = StorageValue<Double?>(key: "lastPumpReservoirU", defaultValue: nil)
  85. var lastAutosens = StorageValue<Double?>(key: "lastAutosens", defaultValue: nil)
  86. var lastTdd = StorageValue<Double?>(key: "lastTdd", defaultValue: nil)
  87. var lastTargetLowMgdl = StorageValue<Double?>(key: "lastTargetLowMgdl", defaultValue: nil)
  88. var lastTargetHighMgdl = StorageValue<Double?>(key: "lastTargetHighMgdl", defaultValue: nil)
  89. var lastIsfMgdlPerU = StorageValue<Double?>(key: "lastIsfMgdlPerU", defaultValue: nil)
  90. var lastCarbRatio = StorageValue<Double?>(key: "lastCarbRatio", defaultValue: nil)
  91. var lastCarbsToday = StorageValue<Double?>(key: "lastCarbsToday", defaultValue: nil)
  92. var lastProfileName = StorageValue<String>(key: "lastProfileName", defaultValue: "")
  93. var iageInsertTime = StorageValue<TimeInterval>(key: "iageInsertTime", defaultValue: 0)
  94. var lastMinBgMgdl = StorageValue<Double?>(key: "lastMinBgMgdl", defaultValue: nil)
  95. var lastMaxBgMgdl = StorageValue<Double?>(key: "lastMaxBgMgdl", defaultValue: nil)
  96. // Live Activity
  97. var laEnabled = StorageValue<Bool>(key: "laEnabled", defaultValue: false)
  98. var laRenewBy = StorageValue<TimeInterval>(key: "laRenewBy", defaultValue: 0)
  99. var laRenewalFailed = StorageValue<Bool>(key: "laRenewalFailed", defaultValue: false)
  100. // Graph Settings [BEGIN]
  101. var showDots = StorageValue<Bool>(key: "showDots", defaultValue: true)
  102. var showLines = StorageValue<Bool>(key: "showLines", defaultValue: true)
  103. var showValues = StorageValue<Bool>(key: "showValues", defaultValue: true)
  104. var showAbsorption = StorageValue<Bool>(key: "showAbsorption", defaultValue: true)
  105. var showDIALines = StorageValue<Bool>(key: "showDIAMarkers", defaultValue: true)
  106. var show30MinLine = StorageValue<Bool>(key: "show30MinLine", defaultValue: false)
  107. var show90MinLine = StorageValue<Bool>(key: "show90MinLine", defaultValue: false)
  108. var showMidnightLines = StorageValue<Bool>(key: "showMidnightMarkers", defaultValue: false)
  109. var smallGraphTreatments = StorageValue<Bool>(key: "smallGraphTreatments", defaultValue: true)
  110. var smallGraphHeight = StorageValue<Int>(key: "smallGraphHeight", defaultValue: 40)
  111. var predictionToLoad = StorageValue<Double>(key: "predictionToLoad", defaultValue: 1.0)
  112. var minBasalScale = StorageValue<Double>(key: "minBasalScale", defaultValue: 5.0)
  113. var minBGScale = StorageValue<Double>(key: "minBGScale", defaultValue: 250.0)
  114. var lowLine = StorageValue<Double>(key: "lowLine", defaultValue: 70.0)
  115. var highLine = StorageValue<Double>(key: "highLine", defaultValue: 180.0)
  116. var downloadDays = StorageValue<Int>(key: "downloadDays", defaultValue: 1)
  117. var graphTimeZoneEnabled = StorageValue<Bool>(key: "graphTimeZoneEnabled", defaultValue: false)
  118. var graphTimeZoneIdentifier = StorageValue<String>(key: "graphTimeZoneIdentifier", defaultValue: TimeZone.current.identifier)
  119. // Graph Settings [END]
  120. // Calendar entries [BEGIN]
  121. var writeCalendarEvent = StorageValue<Bool>(key: "writeCalendarEvent", defaultValue: false)
  122. var calendarIdentifier = StorageValue<String>(key: "calendarIdentifier", defaultValue: "")
  123. var watchLine1 = StorageValue<String>(key: "watchLine1", defaultValue: "%BG% %DIRECTION% %DELTA% %MINAGO%")
  124. var watchLine2 = StorageValue<String>(key: "watchLine2", defaultValue: "C:%COB% I:%IOB% B:%BASAL%")
  125. // Calendar entries [END]
  126. // MARK: - Dexcom Share --------------------------------------------------------
  127. var shareUserName = StorageValue<String>(key: "shareUserName", defaultValue: "")
  128. var sharePassword = StorageValue<String>(key: "sharePassword", defaultValue: "")
  129. var shareServer = StorageValue<String>(key: "shareServer", defaultValue: "US")
  130. // MARK: - Graph ---------------------------------------------------------------
  131. var chartScaleX = StorageValue<Double>(key: "chartScaleX", defaultValue: 18.0)
  132. // MARK: - Advanced settings ---------------------------------------------------
  133. var downloadTreatments = StorageValue<Bool>(key: "downloadTreatments", defaultValue: true)
  134. var downloadPrediction = StorageValue<Bool>(key: "downloadPrediction", defaultValue: true)
  135. var graphOtherTreatments = StorageValue<Bool>(key: "graphOtherTreatments", defaultValue: true)
  136. var graphBasal = StorageValue<Bool>(key: "graphBasal", defaultValue: true)
  137. var graphBolus = StorageValue<Bool>(key: "graphBolus", defaultValue: true)
  138. var graphCarbs = StorageValue<Bool>(key: "graphCarbs", defaultValue: true)
  139. var bgUpdateDelay = StorageValue<Int>(key: "bgUpdateDelay", defaultValue: 10)
  140. // MARK: - Insert times (sensor / pump) ---------------------------------------
  141. var cageInsertTime = StorageValue<TimeInterval>(key: "cageInsertTime", defaultValue: 0)
  142. var sageInsertTime = StorageValue<TimeInterval>(key: "sageInsertTime", defaultValue: 0)
  143. // MARK: - Version-info ---------------------------
  144. var cachedForVersion = StorageValue<String?>(key: "cachedForVersion", defaultValue: nil)
  145. var latestVersion = StorageValue<String?>(key: "latestVersion", defaultValue: nil)
  146. var latestVersionChecked = StorageValue<Date?>(key: "latestVersionChecked", defaultValue: nil)
  147. var currentVersionBlackListed = StorageValue<Bool>(key: "currentVersionBlackListed", defaultValue: false)
  148. var lastBlacklistNotificationShown = StorageValue<Date?>(key: "lastBlacklistNotificationShown", defaultValue: nil)
  149. var lastVersionUpdateNotificationShown = StorageValue<Date?>(key: "lastVersionUpdateNotificationShown", defaultValue: nil)
  150. var lastExpirationNotificationShown = StorageValue<Date?>(key: "lastExpirationNotificationShown", defaultValue: nil)
  151. var hideInfoTable = StorageValue<Bool>(key: "hideInfoTable", defaultValue: false)
  152. var token = StorageValue<String>(key: "token", defaultValue: "")
  153. var units = StorageValue<String>(key: "units", defaultValue: "mg/dL")
  154. var infoSort = StorageValue<[Int]>(key: "infoSort", defaultValue: InfoType.allCases.map(\.sortOrder))
  155. var infoVisible = StorageValue<[Bool]>(key: "infoVisible", defaultValue: InfoType.allCases.map(\.defaultVisible))
  156. var url = StorageValue<String>(key: "url", defaultValue: "")
  157. var device = StorageValue<String>(key: "device", defaultValue: "")
  158. var nsWriteAuth = StorageValue<Bool>(key: "nsWriteAuth", defaultValue: false)
  159. var nsAdminAuth = StorageValue<Bool>(key: "nsAdminAuth", defaultValue: false)
  160. var migrationStep = StorageValue<Int>(key: "migrationStep", defaultValue: 0)
  161. var persistentNotification = StorageValue<Bool>(key: "persistentNotification", defaultValue: false)
  162. var persistentNotificationLastBGTime = StorageValue<Date>(key: "persistentNotificationLastBGTime", defaultValue: .distantPast)
  163. var lastLoopingChecked = StorageValue<Date?>(key: "lastLoopingChecked", defaultValue: nil)
  164. var lastBGChecked = StorageValue<Date?>(key: "lastBGChecked", defaultValue: nil)
  165. var lastLoopTime = StorageValue<TimeInterval>(key: "lastLoopTime", defaultValue: 0)
  166. // Tab positions - which position each item is in (positions 1-4 are customizable, 5 is always Menu)
  167. var homePosition = StorageValue<TabPosition>(key: "homePosition", defaultValue: .position1)
  168. var alarmsPosition = StorageValue<TabPosition>(key: "alarmsPosition", defaultValue: .position2)
  169. var snoozerPosition = StorageValue<TabPosition>(key: "snoozerPosition", defaultValue: .menu)
  170. var nightscoutPosition = StorageValue<TabPosition>(key: "nightscoutPosition", defaultValue: .position3)
  171. var remotePosition = StorageValue<TabPosition>(key: "remotePosition", defaultValue: .position4)
  172. var statisticsPosition = StorageValue<TabPosition>(key: "statisticsPosition", defaultValue: .menu)
  173. var treatmentsPosition = StorageValue<TabPosition>(key: "treatmentsPosition", defaultValue: .menu)
  174. var loopAPNSQrCodeURL = StorageValue<String>(key: "loopAPNSQrCodeURL", defaultValue: "")
  175. var bolusIncrement = SecureStorageValue<HKQuantity>(key: "bolusIncrement", defaultValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.05))
  176. var bolusIncrementDetected = StorageValue<Bool>(key: "bolusIncrementDetected", defaultValue: false)
  177. // Statistics display preferences
  178. var showGMI = StorageValue<Bool>(key: "showGMI", defaultValue: true)
  179. var showStdDev = StorageValue<Bool>(key: "showStdDev", defaultValue: true)
  180. var showTITR = StorageValue<Bool>(key: "showTITR", defaultValue: false)
  181. static let shared = Storage()
  182. private init() {}
  183. /// Set to true at launch if isProtectedDataAvailable was false (BFU state).
  184. /// Consumed and cleared on the first foreground after that launch.
  185. var needsBFUReload = false
  186. /// Re-reads every StorageValue from UserDefaults, firing @Published only where the value
  187. /// actually changed. Call this when foregrounding after a Before-First-Unlock (BFU) background
  188. /// launch, where Storage was initialized while UserDefaults was encrypted and all values were
  189. /// cached as their defaults.
  190. ///
  191. /// `migrationStep` is intentionally excluded: viewDidLoad writes it to 6 during the BFU
  192. /// launch; if we reloaded it and the flush had somehow not landed yet, migrations would re-run.
  193. ///
  194. /// SecureStorageValue properties (maxBolus, maxCarbs, maxProtein, maxFat, bolusIncrement) are
  195. /// not covered here — SecureStorageValue does not implement reload() and Keychain has the same
  196. /// BFU inaccessibility; that is a separate problem.
  197. func reloadAll() {
  198. remoteType.reload()
  199. deviceToken.reload()
  200. expirationDate.reload()
  201. sharedSecret.reload()
  202. productionEnvironment.reload()
  203. remoteApnsKey.reload()
  204. teamId.reload()
  205. remoteKeyId.reload()
  206. lfApnsKey.reload()
  207. lfKeyId.reload()
  208. bundleId.reload()
  209. user.reload()
  210. mealWithBolus.reload()
  211. mealWithFatProtein.reload()
  212. hasSeenFatProteinOrderChange.reload()
  213. backgroundRefreshType.reload()
  214. selectedBLEDevice.reload()
  215. debugLogLevel.reload()
  216. contactTrend.reload()
  217. contactDelta.reload()
  218. contactEnabled.reload()
  219. contactBackgroundColor.reload()
  220. contactTextColor.reload()
  221. sensorScheduleOffset.reload()
  222. alarms.reload()
  223. alarmConfiguration.reload()
  224. lastOverrideStartNotified.reload()
  225. lastOverrideEndNotified.reload()
  226. lastTempTargetStartNotified.reload()
  227. lastTempTargetEndNotified.reload()
  228. lastRecBolusNotified.reload()
  229. lastCOBNotified.reload()
  230. lastMissedBolusNotified.reload()
  231. appBadge.reload()
  232. colorBGText.reload()
  233. appearanceMode.reload()
  234. showStats.reload()
  235. useIFCC.reload()
  236. showSmallGraph.reload()
  237. screenlockSwitchState.reload()
  238. showDisplayName.reload()
  239. snoozerEmoji.reload()
  240. forcePortraitMode.reload()
  241. speakBG.reload()
  242. speakBGAlways.reload()
  243. speakLowBG.reload()
  244. speakProactiveLowBG.reload()
  245. speakFastDropDelta.reload()
  246. speakLowBGLimit.reload()
  247. speakHighBGLimit.reload()
  248. speakHighBG.reload()
  249. speakLanguage.reload()
  250. lastBgReadingTimeSeconds.reload()
  251. lastDeltaMgdl.reload()
  252. lastTrendCode.reload()
  253. lastIOB.reload()
  254. lastCOB.reload()
  255. projectedBgMgdl.reload()
  256. lastBasal.reload()
  257. lastPumpReservoirU.reload()
  258. lastAutosens.reload()
  259. lastTdd.reload()
  260. lastTargetLowMgdl.reload()
  261. lastTargetHighMgdl.reload()
  262. lastIsfMgdlPerU.reload()
  263. lastCarbRatio.reload()
  264. lastCarbsToday.reload()
  265. lastProfileName.reload()
  266. iageInsertTime.reload()
  267. lastMinBgMgdl.reload()
  268. lastMaxBgMgdl.reload()
  269. laEnabled.reload()
  270. laRenewBy.reload()
  271. laRenewalFailed.reload()
  272. showDots.reload()
  273. showLines.reload()
  274. showValues.reload()
  275. showAbsorption.reload()
  276. showDIALines.reload()
  277. show30MinLine.reload()
  278. show90MinLine.reload()
  279. showMidnightLines.reload()
  280. smallGraphTreatments.reload()
  281. smallGraphHeight.reload()
  282. predictionToLoad.reload()
  283. minBasalScale.reload()
  284. minBGScale.reload()
  285. lowLine.reload()
  286. highLine.reload()
  287. downloadDays.reload()
  288. graphTimeZoneEnabled.reload()
  289. graphTimeZoneIdentifier.reload()
  290. writeCalendarEvent.reload()
  291. calendarIdentifier.reload()
  292. watchLine1.reload()
  293. watchLine2.reload()
  294. shareUserName.reload()
  295. sharePassword.reload()
  296. shareServer.reload()
  297. chartScaleX.reload()
  298. downloadTreatments.reload()
  299. downloadPrediction.reload()
  300. graphOtherTreatments.reload()
  301. graphBasal.reload()
  302. graphBolus.reload()
  303. graphCarbs.reload()
  304. bgUpdateDelay.reload()
  305. cageInsertTime.reload()
  306. sageInsertTime.reload()
  307. cachedForVersion.reload()
  308. latestVersion.reload()
  309. latestVersionChecked.reload()
  310. currentVersionBlackListed.reload()
  311. lastBlacklistNotificationShown.reload()
  312. lastVersionUpdateNotificationShown.reload()
  313. lastExpirationNotificationShown.reload()
  314. hideInfoTable.reload()
  315. token.reload()
  316. units.reload()
  317. infoSort.reload()
  318. infoVisible.reload()
  319. url.reload()
  320. device.reload()
  321. nsWriteAuth.reload()
  322. nsAdminAuth.reload()
  323. // migrationStep intentionally excluded — see method comment above.
  324. persistentNotification.reload()
  325. persistentNotificationLastBGTime.reload()
  326. lastLoopingChecked.reload()
  327. lastBGChecked.reload()
  328. lastLoopTime.reload()
  329. homePosition.reload()
  330. alarmsPosition.reload()
  331. snoozerPosition.reload()
  332. nightscoutPosition.reload()
  333. remotePosition.reload()
  334. statisticsPosition.reload()
  335. treatmentsPosition.reload()
  336. loopAPNSQrCodeURL.reload()
  337. bolusIncrementDetected.reload()
  338. showGMI.reload()
  339. showStdDev.reload()
  340. showTITR.reload()
  341. }
  342. // MARK: - Tab Position Helpers
  343. /// Get the position for a given tab item
  344. func position(for item: TabItem) -> TabPosition {
  345. switch item {
  346. case .home: homePosition.value
  347. case .alarms: alarmsPosition.value
  348. case .remote: remotePosition.value
  349. case .nightscout: nightscoutPosition.value
  350. case .snoozer: snoozerPosition.value
  351. case .stats: statisticsPosition.value
  352. case .treatments: treatmentsPosition.value
  353. }
  354. }
  355. /// Set the position for a given tab item
  356. func setPosition(_ position: TabPosition, for item: TabItem) {
  357. switch item {
  358. case .home: homePosition.value = position
  359. case .alarms: alarmsPosition.value = position
  360. case .remote: remotePosition.value = position
  361. case .nightscout: nightscoutPosition.value = position
  362. case .snoozer: snoozerPosition.value = position
  363. case .stats: statisticsPosition.value = position
  364. case .treatments: treatmentsPosition.value = position
  365. }
  366. }
  367. /// Get the tab item at a specific position (nil if no item at that position)
  368. func tabItem(at position: TabPosition) -> TabItem? {
  369. for item in TabItem.allCases {
  370. // Use normalized comparison to handle legacy values (.more, .disabled -> .menu)
  371. if self.position(for: item).normalized == position.normalized {
  372. return item
  373. }
  374. }
  375. return nil
  376. }
  377. /// Get all items in the Menu (position 5)
  378. func itemsInMenu() -> [TabItem] {
  379. TabItem.featureOrder.filter { position(for: $0).normalized == .menu }
  380. }
  381. /// Get items ordered by their position in the tab bar (positions 1-4)
  382. func orderedTabBarItems() -> [TabItem] {
  383. TabPosition.customizablePositions.compactMap { tabItem(at: $0) }
  384. }
  385. }