Skip to content

Commit fff2295

Browse files
codebyminibjorkert
andauthored
Units selection (#558)
* Refactor unit settings management and enhance metrics configuration - Introduced UnitSettingsStore to centralize unit and metric settings management. - Added new enums for glucose display units, time in range modes, glycemic metrics, and variability metrics. - Updated StatsData to calculate and store coefficient of variation. - Refactored Localizer to utilize UnitSettingsStore for unit conversions and formatting. - Enhanced Nightscout and Dexcom settings views to support unit configuration and onboarding. - Created UnitsConfigurationView for reusable unit and metric settings. - Updated AggregatedStatsView and SimpleStatsViewModel to reflect new unit settings. - Modified TIRView and its ViewModel to use UnitSettingsStore for thresholds and display. - Removed legacy storage references for unit settings in favor of the new centralized approach. - Added export functionality for new unit and metric settings in Nightscout settings. * Add commit guidelines and best practices to README.md * Fix navigation title for UnitsSettingsView to match consistency in SettingsMenuView * Refactor absorption time handling in LoopAPNSCarbsView to use separate hour and minute states, enhancing clarity and usability * Add polling of data after sucessful remote command * Remove remote command polling (moved to separate PR #566) * Replace deprecated NavigationLink(isActive:) with navigationDestination * Remove carbs screen redesign (moved to separate PR) * Remove README commit guidelines (moved to separate PR) * Added custom range * Remove list_prs.sh from repo Accidentally committed utility script that belongs outside the project. * Fix BGPicker not updating display when value changes or unit switches Use @State for glucoseUnit, lowValue, and highValue so SwiftUI can track changes and re-render. Add .id(glucoseUnit) on BGPickers to force recreation when the unit changes, ensuring allValues and formatting update correctly. --------- Co-authored-by: Jonas Björkert <jonas@bjorkert.se>
1 parent 8f1b9ac commit fff2295

24 files changed

Lines changed: 749 additions & 195 deletions

LoopFollow.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC572E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift */; };
6060
6589CC712E9E814F00BB18FE /* AlarmSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */; };
6161
6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */; };
62+
65A100012F5AA00000AA1001 /* UnitsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */; };
63+
65A100032F5AA00000AA1002 /* UnitsConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A100022F5AA00000AA1002 /* UnitsConfigurationView.swift */; };
6264
65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; };
6365
65E8A2862E44B0300065037B /* VolumeButtonHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */; };
6466
66E3D12E66AA4534A144A54B /* BackgroundRefreshManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8CA8BE0B3D247408FE088B4 /* BackgroundRefreshManager.swift */; };
@@ -509,6 +511,8 @@
509511
6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationModal.swift; sourceTree = "<group>"; };
510512
6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSelectionView.swift; sourceTree = "<group>"; };
511513
6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationManager.swift; sourceTree = "<group>"; };
514+
65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsSettingsView.swift; sourceTree = "<group>"; };
515+
65A100022F5AA00000AA1002 /* UnitsConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsConfigurationView.swift; sourceTree = "<group>"; };
512516
65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = "<group>"; };
513517
65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtonHandler.swift; sourceTree = "<group>"; };
514518
A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -970,6 +974,8 @@
970974
6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */,
971975
657F98172F043D8100F732BD /* HomeContentView.swift */,
972976
6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */,
977+
65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */,
978+
65A100022F5AA00000AA1002 /* UnitsConfigurationView.swift */,
973979
6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */,
974980
);
975981
path = Settings;
@@ -2251,6 +2257,8 @@
22512257
6589CC6C2E9E7D1600BB18FE /* GraphSettingsView.swift in Sources */,
22522258
6589CC6D2E9E7D1600BB18FE /* CalendarSettingsView.swift in Sources */,
22532259
6589CC6E2E9E7D1600BB18FE /* SettingsMenuView.swift in Sources */,
2260+
65A100012F5AA00000AA1001 /* UnitsSettingsView.swift in Sources */,
2261+
65A100032F5AA00000AA1002 /* UnitsConfigurationView.swift in Sources */,
22542262
657F98182F043D8100F732BD /* HomeContentView.swift in Sources */,
22552263
6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */,
22562264
DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */,

