// LoopFollow // Storage.swift import Foundation import HealthKit import UIKit /* Observable persistant storage */ class Storage { var remoteType = StorageValue(key: "remoteType", defaultValue: .none) var deviceToken = StorageValue(key: "deviceToken", defaultValue: "") var expirationDate = StorageValue(key: "expirationDate", defaultValue: nil) var sharedSecret = StorageValue(key: "sharedSecret", defaultValue: "") var productionEnvironment = StorageValue(key: "productionEnvironment", defaultValue: false) var apnsKey = StorageValue(key: "apnsKey", defaultValue: "") var teamId = StorageValue(key: "teamId", defaultValue: nil) var keyId = StorageValue(key: "keyId", defaultValue: "") var bundleId = StorageValue(key: "bundleId", defaultValue: "") var user = StorageValue(key: "user", defaultValue: "") var maxBolus = SecureStorageValue(key: "maxBolus", defaultValue: HKQuantity(unit: .internationalUnit(), doubleValue: 1.0)) var maxCarbs = SecureStorageValue(key: "maxCarbs", defaultValue: HKQuantity(unit: .gram(), doubleValue: 30.0)) var maxProtein = SecureStorageValue(key: "maxProtein", defaultValue: HKQuantity(unit: .gram(), doubleValue: 30.0)) var maxFat = SecureStorageValue(key: "maxFat", defaultValue: HKQuantity(unit: .gram(), doubleValue: 30.0)) var mealWithBolus = StorageValue(key: "mealWithBolus", defaultValue: false) var mealWithFatProtein = StorageValue(key: "mealWithFatProtein", defaultValue: false) // TODO: This flag can be deleted in March 2027. Check the commit for other places to cleanup. var hasSeenFatProteinOrderChange = StorageValue(key: "hasSeenFatProteinOrderChange", defaultValue: false) var cachedJWT = StorageValue(key: "cachedJWT", defaultValue: nil) var jwtExpirationDate = StorageValue(key: "jwtExpirationDate", defaultValue: nil) var backgroundRefreshType = StorageValue(key: "backgroundRefreshType", defaultValue: .silentTune) var selectedBLEDevice = StorageValue(key: "selectedBLEDevice", defaultValue: nil) var debugLogLevel = StorageValue(key: "debugLogLevel", defaultValue: false) var contactTrend = StorageValue(key: "contactTrend", defaultValue: .off) var contactDelta = StorageValue(key: "contactDelta", defaultValue: .off) var contactEnabled = StorageValue(key: "contactEnabled", defaultValue: false) var contactBackgroundColor = StorageValue(key: "contactBackgroundColor", defaultValue: ContactColorOption.black.rawValue) var contactTextColor = StorageValue(key: "contactTextColor", defaultValue: ContactColorOption.white.rawValue) var sensorScheduleOffset = StorageValue(key: "sensorScheduleOffset", defaultValue: nil) var alarms = StorageValue<[Alarm]>(key: "alarms", defaultValue: []) var alarmConfiguration = StorageValue(key: "alarmConfiguration", defaultValue: .default) var lastOverrideStartNotified = StorageValue(key: "lastOverrideStartNotified", defaultValue: nil) var lastOverrideEndNotified = StorageValue(key: "lastOverrideEndNotified", defaultValue: nil) var lastTempTargetStartNotified = StorageValue(key: "lastTempTargetStartNotified", defaultValue: nil) var lastTempTargetEndNotified = StorageValue(key: "lastTempTargetEndNotified", defaultValue: nil) var lastRecBolusNotified = StorageValue(key: "lastRecBolusNotified", defaultValue: nil) var lastCOBNotified = StorageValue(key: "lastCOBNotified", defaultValue: nil) var lastMissedBolusNotified = StorageValue(key: "lastMissedBolusNotified", defaultValue: nil) // General Settings [BEGIN] var appBadge = StorageValue(key: "appBadge", defaultValue: true) var colorBGText = StorageValue(key: "colorBGText", defaultValue: true) var appearanceMode = StorageValue(key: "appearanceMode", defaultValue: .dark) var showStats = StorageValue(key: "showStats", defaultValue: true) var useIFCC = StorageValue(key: "useIFCC", defaultValue: false) var showSmallGraph = StorageValue(key: "showSmallGraph", defaultValue: true) var screenlockSwitchState = StorageValue(key: "screenlockSwitchState", defaultValue: true) var showDisplayName = StorageValue(key: "showDisplayName", defaultValue: false) var snoozerEmoji = StorageValue(key: "snoozerEmoji", defaultValue: false) var forcePortraitMode = StorageValue(key: "forcePortraitMode", defaultValue: false) var speakBG = StorageValue(key: "speakBG", defaultValue: false) var speakBGAlways = StorageValue(key: "speakBGAlways", defaultValue: true) var speakLowBG = StorageValue(key: "speakLowBG", defaultValue: false) var speakProactiveLowBG = StorageValue(key: "speakProactiveLowBG", defaultValue: false) var speakFastDropDelta = StorageValue(key: "speakFastDropDelta", defaultValue: 10.0) var speakLowBGLimit = StorageValue(key: "speakLowBGLimit", defaultValue: 72.0) var speakHighBGLimit = StorageValue(key: "speakHighBGLimit", defaultValue: 180.0) var speakHighBG = StorageValue(key: "speakHighBG", defaultValue: false) var speakLanguage = StorageValue(key: "speakLanguage", defaultValue: "en") // General Settings [END] // Graph Settings [BEGIN] var showDots = StorageValue(key: "showDots", defaultValue: true) var showLines = StorageValue(key: "showLines", defaultValue: true) var showValues = StorageValue(key: "showValues", defaultValue: true) var showAbsorption = StorageValue(key: "showAbsorption", defaultValue: true) var showDIALines = StorageValue(key: "showDIAMarkers", defaultValue: true) var show30MinLine = StorageValue(key: "show30MinLine", defaultValue: false) var show90MinLine = StorageValue(key: "show90MinLine", defaultValue: false) var showMidnightLines = StorageValue(key: "showMidnightMarkers", defaultValue: false) var smallGraphTreatments = StorageValue(key: "smallGraphTreatments", defaultValue: true) var smallGraphHeight = StorageValue(key: "smallGraphHeight", defaultValue: 40) var predictionToLoad = StorageValue(key: "predictionToLoad", defaultValue: 1.0) var minBasalScale = StorageValue(key: "minBasalScale", defaultValue: 5.0) var minBGScale = StorageValue(key: "minBGScale", defaultValue: 250.0) var lowLine = StorageValue(key: "lowLine", defaultValue: 70.0) var highLine = StorageValue(key: "highLine", defaultValue: 180.0) var downloadDays = StorageValue(key: "downloadDays", defaultValue: 1) var graphTimeZoneEnabled = StorageValue(key: "graphTimeZoneEnabled", defaultValue: false) var graphTimeZoneIdentifier = StorageValue(key: "graphTimeZoneIdentifier", defaultValue: TimeZone.current.identifier) // Graph Settings [END] // Calendar entries [BEGIN] var writeCalendarEvent = StorageValue(key: "writeCalendarEvent", defaultValue: false) var calendarIdentifier = StorageValue(key: "calendarIdentifier", defaultValue: "") var watchLine1 = StorageValue(key: "watchLine1", defaultValue: "%BG% %DIRECTION% %DELTA% %MINAGO%") var watchLine2 = StorageValue(key: "watchLine2", defaultValue: "C:%COB% I:%IOB% B:%BASAL%") // Calendar entries [END] // MARK: - Dexcom Share -------------------------------------------------------- var shareUserName = StorageValue(key: "shareUserName", defaultValue: "") var sharePassword = StorageValue(key: "sharePassword", defaultValue: "") var shareServer = StorageValue(key: "shareServer", defaultValue: "US") // MARK: - Graph --------------------------------------------------------------- var chartScaleX = StorageValue(key: "chartScaleX", defaultValue: 18.0) // MARK: - Advanced settings --------------------------------------------------- var downloadTreatments = StorageValue(key: "downloadTreatments", defaultValue: true) var downloadPrediction = StorageValue(key: "downloadPrediction", defaultValue: true) var graphOtherTreatments = StorageValue(key: "graphOtherTreatments", defaultValue: true) var graphBasal = StorageValue(key: "graphBasal", defaultValue: true) var graphBolus = StorageValue(key: "graphBolus", defaultValue: true) var graphCarbs = StorageValue(key: "graphCarbs", defaultValue: true) var bgUpdateDelay = StorageValue(key: "bgUpdateDelay", defaultValue: 10) // MARK: - Insert times (sensor / pump) --------------------------------------- var cageInsertTime = StorageValue(key: "cageInsertTime", defaultValue: 0) var sageInsertTime = StorageValue(key: "sageInsertTime", defaultValue: 0) // MARK: - Version-info --------------------------- var cachedForVersion = StorageValue(key: "cachedForVersion", defaultValue: nil) var latestVersion = StorageValue(key: "latestVersion", defaultValue: nil) var latestVersionChecked = StorageValue(key: "latestVersionChecked", defaultValue: nil) var currentVersionBlackListed = StorageValue(key: "currentVersionBlackListed", defaultValue: false) var lastBlacklistNotificationShown = StorageValue(key: "lastBlacklistNotificationShown", defaultValue: nil) var lastVersionUpdateNotificationShown = StorageValue(key: "lastVersionUpdateNotificationShown", defaultValue: nil) var lastExpirationNotificationShown = StorageValue(key: "lastExpirationNotificationShown", defaultValue: nil) var hideInfoTable = StorageValue(key: "hideInfoTable", defaultValue: false) var token = StorageValue(key: "token", defaultValue: "") var units = StorageValue(key: "units", defaultValue: "mg/dL") var infoSort = StorageValue<[Int]>(key: "infoSort", defaultValue: InfoType.allCases.map { $0.sortOrder }) var infoVisible = StorageValue<[Bool]>(key: "infoVisible", defaultValue: InfoType.allCases.map { $0.defaultVisible }) var url = StorageValue(key: "url", defaultValue: "") var device = StorageValue(key: "device", defaultValue: "") var nsWriteAuth = StorageValue(key: "nsWriteAuth", defaultValue: false) var nsAdminAuth = StorageValue(key: "nsAdminAuth", defaultValue: false) var migrationStep = StorageValue(key: "migrationStep", defaultValue: 0) var persistentNotification = StorageValue(key: "persistentNotification", defaultValue: false) var persistentNotificationLastBGTime = StorageValue(key: "persistentNotificationLastBGTime", defaultValue: .distantPast) var lastLoopingChecked = StorageValue(key: "lastLoopingChecked", defaultValue: nil) var lastBGChecked = StorageValue(key: "lastBGChecked", defaultValue: nil) // Tab positions - which position each item is in (positions 1-4 are customizable, 5 is always Menu) var homePosition = StorageValue(key: "homePosition", defaultValue: .position1) var alarmsPosition = StorageValue(key: "alarmsPosition", defaultValue: .position2) var snoozerPosition = StorageValue(key: "snoozerPosition", defaultValue: .menu) var nightscoutPosition = StorageValue(key: "nightscoutPosition", defaultValue: .position3) var remotePosition = StorageValue(key: "remotePosition", defaultValue: .position4) var statisticsPosition = StorageValue(key: "statisticsPosition", defaultValue: .menu) var treatmentsPosition = StorageValue(key: "treatmentsPosition", defaultValue: .menu) var loopAPNSQrCodeURL = StorageValue(key: "loopAPNSQrCodeURL", defaultValue: "") var returnApnsKey = StorageValue(key: "returnApnsKey", defaultValue: "") var returnKeyId = StorageValue(key: "returnKeyId", defaultValue: "") var bolusIncrement = SecureStorageValue(key: "bolusIncrement", defaultValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.05)) var bolusIncrementDetected = StorageValue(key: "bolusIncrementDetected", defaultValue: false) // Statistics display preferences var showGMI = StorageValue(key: "showGMI", defaultValue: true) var showStdDev = StorageValue(key: "showStdDev", defaultValue: true) var showTITR = StorageValue(key: "showTITR", defaultValue: false) static let shared = Storage() private init() {} // MARK: - Tab Position Helpers /// Get the position for a given tab item func position(for item: TabItem) -> TabPosition { switch item { case .home: return homePosition.value case .alarms: return alarmsPosition.value case .remote: return remotePosition.value case .nightscout: return nightscoutPosition.value case .snoozer: return snoozerPosition.value case .stats: return statisticsPosition.value case .treatments: return treatmentsPosition.value } } /// Set the position for a given tab item func setPosition(_ position: TabPosition, for item: TabItem) { switch item { case .home: homePosition.value = position case .alarms: alarmsPosition.value = position case .remote: remotePosition.value = position case .nightscout: nightscoutPosition.value = position case .snoozer: snoozerPosition.value = position case .stats: statisticsPosition.value = position case .treatments: treatmentsPosition.value = position } } /// Get the tab item at a specific position (nil if no item at that position) func tabItem(at position: TabPosition) -> TabItem? { for item in TabItem.allCases { // Use normalized comparison to handle legacy values (.more, .disabled -> .menu) if self.position(for: item).normalized == position.normalized { return item } } return nil } /// Get all items in the Menu (position 5) func itemsInMenu() -> [TabItem] { TabItem.featureOrder.filter { position(for: $0).normalized == .menu } } /// Get items ordered by their position in the tab bar (positions 1-4) func orderedTabBarItems() -> [TabItem] { TabPosition.customizablePositions.compactMap { tabItem(at: $0) } } }