Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ extension AlarmCondition {
func passesBGLimits(alarm: Alarm, data: AlarmData) -> Bool {
let bgReading = data.bgReadings.last?.sgv
let haveBG = (bgReading ?? 0) > 0
let bgValue = Double(bgReading ?? 0)
let normalize = UnitSettingsStore.shared.normalizeToDisplayPrecision
let bgValue = normalize(Double(bgReading ?? 0))

// ────────────────────────────────────
// 1. BG-based alarms always need data
Expand All @@ -46,7 +47,7 @@ extension AlarmCondition {
// BG-based alarms already bailed out above.
guard haveBG else { return false }

switch (alarm.belowBG, alarm.aboveBG) {
switch (alarm.belowBG.map(normalize), alarm.aboveBG.map(normalize)) {
case let (lo?, hi?):
return lo < hi ? (bgValue <= lo || bgValue >= hi) // fire outside band
: (hi <= bgValue && bgValue <= lo) // fire inside band
Expand Down
5 changes: 4 additions & 1 deletion LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ struct HighBGCondition: AlarmCondition {
// ────────────────────────────────
guard let high = alarm.aboveBG else { return false }

let normalize = UnitSettingsStore.shared.normalizeToDisplayPrecision
let highLimit = normalize(high)

func isHigh(_ g: GlucoseValue) -> Bool {
g.sgv > 0 && Double(g.sgv) >= high
g.sgv > 0 && normalize(Double(g.sgv)) >= highLimit
}

// we already know from `passesBGLimits` that the **latest** reading is ≥ high,
Expand Down
5 changes: 4 additions & 1 deletion LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ struct LowBGCondition: AlarmCondition {
guard let belowBG = alarm.belowBG else { return false }
guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false }

let normalize = UnitSettingsStore.shared.normalizeToDisplayPrecision
let lowLimit = normalize(belowBG)

func isLow(_ g: GlucoseValue) -> Bool {
g.sgv > 0 && Double(g.sgv) <= belowBG
g.sgv > 0 && normalize(Double(g.sgv)) <= lowLimit
}

// ────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ struct MissedBolusCondition: AlarmCondition {
guard carb.grams > minCarbGr else { return false }

// – ignore if BG is low
let normalize = UnitSettingsStore.shared.normalizeToDisplayPrecision
if let latestBG = data.bgReadings.last,
Double(latestBG.sgv) <= minBG { return false }
normalize(Double(latestBG.sgv)) <= normalize(minBG)
{
return false
}

// ────────────────────────────────
// 2. already alerted for this carb?
Expand Down
10 changes: 4 additions & 6 deletions LoopFollow/Charts/BGChartModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,10 @@ final class BGChartModel: ObservableObject {
}

private func colorFor(_ sgv: Int, thresholds: (low: Double, high: Double)) -> Color {
if Double(sgv) >= thresholds.high {
return .yellow
} else if Double(sgv) <= thresholds.low {
return .red
} else {
return .green
switch UnitSettingsStore.shared.glucoseBand(for: Double(sgv), thresholds: thresholds) {
case .high: return .yellow
case .low: return .red
case .inRange: return .green
}
}

Expand Down
15 changes: 5 additions & 10 deletions LoopFollow/Contact/ContactColorMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,16 @@ enum ContactColorMode: String, Codable, CaseIterable {
}
}

/// Returns the appropriate text color based on the mode and BG value
/// Returns the appropriate text color based on the mode and BG value (in mg/dL).
func textColor(for bgValue: Double, staticColor: UIColor) -> UIColor {
switch self {
case .staticColor:
return staticColor
case .dynamic:
let highLine = Storage.shared.highLine.value
let lowLine = Storage.shared.lowLine.value

if bgValue >= highLine {
return .systemYellow
} else if bgValue <= lowLine {
return .systemRed
} else {
return .systemGreen
switch UnitSettingsStore.shared.glucoseBand(for: bgValue) {
case .high: return .systemYellow
case .low: return .systemRed
case .inRange: return .systemGreen
}
}
}
Expand Down
12 changes: 4 additions & 8 deletions LoopFollow/Controllers/Stats.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,13 @@ class StatsData {
coefficientOfVariation = 0.0

let thresholds = UnitSettingsStore.shared.effectiveThresholds()
let lowThreshold = thresholds.low
let highThreshold = thresholds.high

for i in 0 ..< bgData.count {
// Set low/range/high counts for pie chart and %'s
if Double(bgData[i].sgv) < lowThreshold {
countLow += 1
} else if Double(bgData[i].sgv) > highThreshold {
countHigh += 1
} else {
countRange += 1
switch UnitSettingsStore.shared.glucoseBand(for: Double(bgData[i].sgv), thresholds: thresholds) {
case .low: countLow += 1
case .high: countHigh += 1
case .inRange: countRange += 1
}

// set total bg for average
Expand Down
17 changes: 17 additions & 0 deletions LoopFollow/Helpers/Localizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ final class UnitSettingsStore {
}
}

/// Rounds a mg/dL value to the display precision of the current unit, so colors,
/// stats and alarms match the number shown. Apply to both the reading and the threshold.
func normalizeToDisplayPrecision(_ mgdl: Double) -> Double {
switch glucoseUnit {
case .mgdL:
return mgdl
case .mmolL:
return GlucoseConversion.toDisplayedMmol(mgdl)
}
}

/// Band of a mg/dL reading against the selected range (or `thresholds` when given).
func glucoseBand(for mgdl: Double, thresholds: (low: Double, high: Double)? = nil) -> GlucoseBand {
let range = thresholds ?? effectiveThresholds()
return GlucoseBand.of(mgdl, low: range.low, high: range.high, mmol: glucoseUnit == .mmolL)
}

var glycemicMetricMode: GlycemicMetricMode {
get {
Storage.shared.showGMI.value ? .gmi : .ehba1c
Expand Down
7 changes: 4 additions & 3 deletions LoopFollow/Helpers/Views/BGPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ struct BGPicker: View {
private var allValues: [Double] {
if unit == .millimolesPerLiter {
let step = 0.1
let lower = ceil((range.lowerBound / 18) / step) * step
let upper = floor((range.upperBound / 18) / step) * step
return stride(from: lower, through: upper, by: step).map { $0 * 18 }
let factor = GlucoseConversion.mmolToMgDl
let lower = ceil((range.lowerBound / factor) / step) * step
let upper = floor((range.upperBound / factor) / step) * step
return stride(from: lower, through: upper, by: step).map { $0 * factor }
} else {
return Array(stride(from: range.lowerBound,
through: range.upperBound,
Expand Down
36 changes: 23 additions & 13 deletions LoopFollow/LiveActivity/LiveActivityManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#if !targetEnvironment(macCatalyst)

@preconcurrency import ActivityKit
import Combine
import Foundation
import os
import UIKit
Expand Down Expand Up @@ -42,6 +43,16 @@ final class LiveActivityManager {
)
startPushToStartTokenObservation()
startActivityUpdatesObservation()

Publishers.Merge(
Storage.shared.lowLine.$value.map { _ in () },
Storage.shared.highLine.$value.map { _ in () }
)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.refreshFromCurrentState(reason: "thresholds changed")
}
.store(in: &cancellables)
}

// MARK: - Push-to-start observation
Expand Down Expand Up @@ -182,10 +193,7 @@ final class LiveActivityManager {
let provider = StorageCurrentGlucoseStateProvider()
guard let snapshot = GlucoseSnapshotBuilder.build(from: provider) else { return }

LAAppGroupSettings.setThresholds(
lowMgdl: Storage.shared.lowLine.value,
highMgdl: Storage.shared.highLine.value,
)
writeThresholds()
GlucoseSnapshotStore.shared.save(snapshot)

seq += 1
Expand Down Expand Up @@ -430,6 +438,7 @@ final class LiveActivityManager {
/// and arrives within a few seconds of the first request.
private static let pushToStartTokenRetryDelay: TimeInterval = 10

private var cancellables = Set<AnyCancellable>()
private(set) var current: Activity<GlucoseLiveActivityAttributes>?
private var stateObserverTask: Task<Void, Never>?
private var updateTask: Task<Void, Never>?
Expand Down Expand Up @@ -624,10 +633,7 @@ final class LiveActivityManager {

let provider = StorageCurrentGlucoseStateProvider()
if let snapshot = GlucoseSnapshotBuilder.build(from: provider) {
LAAppGroupSettings.setThresholds(
lowMgdl: Storage.shared.lowLine.value,
highMgdl: Storage.shared.highLine.value,
)
writeThresholds()
LAAppGroupSettings.setDisplayName(
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "LoopFollow",
show: Storage.shared.showDisplayName.value
Expand Down Expand Up @@ -904,6 +910,12 @@ final class LiveActivityManager {
}
}

/// Publishes the range for the selected time-in-range mode, matching the chart and BG header.
private func writeThresholds() {
let thresholds = UnitSettingsStore.shared.effectiveThresholds()
LAAppGroupSettings.setThresholds(lowMgdl: thresholds.low, highMgdl: thresholds.high)
}

private func performRefresh(reason: String) {
let provider = StorageCurrentGlucoseStateProvider()
guard let snapshot = GlucoseSnapshotBuilder.build(from: provider) else {
Expand All @@ -928,12 +940,10 @@ final class LiveActivityManager {
let forceRefreshNeeded = timeSinceLastUpdate >= 5 * 60
// Capture dedup result BEFORE saving so the store comparison is valid.
let snapshotUnchanged = GlucoseSnapshotStore.shared.load() == snapshot
let thresholdsChanged = LAAppGroupSettings.thresholdsMgdl() != UnitSettingsStore.shared.effectiveThresholds()

// Store + Watch: always update, independent of LA state.
LAAppGroupSettings.setThresholds(
lowMgdl: Storage.shared.lowLine.value,
highMgdl: Storage.shared.highLine.value,
)
writeThresholds()
GlucoseSnapshotStore.shared.save(snapshot)
// WatchConnectivityManager.shared.send(snapshot: snapshot)

Expand All @@ -946,7 +956,7 @@ final class LiveActivityManager {
LogManager.shared.log(category: .general, message: "[LA] refresh: LA update skipped — dismissedByUser=true reason=\(reason)")
return
}
guard !snapshotUnchanged || forceRefreshNeeded else { return }
guard !snapshotUnchanged || thresholdsChanged || forceRefreshNeeded else { return }
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
LogManager.shared.log(category: .general, message: "[LA] refresh: LA update skipped — areActivitiesEnabled=false reason=\(reason)")
return
Expand Down
17 changes: 12 additions & 5 deletions LoopFollow/Stats/GRI/GRICalculator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,23 @@ class GRICalculator {
var highCount = 0
var vHighCount = 0

// Compare at display precision so readings that show the same value share a band.
let normalize = UnitSettingsStore.shared.normalizeToDisplayPrecision
let vLowLimit = normalize(vLowThreshold)
let lowLimit = normalize(lowThreshold)
let highLimit = normalize(highThreshold)
let vHighLimit = normalize(vHighThreshold)

for reading in bgData {
let glucose = Double(reading.sgv)
let glucose = normalize(Double(reading.sgv))

if glucose < vLowThreshold {
if glucose < vLowLimit {
vLowCount += 1
} else if glucose < lowThreshold {
} else if glucose < lowLimit {
lowCount += 1
} else if glucose > vHighThreshold {
} else if glucose > vHighLimit {
vHighCount += 1
} else if glucose > highThreshold {
} else if glucose > highLimit {
highCount += 1
}
}
Expand Down
20 changes: 14 additions & 6 deletions LoopFollow/Stats/TIR/TIRCalculator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class TIRCalculator {
return tirPoints
}

private static func calculatePercentages(readings: [Double],
static func calculatePercentages(readings: [Double],
veryLowThreshold: Double,
lowThreshold: Double,
highThreshold: Double,
Expand All @@ -108,14 +108,22 @@ class TIRCalculator {
var highCount = 0
var veryHighCount = 0

for glucose in readings {
if glucose < veryLowThreshold {
// Compare at display precision so readings that show the same value share a band.
let normalize = UnitSettingsStore.shared.normalizeToDisplayPrecision
let veryLowLimit = normalize(veryLowThreshold)
let lowLimit = normalize(lowThreshold)
let highLimit = normalize(highThreshold)
let veryHighLimit = normalize(veryHighThreshold)

for reading in readings {
let glucose = normalize(reading)
if glucose < veryLowLimit {
veryLowCount += 1
} else if glucose < lowThreshold {
} else if glucose < lowLimit {
lowCount += 1
} else if glucose > veryHighThreshold {
} else if glucose > veryHighLimit {
veryHighCount += 1
} else if glucose > highThreshold {
} else if glucose > highLimit {
highCount += 1
} else {
inRangeCount += 1
Expand Down
17 changes: 9 additions & 8 deletions LoopFollow/ViewControllers/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,9 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate {
Storage.shared.units.$value.map { _ in () }.eraseToAnyPublisher(),
Storage.shared.useIFCC.$value.map { _ in () }.eraseToAnyPublisher(),
Storage.shared.showGMI.$value.map { _ in () }.eraseToAnyPublisher(),
Storage.shared.showStdDev.$value.map { _ in () }.eraseToAnyPublisher()
Storage.shared.showStdDev.$value.map { _ in () }.eraseToAnyPublisher(),
Storage.shared.lowLine.$value.map { _ in () }.eraseToAnyPublisher(),
Storage.shared.highLine.$value.map { _ in () }.eraseToAnyPublisher()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
Expand All @@ -282,6 +284,8 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate {
.sink { [weak self] _ in
self?.updateBGGraphSettings()
self?.updateBGGraph()
LiveActivityManager.shared.refreshFromCurrentState(reason: "range mode changed")
self?.updateStats()
}
.store(in: &cancellables)

Expand Down Expand Up @@ -828,13 +832,10 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate {
if bgData.count > 0 {
let latestBG = bgData[bgData.count - 1].sgv
if Storage.shared.colorBGText.value {
let thresholds = UnitSettingsStore.shared.effectiveThresholds()
if Double(latestBG) >= thresholds.high {
Observable.shared.bgTextColor.value = .yellow
} else if Double(latestBG) <= thresholds.low {
Observable.shared.bgTextColor.value = .red
} else {
Observable.shared.bgTextColor.value = .green
switch UnitSettingsStore.shared.glucoseBand(for: Double(latestBG)) {
case .high: Observable.shared.bgTextColor.value = .yellow
case .low: Observable.shared.bgTextColor.value = .red
case .inRange: Observable.shared.bgTextColor.value = .green
}
} else {
Observable.shared.bgTextColor.value = .primary
Expand Down
Loading
Loading