LoopFollow/Application/Base.lproj/Main.storyboard

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@
223223
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="xGF-Pj-QE0">
224224
<rect key="frame" x="102.66666666666666" y="0.0" width="92.666666666666657" height="45"/>
225225
<subviews>
226-
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Est A1C:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WV0-Jy-FPs" userLabel="Est A1C:">
226+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Est. A1C:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WV0-Jy-FPs" userLabel="Est. A1C:">
227227
<rect key="frame" x="18.333333333333346" y="0.0" width="56.333333333333343" height="18"/>
228228
<fontDescription key="fontDescription" type="system" pointSize="15"/>
229229
<nil key="textColor"/>
@@ -306,11 +306,13 @@
306306
<outlet property="smallGraphHeightConstraint" destination="qmO-ga-QWl" id="VsZ-zJ-LsJ"/>
307307
<outlet property="statsAvgBG" destination="jpA-Nb-pU7" id="Uo8-a4-Aus"/>
308308
<outlet property="statsEstA1C" destination="7Jx-XF-1vS" id="4RD-nm-JxO"/>
309+
<outlet property="statsEstA1CTitle" destination="WV0-Jy-FPs" id="WnU-h8-2hf"/>
309310
<outlet property="statsHighPercent" destination="HON-rt-8pC" id="283-3S-PCR"/>
310311
<outlet property="statsInRangePercent" destination="7mH-Np-j0L" id="vUp-Pv-Mva"/>
311312
<outlet property="statsLowPercent" destination="TzL-hn-9qu" id="0QR-Mz-KJe"/>
312313
<outlet property="statsPieChart" destination="Hhh-F1-s1p" id="Rhh-Up-Kr0"/>
313314
<outlet property="statsStdDev" destination="wAI-Tp-784" id="BUZ-lS-JfA"/>
315+
<outlet property="statsStdDevTitle" destination="iXC-Mz-I09" id="QHf-Q8-J7B"/>
314316
<outlet property="statsView" destination="ikj-at-auF" id="7AQ-VA-Pw2"/>
315317
</connections>
316318
</viewController>

