| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- // LoopFollow
- // LAAppGroupSettings.swift
- import Foundation
- // MARK: - Slot option enum
- /// One displayable metric that can occupy a slot in the Live Activity 2×2 grid.
- ///
- /// - `.none` is the empty/blank state — leaves the slot visually empty.
- /// - Optional cases (isOptional == true) may display "—" for Dexcom-only users
- /// whose setup does not provide that metric.
- /// - All values are read from GlucoseSnapshot at render time inside the widget
- /// extension; no additional App Group reads are required per slot.
- enum LiveActivitySlotOption: String, CaseIterable, Codable {
- // Core glucose
- case none
- case delta
- case projectedBG
- case minMax
- // Loop metrics
- case iob
- case cob
- case recBolus
- case autosens
- case tdd
- // Pump / device
- case basal
- case pump
- case pumpBattery
- case battery
- case target
- case isf
- case carbRatio
- // Ages
- case sage
- case cage
- case iage
- // Other
- case carbsToday
- case override
- case profile
- /// Human-readable label shown in the slot picker in Settings.
- var displayName: String {
- switch self {
- case .none: "Empty"
- case .delta: "Delta"
- case .projectedBG: "Projected BG"
- case .minMax: "Min/Max"
- case .iob: "IOB"
- case .cob: "COB"
- case .recBolus: "Rec. Bolus"
- case .autosens: "Autosens"
- case .tdd: "TDD"
- case .basal: "Basal"
- case .pump: "Pump"
- case .pumpBattery: "Pump Battery"
- case .battery: "Battery"
- case .target: "Target"
- case .isf: "ISF"
- case .carbRatio: "CR"
- case .sage: "SAGE"
- case .cage: "CAGE"
- case .iage: "IAGE"
- case .carbsToday: "Carbs today"
- case .override: "Override"
- case .profile: "Profile"
- }
- }
- /// Short label used inside the MetricBlock on the Live Activity card.
- var gridLabel: String {
- switch self {
- case .none: ""
- case .delta: "Delta"
- case .projectedBG: "Proj"
- case .minMax: "Min/Max"
- case .iob: "IOB"
- case .cob: "COB"
- case .recBolus: "Rec."
- case .autosens: "Sens"
- case .tdd: "TDD"
- case .basal: "Basal"
- case .pump: "Pump"
- case .pumpBattery: "Pump%"
- case .battery: "Bat."
- case .target: "Target"
- case .isf: "ISF"
- case .carbRatio: "CR"
- case .sage: "SAGE"
- case .cage: "CAGE"
- case .iage: "IAGE"
- case .carbsToday: "Carbs"
- case .override: "Ovrd"
- case .profile: "Prof"
- }
- }
- /// True when the value is a glucose measurement and should be followed by
- /// the user's preferred unit label (mg/dL or mmol/L) in compact displays.
- var isGlucoseUnit: Bool {
- switch self {
- case .projectedBG, .delta, .minMax, .target, .isf: return true
- default: return false
- }
- }
- /// True when the underlying value may be nil (e.g. Dexcom-only users who have
- /// no Loop data). The widget renders "—" in those cases.
- var isOptional: Bool {
- switch self {
- case .none, .delta: false
- default: true
- }
- }
- }
- // MARK: - Default slot assignments
- enum LiveActivitySlotDefaults {
- /// Top-left slot
- static let slot1: LiveActivitySlotOption = .iob
- /// Bottom-left slot
- static let slot2: LiveActivitySlotOption = .cob
- /// Top-right slot
- static let slot3: LiveActivitySlotOption = .projectedBG
- /// Bottom-right slot — intentionally empty until the user configures it
- static let slot4: LiveActivitySlotOption = .none
- /// Small widget (CarPlay / Watch Smart Stack) right slot
- static let smallWidgetSlot: LiveActivitySlotOption = .projectedBG
- static var all: [LiveActivitySlotOption] {
- [slot1, slot2, slot3, slot4]
- }
- }
- // MARK: - App Group settings
- /// Minimal App Group settings needed by the Live Activity UI.
- ///
- /// We keep this separate from Storage.shared to avoid target-coupling and
- /// ensure the widget extension reads the same values as the app.
- enum LAAppGroupSettings {
- private enum Keys {
- static let lowLineMgdl = "la.lowLine.mgdl"
- static let highLineMgdl = "la.highLine.mgdl"
- static let slots = "la.slots"
- static let smallWidgetSlot = "la.smallWidgetSlot"
- static let displayName = "la.displayName"
- static let showDisplayName = "la.showDisplayName"
- }
- private static var defaults: UserDefaults? {
- UserDefaults(suiteName: AppGroupID.current())
- }
- // MARK: - Thresholds (Write)
- static func setThresholds(lowMgdl: Double, highMgdl: Double) {
- defaults?.set(lowMgdl, forKey: Keys.lowLineMgdl)
- defaults?.set(highMgdl, forKey: Keys.highLineMgdl)
- }
- // MARK: - Thresholds (Read)
- static func thresholdsMgdl(fallbackLow: Double = 70, fallbackHigh: Double = 180) -> (low: Double, high: Double) {
- let low = defaults?.object(forKey: Keys.lowLineMgdl) as? Double ?? fallbackLow
- let high = defaults?.object(forKey: Keys.highLineMgdl) as? Double ?? fallbackHigh
- return (low, high)
- }
- // MARK: - Slot configuration (Write)
- /// Persists a 4-slot configuration to the App Group container.
- /// - Parameter slots: Array of exactly 4 `LiveActivitySlotOption` values;
- /// extra elements are ignored, missing elements are filled with `.none`.
- static func setSlots(_ slots: [LiveActivitySlotOption]) {
- let raw = slots.prefix(4).map(\.rawValue)
- defaults?.set(raw, forKey: Keys.slots)
- }
- // MARK: - Slot configuration (Read)
- /// Returns the current 4-slot configuration, falling back to defaults
- /// if no configuration has been saved yet.
- static func slots() -> [LiveActivitySlotOption] {
- guard let raw = defaults?.stringArray(forKey: Keys.slots), raw.count == 4 else {
- return LiveActivitySlotDefaults.all
- }
- return raw.map { LiveActivitySlotOption(rawValue: $0) ?? .none }
- }
- // MARK: - Small widget slot (Write)
- static func setSmallWidgetSlot(_ slot: LiveActivitySlotOption) {
- defaults?.set(slot.rawValue, forKey: Keys.smallWidgetSlot)
- }
- // MARK: - Small widget slot (Read)
- static func smallWidgetSlot() -> LiveActivitySlotOption {
- guard let raw = defaults?.string(forKey: Keys.smallWidgetSlot) else {
- return LiveActivitySlotDefaults.smallWidgetSlot
- }
- return LiveActivitySlotOption(rawValue: raw) ?? LiveActivitySlotDefaults.smallWidgetSlot
- }
- // MARK: - Display Name
- static func setDisplayName(_ name: String, show: Bool) {
- defaults?.set(name, forKey: Keys.displayName)
- defaults?.set(show, forKey: Keys.showDisplayName)
- }
- static func displayName() -> String {
- defaults?.string(forKey: Keys.displayName) ?? "LoopFollow"
- }
- static func showDisplayName() -> Bool {
- defaults?.bool(forKey: Keys.showDisplayName) ?? false
- }
- }
|