LoopFollow/Controllers/Graphs.swift

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ class TempTargetRenderer: LineChartRenderer {
211211

212212
let ScaleXMax: Double = 150.0
213213
extension MainViewController {
214+
private func graphRangeThresholds() -> (low: Double, high: Double) {
215+
UnitSettingsStore.shared.effectiveThresholds()
216+
}
217+
214218
func updateChartRenderers() {
215219
let tempTargetDataIndex = GraphDataIndex.tempTarget.rawValue
216220
let smbDataIndex = GraphDataIndex.smb.rawValue
@@ -627,15 +631,17 @@ extension MainViewController {
627631
// Clear limit lines so they don't add multiples when changing the settings
628632
BGChart.rightAxis.removeAllLimitLines()
629633

630-
// Add lower red line based on low alert value
634+
let thresholds = graphRangeThresholds()
635+
636+
// Add lower red line
631637
let ll = ChartLimitLine()
632-
ll.limit = Storage.shared.lowLine.value
638+
ll.limit = thresholds.low
633639
ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5)
634640
BGChart.rightAxis.addLimitLine(ll)
635641

636-
// Add upper yellow line based on low alert value
642+
// Add upper yellow line
637643
let ul = ChartLimitLine()
638-
ul.limit = Storage.shared.highLine.value
644+
ul.limit = thresholds.high
639645
ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5)
640646
BGChart.rightAxis.addLimitLine(ul)
641647

@@ -786,15 +792,17 @@ extension MainViewController {
786792
// Clear limit lines so they don't add multiples when changing the settings
787793
BGChart.rightAxis.removeAllLimitLines()
788794

789-
// Add lower red line based on low alert value
795+
let thresholds = graphRangeThresholds()
796+
797+
// Add lower red line
790798
let ll = ChartLimitLine()
791-
ll.limit = Storage.shared.lowLine.value
799+
ll.limit = thresholds.low
792800
ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5)
793801
BGChart.rightAxis.addLimitLine(ll)
794802

795-
// Add upper yellow line based on low alert value
803+
// Add upper yellow line
796804
let ul = ChartLimitLine()
797-
ul.limit = Storage.shared.highLine.value
805+
ul.limit = thresholds.high
798806
ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5)
799807
BGChart.rightAxis.addLimitLine(ul)
800808

@@ -824,6 +832,7 @@ extension MainViewController {
824832
var colors = [NSUIColor]()
825833

826834
topBG = Storage.shared.minBGScale.value
835+
let thresholds = graphRangeThresholds()
827836
for i in 0 ..< entries.count {
828837
// Clamp the plotted y-value to the same bounds the header text uses
829838
// (HIGH/LOW), so the graph stays consistent with the main display.
@@ -836,9 +845,9 @@ extension MainViewController {
836845
mainChart.append(value)
837846
smallChart.append(value)
838847

839-
if Double(entries[i].sgv) >= Storage.shared.highLine.value {
848+
if Double(entries[i].sgv) >= thresholds.high {
840849
colors.append(NSUIColor.systemYellow)
841-
} else if Double(entries[i].sgv) <= Storage.shared.lowLine.value {
850+
} else if Double(entries[i].sgv) <= thresholds.low {
842851
colors.append(NSUIColor.systemRed)
843852
} else {
844853
colors.append(NSUIColor.systemGreen)

LoopFollow/Controllers/MainViewController+updateStats.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,25 @@ extension MainViewController {
2626
statsInRangePercent.text = String(format: "%.1f%%", stats.percentRange)
2727
statsHighPercent.text = String(format: "%.1f%%", stats.percentHigh)
2828
statsAvgBG.text = Localizer.toDisplayUnits(String(format: "%.0f", stats.avgBG))
29-
if Storage.shared.useIFCC.value {
29+
statsEstA1CTitle.text = UnitSettingsStore.shared.glycemicMetricMode == .gmi ? "GMI:" : "Est. A1C:"
30+
31+
if UnitSettingsStore.shared.glycemicOutputUnit == .mmolMol {
3032
statsEstA1C.text = String(format: "%.0f", stats.a1C)
3133
} else {
3234
statsEstA1C.text = String(format: "%.1f", stats.a1C)
3335
}
34-
statsStdDev.text = String(format: "%.2f", stats.stdDev)
36+
37+
if UnitSettingsStore.shared.variabilityMetricMode == .stdDeviation {
38+
statsStdDevTitle.text = "Std Dev:"
39+
if UnitSettingsStore.shared.glucoseUnit == .mgdL {
40+
statsStdDev.text = String(format: "%.0f", stats.stdDev)
41+
} else {
42+
statsStdDev.text = String(format: "%.1f", stats.stdDev)
43+
}
44+
} else {
45+
statsStdDevTitle.text = "CV:"
46+
statsStdDev.text = String(format: "%.1f%%", stats.coefficientOfVariation)
47+
}
3548

3649
createStatsPie(pieData: stats.pie)
3750
}

LoopFollow/Controllers/Stats.swift

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class StatsData {
1414
var avgBG: Float
1515
var a1C: Float
1616
var stdDev: Float
17+
var coefficientOfVariation: Float
1718
var bgDataCount: Int
1819
var pie: [DataStructs.pieData]
1920

@@ -23,12 +24,17 @@ class StatsData {
2324
countHigh = 0
2425
totalGlucose = 0
2526
a1C = 0.0
27+
coefficientOfVariation = 0.0
28+
29+
let thresholds = UnitSettingsStore.shared.effectiveThresholds()
30+
let lowThreshold = thresholds.low
31+
let highThreshold = thresholds.high
2632

2733
for i in 0 ..< bgData.count {
2834
// Set low/range/high counts for pie chart and %'s
29-
if Double(bgData[i].sgv) <= Storage.shared.lowLine.value {
35+
if Double(bgData[i].sgv) < lowThreshold {
3036
countLow += 1
31-
} else if Double(bgData[i].sgv) >= Storage.shared.highLine.value {
37+
} else if Double(bgData[i].sgv) > highThreshold {
3238
countHigh += 1
3339
} else {
3440
countRange += 1
@@ -60,15 +66,19 @@ class StatsData {
6066
partialSum += (Float(bgData[i].sgv) - avgBG) * (Float(bgData[i].sgv) - avgBG)
6167
}
6268

63-
stdDev = sqrt(partialSum / Float(bgData.count))
64-
if Storage.shared.units.value != "mg/dL" {
65-
stdDev = stdDev * Float(GlucoseConversion.mgDlToMmolL)
69+
let stdDevMgdL = sqrt(partialSum / Float(bgData.count))
70+
if avgBG > 0 {
71+
coefficientOfVariation = (stdDevMgdL / avgBG) * 100.0
6672
}
73+
stdDev = Float(UnitSettingsStore.shared.convertMgdlToDisplay(Double(stdDevMgdL)))
6774

68-
if Storage.shared.useIFCC.value {
69-
a1C = (((46.7 + Float(avgBG)) / 28.7) - 2.152) / 0.09148
75+
let avgBGDisplay = UnitSettingsStore.shared.convertMgdlToDisplay(Double(avgBG))
76+
let metricValue: Double?
77+
if UnitSettingsStore.shared.glycemicMetricMode == .gmi {
78+
metricValue = GlycemicMetricCalculator.calculateGMI(avgGlucoseInDisplayUnits: avgBGDisplay)
7079
} else {
71-
a1C = (46.7 + Float(avgBG)) / 28.7
80+
metricValue = GlycemicMetricCalculator.calculateEhba1c(avgGlucoseInDisplayUnits: avgBGDisplay)
7281
}
82+
a1C = Float(metricValue ?? 0.0)
7383
}
7484
}

0 commit comments

Comments
 (0)