Переглянути джерело

Merge pull request #318 from loopandlearn/infotable-trio

Info Table improvements
Marion Barker 2 роки тому
батько
коміт
a392f74237
33 змінених файлів з 961 додано та 426 видалено
  1. 62 6
      LoopFollow.xcodeproj/project.pbxproj
  2. 7 7
      LoopFollow/Controllers/Alarms.swift
  3. 4 4
      LoopFollow/Controllers/Graphs.swift
  4. 1 17
      LoopFollow/Controllers/NightScout.swift
  5. 6 6
      LoopFollow/Controllers/Nightscout/BGData.swift
  6. 4 4
      LoopFollow/Controllers/Nightscout/CAge.swift
  7. 22 26
      LoopFollow/Controllers/Nightscout/DeviceStatus.swift
  8. 45 13
      LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift
  9. 124 49
      LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift
  10. 50 0
      LoopFollow/Controllers/Nightscout/NSProfile.swift
  11. 3 4
      LoopFollow/Controllers/Nightscout/Profile.swift
  12. 120 0
      LoopFollow/Controllers/Nightscout/ProfileManager.swift
  13. 5 4
      LoopFollow/Controllers/Nightscout/SAge.swift
  14. 10 8
      LoopFollow/Controllers/Nightscout/Treatments/Basals.swift
  15. 1 2
      LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift
  16. 1 1
      LoopFollow/Controllers/StatsView.swift
  17. 20 0
      LoopFollow/Extensions/HKUnit+Extensions.swift
  18. 19 0
      LoopFollow/InfoTable/InfoData.swift
  19. 15 0
      LoopFollow/InfoTable/InfoDataSeparator.swift
  20. 78 0
      LoopFollow/InfoTable/InfoManager.swift
  21. 49 0
      LoopFollow/InfoTable/InfoType.swift
  22. 25 0
      LoopFollow/Metric/CarbMetric.swift
  23. 25 0
      LoopFollow/Metric/InsulinMetric.swift
  24. 25 0
      LoopFollow/Metric/Metric.swift
  25. 13 13
      LoopFollow/ViewControllers/AlarmViewController.swift
  26. 3 3
      LoopFollow/ViewControllers/GeneralSettingsViewController.swift
  27. 3 3
      LoopFollow/ViewControllers/GraphSettingsViewController.swift
  28. 51 54
      LoopFollow/ViewControllers/InfoDisplaySettingsViewController.swift
  29. 28 111
      LoopFollow/ViewControllers/MainViewController.swift
  30. 1 1
      LoopFollow/helpers/Chart.swift
  31. 117 0
      LoopFollow/helpers/Localizer.swift
  32. 0 67
      LoopFollow/helpers/bgUnits.swift
  33. 24 23
      LoopFollow/repository/UserDefaults.swift

+ 62 - 6
LoopFollow.xcodeproj/project.pbxproj

@@ -9,6 +9,16 @@
 /* Begin PBXBuildFile section */
 /* Begin PBXBuildFile section */
 		3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; };
 		3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; };
 		DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */; };
 		DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */; };
+		DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */; };
+		DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C612C4175FD00DBADDF /* NSProfile.swift */; };
+		DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */; };
+		DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */; };
+		DD0C0C682C48529400DBADDF /* Metric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C672C48529400DBADDF /* Metric.swift */; };
+		DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */; };
+		DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */; };
+		DD13BC752C3FD6210062313B /* InfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC742C3FD6200062313B /* InfoType.swift */; };
+		DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC762C3FD64E0062313B /* InfoData.swift */; };
+		DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC782C3FE63A0062313B /* InfoManager.swift */; };
 		DD152D3B24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD152D3A24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift */; };
 		DD152D3B24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD152D3A24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift */; };
 		DD493AD52ACF2109009A6922 /* ResumePump.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AD42ACF2109009A6922 /* ResumePump.swift */; };
 		DD493AD52ACF2109009A6922 /* ResumePump.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AD42ACF2109009A6922 /* ResumePump.swift */; };
 		DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AD62ACF2139009A6922 /* SuspendPump.swift */; };
 		DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AD62ACF2139009A6922 /* SuspendPump.swift */; };
@@ -22,8 +32,8 @@
 		DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */; };
 		DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */; };
 		DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; };
 		DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; };
 		DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */; };
 		DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */; };
-		DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */; };
 		DD608A0A2C23593900F91132 /* SMB.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A092C23593900F91132 /* SMB.swift */; };
 		DD608A0A2C23593900F91132 /* SMB.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A092C23593900F91132 /* SMB.swift */; };
+		DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */; };
 		DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */; };
 		DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */; };
 		DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19832ACDA50C00DBD158 /* Overrides.swift */; };
 		DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19832ACDA50C00DBD158 /* Overrides.swift */; };
 		DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19852ACDA59700DBD158 /* BGCheck.swift */; };
 		DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19852ACDA59700DBD158 /* BGCheck.swift */; };
@@ -193,7 +203,7 @@
 		FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD2A27C24C9D044009F7B7B /* Globals.swift */; };
 		FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD2A27C24C9D044009F7B7B /* Globals.swift */; };
 		FCD49B6C24AA536E007879DC /* DebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD49B6B24AA536E007879DC /* DebugViewController.swift */; };
 		FCD49B6C24AA536E007879DC /* DebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD49B6B24AA536E007879DC /* DebugViewController.swift */; };
 		FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */; };
 		FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */; };
-		FCEF87AC24A141A700AE6FA0 /* bgUnits.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEF87AA24A1417900AE6FA0 /* bgUnits.swift */; };
+		FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEF87AA24A1417900AE6FA0 /* Localizer.swift */; };
 		FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCFEEC9D2486E68E00402A7F /* WebKit.framework */; };
 		FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCFEEC9D2486E68E00402A7F /* WebKit.framework */; };
 		FCFEECA02488157B00402A7F /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEEC9F2488157B00402A7F /* Chart.swift */; };
 		FCFEECA02488157B00402A7F /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEEC9F2488157B00402A7F /* Chart.swift */; };
 		FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEECA1248857A600402A7F /* SettingsViewController.swift */; };
 		FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEECA1248857A600402A7F /* SettingsViewController.swift */; };
@@ -203,6 +213,16 @@
 		059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = "<group>"; };
 		059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = "<group>"; };
 		A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = "<group>"; };
 		DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = "<group>"; };
+		DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = "<group>"; };
+		DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = "<group>"; };
+		DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKUnit+Extensions.swift"; sourceTree = "<group>"; };
+		DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDataSeparator.swift; sourceTree = "<group>"; };
+		DD0C0C672C48529400DBADDF /* Metric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metric.swift; sourceTree = "<group>"; };
+		DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinMetric.swift; sourceTree = "<group>"; };
+		DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbMetric.swift; sourceTree = "<group>"; };
+		DD13BC742C3FD6200062313B /* InfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoType.swift; sourceTree = "<group>"; };
+		DD13BC762C3FD64E0062313B /* InfoData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoData.swift; sourceTree = "<group>"; };
+		DD13BC782C3FE63A0062313B /* InfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoManager.swift; sourceTree = "<group>"; };
 		DD152D3A24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDisplaySettingsViewController.swift; sourceTree = "<group>"; };
 		DD152D3A24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDisplaySettingsViewController.swift; sourceTree = "<group>"; };
 		DD493AD42ACF2109009A6922 /* ResumePump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumePump.swift; sourceTree = "<group>"; };
 		DD493AD42ACF2109009A6922 /* ResumePump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumePump.swift; sourceTree = "<group>"; };
 		DD493AD62ACF2139009A6922 /* SuspendPump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendPump.swift; sourceTree = "<group>"; };
 		DD493AD62ACF2139009A6922 /* SuspendPump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendPump.swift; sourceTree = "<group>"; };
@@ -216,8 +236,8 @@
 		DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = "<group>"; };
 		DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = "<group>"; };
 		DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = "<group>"; };
 		DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = "<group>"; };
 		DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusLoop.swift; sourceTree = "<group>"; };
 		DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusLoop.swift; sourceTree = "<group>"; };
-		DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAlertManager.swift; sourceTree = "<group>"; };
 		DD608A092C23593900F91132 /* SMB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMB.swift; sourceTree = "<group>"; };
 		DD608A092C23593900F91132 /* SMB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMB.swift; sourceTree = "<group>"; };
+		DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAlertManager.swift; sourceTree = "<group>"; };
 		DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusOpenAPS.swift; sourceTree = "<group>"; };
 		DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusOpenAPS.swift; sourceTree = "<group>"; };
 		DD7E19832ACDA50C00DBD158 /* Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Overrides.swift; sourceTree = "<group>"; };
 		DD7E19832ACDA50C00DBD158 /* Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Overrides.swift; sourceTree = "<group>"; };
 		DD7E19852ACDA59700DBD158 /* BGCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGCheck.swift; sourceTree = "<group>"; };
 		DD7E19852ACDA59700DBD158 /* BGCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGCheck.swift; sourceTree = "<group>"; };
@@ -393,7 +413,7 @@
 		FCD49B6B24AA536E007879DC /* DebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = "<group>"; };
 		FCD49B6B24AA536E007879DC /* DebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = "<group>"; };
 		FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = carbBolusArrays.swift; sourceTree = "<group>"; };
 		FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = carbBolusArrays.swift; sourceTree = "<group>"; };
 		FCE537C2249AAB2600F80BF8 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; };
 		FCE537C2249AAB2600F80BF8 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; };
-		FCEF87AA24A1417900AE6FA0 /* bgUnits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = bgUnits.swift; sourceTree = "<group>"; };
+		FCEF87AA24A1417900AE6FA0 /* Localizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localizer.swift; sourceTree = "<group>"; };
 		FCFEEC9D2486E68E00402A7F /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
 		FCFEEC9D2486E68E00402A7F /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
 		FCFEEC9F2488157B00402A7F /* Chart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chart.swift; sourceTree = "<group>"; };
 		FCFEEC9F2488157B00402A7F /* Chart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chart.swift; sourceTree = "<group>"; };
 		FCFEECA1248857A600402A7F /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
 		FCFEECA1248857A600402A7F /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
@@ -431,6 +451,27 @@
 			path = Pods;
 			path = Pods;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		DD0C0C692C4852A100DBADDF /* Metric */ = {
+			isa = PBXGroup;
+			children = (
+				DD0C0C672C48529400DBADDF /* Metric.swift */,
+				DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */,
+				DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */,
+			);
+			path = Metric;
+			sourceTree = "<group>";
+		};
+		DD13BC732C3FD60E0062313B /* InfoTable */ = {
+			isa = PBXGroup;
+			children = (
+				DD13BC742C3FD6200062313B /* InfoType.swift */,
+				DD13BC762C3FD64E0062313B /* InfoData.swift */,
+				DD13BC782C3FE63A0062313B /* InfoManager.swift */,
+				DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */,
+			);
+			path = InfoTable;
+			sourceTree = "<group>";
+		};
 		DD493AEA2ACF2761009A6922 /* Treatments */ = {
 		DD493AEA2ACF2761009A6922 /* Treatments */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -461,6 +502,8 @@
 				DD493AE82ACF2445009A6922 /* BGData.swift */,
 				DD493AE82ACF2445009A6922 /* BGData.swift */,
 				DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */,
 				DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */,
 				DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */,
 				DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */,
+				DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */,
+				DD0C0C612C4175FD00DBADDF /* NSProfile.swift */,
 			);
 			);
 			path = Nightscout;
 			path = Nightscout;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -471,6 +514,7 @@
 				DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */,
 				DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */,
 				DDCF979324C0D380002C9752 /* UIViewExtension.swift */,
 				DDCF979324C0D380002C9752 /* UIViewExtension.swift */,
 				DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */,
 				DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */,
+				DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */,
 			);
 			);
 			path = Extensions;
 			path = Extensions;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -646,6 +690,8 @@
 		FC8DEEE32485D1680075863F /* LoopFollow */ = {
 		FC8DEEE32485D1680075863F /* LoopFollow */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DD0C0C692C4852A100DBADDF /* Metric */,
+				DD13BC732C3FD60E0062313B /* InfoTable */,
 				FCC688702489A57C00A0279D /* Loop Follow.entitlements */,
 				FCC688702489A57C00A0279D /* Loop Follow.entitlements */,
 				FC8DEEE62485D1ED0075863F /* Info.plist */,
 				FC8DEEE62485D1ED0075863F /* Info.plist */,
 				FC7CE59A248D334B001F83B8 /* Resources */,
 				FC7CE59A248D334B001F83B8 /* Resources */,
@@ -704,7 +750,7 @@
 				FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */,
 				FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */,
 				FC16A98024996C07003D6245 /* DateTime.swift */,
 				FC16A98024996C07003D6245 /* DateTime.swift */,
 				FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */,
 				FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */,
-				FCEF87AA24A1417900AE6FA0 /* bgUnits.swift */,
+				FCEF87AA24A1417900AE6FA0 /* Localizer.swift */,
 				FC1BDD2E24A232A3001B652C /* DataStructs.swift */,
 				FC1BDD2E24A232A3001B652C /* DataStructs.swift */,
 				FCD2A27C24C9D044009F7B7B /* Globals.swift */,
 				FCD2A27C24C9D044009F7B7B /* Globals.swift */,
 				FC8589BE252B54F500C8FC73 /* Mobileprovision.swift */,
 				FC8589BE252B54F500C8FC73 /* Mobileprovision.swift */,
@@ -1009,12 +1055,15 @@
 				FC16A97D24996747003D6245 /* Alarms.swift in Sources */,
 				FC16A97D24996747003D6245 /* Alarms.swift in Sources */,
 				FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */,
 				FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */,
 				DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */,
 				DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */,
+				DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */,
 				DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */,
 				DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */,
 				FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */,
 				FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */,
+				DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */,
 				FCFEECA02488157B00402A7F /* Chart.swift in Sources */,
 				FCFEECA02488157B00402A7F /* Chart.swift in Sources */,
 				DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */,
 				DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */,
+				DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */,
 				DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */,
 				DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */,
-				FCEF87AC24A141A700AE6FA0 /* bgUnits.swift in Sources */,
+				FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */,
 				FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */,
 				FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */,
 				FCC6886524898EEE00A0279D /* UserDefaults.swift in Sources */,
 				FCC6886524898EEE00A0279D /* UserDefaults.swift in Sources */,
 				FC16A97F249969E2003D6245 /* Graphs.swift in Sources */,
 				FC16A97F249969E2003D6245 /* Graphs.swift in Sources */,
@@ -1024,6 +1073,8 @@
 				DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */,
 				DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */,
 				DD152D3B24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift in Sources */,
 				DD152D3B24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift in Sources */,
 				DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */,
 				DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */,
+				DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */,
+				DD13BC752C3FD6210062313B /* InfoType.swift in Sources */,
 				DDCF979C24C14EFB002C9752 /* AdvancedSettingsViewController.swift in Sources */,
 				DDCF979C24C14EFB002C9752 /* AdvancedSettingsViewController.swift in Sources */,
 				FC97881C2485969B00A7906C /* MainViewController.swift in Sources */,
 				FC97881C2485969B00A7906C /* MainViewController.swift in Sources */,
 				DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */,
 				DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */,
@@ -1033,20 +1084,25 @@
 				DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */,
 				DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */,
 				FC9788182485969B00A7906C /* AppDelegate.swift in Sources */,
 				FC9788182485969B00A7906C /* AppDelegate.swift in Sources */,
 				DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */,
 				DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */,
+				DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */,
 				DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */,
 				DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */,
 				DD493AE92ACF2445009A6922 /* BGData.swift in Sources */,
 				DD493AE92ACF2445009A6922 /* BGData.swift in Sources */,
 				FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */,
 				FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */,
 				DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */,
 				DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */,
 				DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */,
 				DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */,
+				DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */,
 				DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */,
 				DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */,
+				DD0C0C682C48529400DBADDF /* Metric.swift in Sources */,
 				FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,
 				FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,
 				FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */,
 				FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */,
+				DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */,
 				DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */,
 				DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */,
 				FCD49B6C24AA536E007879DC /* DebugViewController.swift in Sources */,
 				FCD49B6C24AA536E007879DC /* DebugViewController.swift in Sources */,
 				FC1BDD2B24A22650001B652C /* Stats.swift in Sources */,
 				FC1BDD2B24A22650001B652C /* Stats.swift in Sources */,
 				FC1BDD2D24A23204001B652C /* StatsView.swift in Sources */,
 				FC1BDD2D24A23204001B652C /* StatsView.swift in Sources */,
 				FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */,
 				FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */,
 				FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */,
 				FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */,
+				DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */,
 				DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */,
 				DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */,
 				DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */,
 				DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */,
 				FCA2DDE62501095000254A8C /* Timers.swift in Sources */,
 				FCA2DDE62501095000254A8C /* Timers.swift in Sources */,

+ 7 - 7
LoopFollow/Controllers/Alarms.swift

@@ -86,7 +86,7 @@ extension MainViewController {
             }
             }
             if bolusCount >= UserDefaultsRepository.alertIOBNumber.value ||
             if bolusCount >= UserDefaultsRepository.alertIOBNumber.value ||
                 totalBoluses >= Double(UserDefaultsRepository.alertIOBMaxBoluses.value) ||
                 totalBoluses >= Double(UserDefaultsRepository.alertIOBMaxBoluses.value) ||
-                Double(latestIOB) ?? 0 >= Double(UserDefaultsRepository.alertIOBMaxBoluses.value) {
+                (latestIOB?.value ?? 0) >= Double(UserDefaultsRepository.alertIOBMaxBoluses.value) {
                 AlarmSound.whichAlarm = "IOB Alert"
                 AlarmSound.whichAlarm = "IOB Alert"
                 //determine if it is day or night and what should happen
                 //determine if it is day or night and what should happen
                 if UserDefaultsRepository.nightTime.value {
                 if UserDefaultsRepository.nightTime.value {
@@ -104,7 +104,7 @@ extension MainViewController {
         // Check COB
         // Check COB
         if UserDefaultsRepository.alertCOB.value && !UserDefaultsRepository.alertCOBIsSnoozed.value {
         if UserDefaultsRepository.alertCOB.value && !UserDefaultsRepository.alertCOBIsSnoozed.value {
             let alertAt = Double(UserDefaultsRepository.alertCOBAt.value)
             let alertAt = Double(UserDefaultsRepository.alertCOBAt.value)
-            if Double(latestCOB) ?? 0 >= alertAt {
+            if (latestCOB?.value ?? 0) >= alertAt {
                 AlarmSound.whichAlarm = "COB Alert"
                 AlarmSound.whichAlarm = "COB Alert"
                 //determine if it is day or night and what should happen
                 //determine if it is day or night and what should happen
                 if UserDefaultsRepository.nightTime.value {
                 if UserDefaultsRepository.nightTime.value {
@@ -586,7 +586,7 @@ extension MainViewController {
         if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false }
         if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false }
         
         
         guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
         guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
-        snoozer.updateDisplayWhenTriggered(bgVal: bgUnits.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: bgUnits.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm)
+        snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: Localizer.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm)
         if audio && !UserDefaultsRepository.alertMuteAllIsMuted.value && audioDuringCall{
         if audio && !UserDefaultsRepository.alertMuteAllIsMuted.value && audioDuringCall{
             AlarmSound.setSoundFile(str: sound)
             AlarmSound.setSoundFile(str: sound)
             AlarmSound.play(overrideVolume: overrideVolume, numLoops: numLoops)
             AlarmSound.play(overrideVolume: overrideVolume, numLoops: numLoops)
@@ -600,7 +600,7 @@ extension MainViewController {
         if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false }
         if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false }
         
         
         guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
         guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
-        snoozer.updateDisplayWhenTriggered(bgVal: bgUnits.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: bgUnits.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm)
+        snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: Localizer.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm)
         snoozer.SnoozeButton.isHidden = false
         snoozer.SnoozeButton.isHidden = false
         snoozer.AlertLabel.isHidden = false
         snoozer.AlertLabel.isHidden = false
         snoozer.clockLabel.isHidden = true
         snoozer.clockLabel.isHidden = true
@@ -633,7 +633,7 @@ extension MainViewController {
         
         
         AlarmSound.whichAlarm = "none"
         AlarmSound.whichAlarm = "none"
         guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
         guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
-        snoozer.updateDisplayWhenTriggered(bgVal: bgUnits.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: bgUnits.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm)
+        snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: Localizer.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm)
         snoozer.SnoozeButton.isHidden = true
         snoozer.SnoozeButton.isHidden = true
         snoozer.AlertLabel.isHidden = true
         snoozer.AlertLabel.isHidden = true
         if AlarmSound.isPlaying {
         if AlarmSound.isPlaying {
@@ -1026,14 +1026,14 @@ extension MainViewController {
         let texts = AnnouncementTexts.forLanguage(preferredLanguage)
         let texts = AnnouncementTexts.forLanguage(preferredLanguage)
         
         
         let negligibleThreshold = 3
         let negligibleThreshold = 3
-        let localizedCurrentValue = bgUnits.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".")
+        let localizedCurrentValue = Localizer.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".")
         let announcementText: String
         let announcementText: String
         
         
         if abs(bloodGlucoseDifference) <= negligibleThreshold {
         if abs(bloodGlucoseDifference) <= negligibleThreshold {
             announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(texts.stable)"
             announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(texts.stable)"
         } else {
         } else {
             let directionText = bloodGlucoseDifference < 0 ? texts.decrease : texts.increase
             let directionText = bloodGlucoseDifference < 0 ? texts.decrease : texts.increase
-            let absoluteDifference = bgUnits.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".")
+            let absoluteDifference = Localizer.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".")
             announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(directionText) \(absoluteDifference)"
             announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(directionText) \(absoluteDifference)"
         }
         }
         
         

+ 4 - 4
LoopFollow/Controllers/Graphs.swift

@@ -636,7 +636,7 @@ extension MainViewController {
             if Float(entries[i].sgv) > topBG - maxBGOffset {
             if Float(entries[i].sgv) > topBG - maxBGOffset {
                 topBG = Float(entries[i].sgv) + maxBGOffset
                 topBG = Float(entries[i].sgv) + maxBGOffset
             }
             }
-            let value = ChartDataEntry(x: Double(entries[i].date), y: Double(entries[i].sgv), data: formatPillText(line1: bgUnits.toDisplayUnits(String(entries[i].sgv)), time: entries[i].date))
+            let value = ChartDataEntry(x: Double(entries[i].date), y: Double(entries[i].sgv), data: formatPillText(line1: Localizer.toDisplayUnits(String(entries[i].sgv)), time: entries[i].date))
             if UserDefaultsRepository.debugLog.value { writeDebugLog(value: "BG: " + value.description) }
             if UserDefaultsRepository.debugLog.value { writeDebugLog(value: "BG: " + value.description) }
             mainChart.append(value)
             mainChart.append(value)
             smallChart.append(value)
             smallChart.append(value)
@@ -731,7 +731,7 @@ extension MainViewController {
                 colors.append(color ?? NSUIColor.systemPurple)
                 colors.append(color ?? NSUIColor.systemPurple)
             }
             }
             
             
-            let value = ChartDataEntry(x: predictionData[i].date, y: predictionVal, data: formatPillText(line1: bgUnits.toDisplayUnits(String(predictionData[i].sgv)), time: predictionData[i].date))
+            let value = ChartDataEntry(x: predictionData[i].date, y: predictionVal, data: formatPillText(line1: Localizer.toDisplayUnits(String(predictionData[i].sgv)), time: predictionData[i].date))
             mainChart.addEntry(value)
             mainChart.addEntry(value)
             smallChart.addEntry(value)
             smallChart.addEntry(value)
         }
         }
@@ -1025,7 +1025,7 @@ extension MainViewController {
             let graphHours = 24 * UserDefaultsRepository.downloadDays.value
             let graphHours = 24 * UserDefaultsRepository.downloadDays.value
             if bgCheckData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
             if bgCheckData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
             
             
-            let value = ChartDataEntry(x: Double(bgCheckData[i].date), y: Double(bgCheckData[i].sgv), data: formatPillText(line1: bgUnits.toDisplayUnits(String(bgCheckData[i].sgv)), time: bgCheckData[i].date))
+            let value = ChartDataEntry(x: Double(bgCheckData[i].date), y: Double(bgCheckData[i].sgv), data: formatPillText(line1: Localizer.toDisplayUnits(String(bgCheckData[i].sgv)), time: bgCheckData[i].date))
             BGChart.data?.dataSets[dataIndex].addEntry(value)
             BGChart.data?.dataSets[dataIndex].addEntry(value)
             if UserDefaultsRepository.smallGraphTreatments.value {
             if UserDefaultsRepository.smallGraphTreatments.value {
                 BGChartFull.data?.dataSets[dataIndex].addEntry(value)
                 BGChartFull.data?.dataSets[dataIndex].addEntry(value)
@@ -1573,7 +1573,7 @@ extension MainViewController {
                 data: formatPillText(
                 data: formatPillText(
                     line1: chartLabel,
                     line1: chartLabel,
                     time: predictionData[i].date,
                     time: predictionData[i].date,
-                    line2: bgUnits.toDisplayUnits(String(predictionVal))
+                    line2: Localizer.toDisplayUnits(String(predictionVal))
                 )
                 )
             )
             )
             mainChart.addEntry(value)
             mainChart.addEntry(value)

+ 1 - 17
LoopFollow/Controllers/NightScout.swift

@@ -19,29 +19,13 @@ extension MainViewController {
         var created_at: String
         var created_at: String
     }
     }
 
 
-    
     //NS Basal Profile Struct
     //NS Basal Profile Struct
     struct basalProfileStruct: Codable {
     struct basalProfileStruct: Codable {
         var value: Double
         var value: Double
         var time: String
         var time: String
         var timeAsSeconds: Double
         var timeAsSeconds: Double
     }
     }
-    
-    struct NSProfile: Decodable {
-        struct Store: Decodable {
-            struct BasalEntry: Decodable {
-                let value: Double
-                let time: String
-                let timeAsSeconds: Double
-            }
-            
-            let basal: [BasalEntry]
-        }
-        
-        let store: [String: Store]
-        let defaultProfile: String
-    }
-    
+
     //NS Basal Data  Struct
     //NS Basal Data  Struct
     struct basalGraphStruct: Codable {
     struct basalGraphStruct: Codable {
         var basalRate: Double
         var basalRate: Double

+ 6 - 6
LoopFollow/Controllers/Nightscout/BGData.swift

@@ -239,8 +239,8 @@ extension MainViewController {
             var snoozerDelta = ""
             var snoozerDelta = ""
             
             
             // Set BGText with the latest BG value
             // Set BGText with the latest BG value
-            self.BGText.text = bgUnits.toDisplayUnits(String(latestBG))
-            snoozerBG = bgUnits.toDisplayUnits(String(latestBG))
+            self.BGText.text = Localizer.toDisplayUnits(String(latestBG))
+            snoozerBG = Localizer.toDisplayUnits(String(latestBG))
             self.setBGTextColor()
             self.setBGTextColor()
             
             
             // Direction handling
             // Direction handling
@@ -256,12 +256,12 @@ extension MainViewController {
             
             
             // Delta handling
             // Delta handling
             if deltaBG < 0 {
             if deltaBG < 0 {
-                self.DeltaText.text = bgUnits.toDisplayUnits(String(deltaBG))
-                snoozerDelta = bgUnits.toDisplayUnits(String(deltaBG))
+                self.DeltaText.text = Localizer.toDisplayUnits(String(deltaBG))
+                snoozerDelta = Localizer.toDisplayUnits(String(deltaBG))
                 self.latestDeltaString = String(deltaBG)
                 self.latestDeltaString = String(deltaBG)
             } else {
             } else {
-                self.DeltaText.text = "+" + bgUnits.toDisplayUnits(String(deltaBG))
-                snoozerDelta = "+" + bgUnits.toDisplayUnits(String(deltaBG))
+                self.DeltaText.text = "+" + Localizer.toDisplayUnits(String(deltaBG))
+                snoozerDelta = "+" + Localizer.toDisplayUnits(String(deltaBG))
                 self.latestDeltaString = "+" + String(deltaBG)
                 self.latestDeltaString = "+" + String(deltaBG)
             }
             }
             
             

+ 4 - 4
LoopFollow/Controllers/Nightscout/CAge.swift

@@ -30,7 +30,7 @@ extension MainViewController {
     
     
     // NS Cage Response Processor
     // NS Cage Response Processor
     func updateCage(data: [cageData]) {
     func updateCage(data: [cageData]) {
-        self.clearLastInfoData(index: 7)
+        infoManager.clearInfoData(type: .cage)
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: CAGE") }
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: CAGE") }
         if data.count == 0 {
         if data.count == 0 {
             return
             return
@@ -55,9 +55,9 @@ extension MainViewController {
             formatter.allowedUnits = [ .day, .hour ] // Units to display in the formatted string
             formatter.allowedUnits = [ .day, .hour ] // Units to display in the formatted string
             formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
             formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
             
             
-            let formattedDuration = formatter.string(from: secondsAgo)
-            tableData[7].value = formattedDuration ?? ""
+            if let formattedDuration = formatter.string(from: secondsAgo) {
+                infoManager.updateInfoData(type: .cage, value: formattedDuration)
+            }
         }
         }
-        infoTable.reloadData()
     }
     }
 }
 }

+ 22 - 26
LoopFollow/Controllers/Nightscout/DeviceStatus.swift

@@ -83,11 +83,8 @@ extension MainViewController {
         
         
     // NS Device Status Response Processor
     // NS Device Status Response Processor
     func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
     func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
-        self.clearLastInfoData(index: 0)
-        self.clearLastInfoData(index: 1)
-        self.clearLastInfoData(index: 3)
-        self.clearLastInfoData(index: 4)
-        self.clearLastInfoData(index: 5)
+        infoManager.clearInfoData(types: [.iob, .cob, .override, .battery, .pump, .target, .isf, .carbRatio, .updated, .recBolus, .tdd])
+
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
         if jsonDeviceStatus.count == 0 {
         if jsonDeviceStatus.count == 0 {
             return
             return
@@ -106,15 +103,15 @@ extension MainViewController {
             if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970  {
             if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970  {
                 if let reservoirData = lastPumpRecord["reservoir"] as? Double {
                 if let reservoirData = lastPumpRecord["reservoir"] as? Double {
                     latestPumpVolume = reservoirData
                     latestPumpVolume = reservoirData
-                    tableData[5].value = String(format:"%.0f", reservoirData) + "U"
+                    infoManager.updateInfoData(type: .pump, value: String(format: "%.0f", reservoirData) + "U")
                 } else {
                 } else {
                     latestPumpVolume = 50.0
                     latestPumpVolume = 50.0
-                    tableData[5].value = "50+U"
+                    infoManager.updateInfoData(type: .pump, value: "50+U")
                 }
                 }
-                
-                if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
-                    let upbat = uploader["battery"] as! Double
-                    tableData[4].value = String(format:"%.0f", upbat) + "%"
+
+                if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject],
+                   let upbat = uploader["battery"] as? Double {
+                    infoManager.updateInfoData(type: .battery, value: String(format: "%.0f", upbat) + "%")
                     UserDefaultsRepository.deviceBatteryLevel.value = upbat
                     UserDefaultsRepository.deviceBatteryLevel.value = upbat
                 }
                 }
             }
             }
@@ -130,31 +127,30 @@ extension MainViewController {
             DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord)
             DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord)
         }
         }
         
         
-        var oText = "" as String
+        var oText = ""
         currentOverride = 1.0
         currentOverride = 1.0
-        if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
-            if lastOverride["active"] as! Bool {
+        if let lastOverride = lastDeviceStatus?["override"] as? [String: AnyObject],
+           let isActive = lastOverride["active"] as? Bool, isActive {
+            if let lastCorrection = lastOverride["currentCorrectionRange"] as? [String: AnyObject],
+               let minValue = lastCorrection["minValue"] as? Double,
+               let maxValue = lastCorrection["maxValue"] as? Double {
                 
                 
-                let lastCorrection  = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
                 if let multiplier = lastOverride["multiplier"] as? Double {
                 if let multiplier = lastOverride["multiplier"] as? Double {
                     currentOverride = multiplier
                     currentOverride = multiplier
                     oText += String(format: "%.0f%%", (multiplier * 100))
                     oText += String(format: "%.0f%%", (multiplier * 100))
-                }
-                else
-                {
+                } else {
                     oText += "100%"
                     oText += "100%"
                 }
                 }
-                oText += " ("
-                let minValue = lastCorrection["minValue"] as! Double
-                let maxValue = lastCorrection["maxValue"] as! Double
-                oText += bgUnits.toDisplayUnits(String(minValue)) + "-" + bgUnits.toDisplayUnits(String(maxValue)) + ")"
                 
                 
-                tableData[3].value =  oText
+                oText += " ("
+                oText += Localizer.toDisplayUnits(String(minValue)) + "-" + Localizer.toDisplayUnits(String(maxValue)) + ")"
             }
             }
+            
+            infoManager.updateInfoData(type: .override, value: oText)
+        } else {
+            infoManager.clearInfoData(type: .override)
         }
         }
-        
-        infoTable.reloadData()
-        
+
         // Start the timer based on the timestamp
         // Start the timer based on the timestamp
         let now = dateTimeUtils.getNowTimeIntervalUTC()
         let now = dateTimeUtils.getNowTimeIntervalUTC()
         let secondsAgo = now - latestLoopTime
         let secondsAgo = now - latestLoopTime

+ 45 - 13
LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift

@@ -9,6 +9,7 @@
 import Foundation
 import Foundation
 import UIKit
 import UIKit
 import Charts
 import Charts
+import HealthKit
 
 
 extension MainViewController {
 extension MainViewController {
     func DeviceStatusLoop(formatter: ISO8601DateFormatter, lastLoopRecord: [String: AnyObject]) {
     func DeviceStatusLoop(formatter: ISO8601DateFormatter, lastLoopRecord: [String: AnyObject]) {
@@ -29,17 +30,44 @@ extension MainViewController {
                         
                         
                     }
                     }
                 }
                 }
-                if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
-                    tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
-                    latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
+
+                // ISF
+                let profileISF = profileManager.currentISF()
+                if let profileISF = profileISF {
+                    infoManager.updateInfoData(type: .isf, value: profileISF)
+                }
+
+                // Carb Ratio (CR)
+                let profileCR = profileManager.currentCarbRatio()
+                if let profileCR = profileCR {
+                    infoManager.updateInfoData(type: .carbRatio, value: profileCR)
                 }
                 }
-                if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
-                    tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
-                    latestCOB = String(format:"%.0f", cobdata["cob"] as! Double)
+
+                // Target
+                let profileTargetLow = profileManager.currentTargetLow()
+                let profileTargetHigh = profileManager.currentTargetHigh()
+
+                if let profileTargetLow = profileTargetLow, let profileTargetHigh = profileTargetHigh, profileTargetLow != profileTargetHigh {
+                    infoManager.updateInfoData(type: .target, firstValue: profileTargetLow, secondValue: profileTargetHigh, separator: .dash)
+                } else if let profileTargetLow = profileTargetLow {
+                    infoManager.updateInfoData(type: .target, value: profileTargetLow)
+                }
+
+                // IOB
+                if let insulinMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") {
+                    infoManager.updateInfoData(type: .iob, value: insulinMetric)
+                    latestIOB = insulinMetric
                 }
                 }
+
+                // COB
+                if let cobMetric = CarbMetric(from: lastLoopRecord["cob"], key: "cob") {
+                    infoManager.updateInfoData(type: .cob, value: cobMetric)
+                    latestCOB = cobMetric
+                }
+
                 if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
                 if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
                     let prediction = predictdata["values"] as! [Double]
                     let prediction = predictdata["values"] as! [Double]
-                    PredictionLabel.text = bgUnits.toDisplayUnits(String(Int(prediction.last!)))
+                    PredictionLabel.text = Localizer.toDisplayUnits(String(Int(prediction.last!)))
                     PredictionLabel.textColor = UIColor.systemPurple
                     PredictionLabel.textColor = UIColor.systemPurple
                     if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
                     if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
                         predictionData.removeAll()
                         predictionData.removeAll()
@@ -59,19 +87,23 @@ extension MainViewController {
                             i += 1
                             i += 1
                         }
                         }
                         
                         
-                        let predMin = prediction.min()
-                        let predMax = prediction.max()
-                        tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
-                        
+                        if let predMin = prediction.min(), let predMax = prediction.max() {
+                            let formattedMin = Localizer.toDisplayUnits(String(predMin))
+                            let formattedMax = Localizer.toDisplayUnits(String(predMax))
+                            let value = "\(formattedMin)/\(formattedMax)"
+                            infoManager.updateInfoData(type: .minMax, value: value)
+                        }
+
                         updatePredictionGraph()
                         updatePredictionGraph()
                     }
                     }
                 } else {
                 } else {
                     predictionData.removeAll()
                     predictionData.removeAll()
-                    tableData[9].value = ""
+                    infoManager.clearInfoData(type: .minMax)
                     updatePredictionGraph()
                     updatePredictionGraph()
                 }
                 }
                 if let recBolus = lastLoopRecord["recommendedBolus"] as? Double {
                 if let recBolus = lastLoopRecord["recommendedBolus"] as? Double {
-                    tableData[8].value = String(format:"%.2fU", recBolus)
+                    let formattedRecBolus = String(format: "%.2fU", recBolus)
+                    infoManager.updateInfoData(type: .recBolus, value: formattedRecBolus)
                     UserDefaultsRepository.deviceRecBolus.value = recBolus
                     UserDefaultsRepository.deviceRecBolus.value = recBolus
                 }
                 }
                 if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
                 if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {

+ 124 - 49
LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift

@@ -1,92 +1,166 @@
-//
-//  DeviceStatusOpenAPS.swift
-//  LoopFollow
-//
-//  Created by Jonas Björkert on 2024-05-19.
-//  Copyright © 2024 Jon Fawcett. All rights reserved.
-//
+// DeviceStatusOpenAPS.swift
+// LoopFollow
+// Created by Jonas Björkert on 2024-05-19.
+// Copyright © 2024 Jon Fawcett. All rights reserved.
 
 
 import Foundation
 import Foundation
 import UIKit
 import UIKit
+import HealthKit
 
 
 extension MainViewController {
 extension MainViewController {
     func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) {
     func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) {
-        
         if let lastLoopTime = formatter.date(from: (lastDeviceStatus?["created_at"] as! String))?.timeIntervalSince1970 {
         if let lastLoopTime = formatter.date(from: (lastDeviceStatus?["created_at"] as! String))?.timeIntervalSince1970 {
             UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
             UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
             if lastLoopRecord["failureReason"] != nil {
             if lastLoopRecord["failureReason"] != nil {
                 LoopStatusLabel.text = "X"
                 LoopStatusLabel.text = "X"
                 latestLoopStatusString = "X"
                 latestLoopStatusString = "X"
-                if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
             } else {
             } else {
-                var wasEnacted = false
-                if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] {
-                    wasEnacted = true
-                }
-                
-                if let iobdata = lastLoopRecord["iob"] as? [String: AnyObject] {
-                    tableData[0].value = String(format: "%.2f", (iobdata["iob"] as! Double))
-                    latestIOB = String(format: "%.2f", (iobdata["iob"] as! Double))
-                }
-                if let cobdata = lastLoopRecord["enacted"] as? [String: AnyObject] {
-                    tableData[1].value = String(format: "%.0f", cobdata["COB"] as! Double)
-                    latestCOB = String(format: "%.0f", cobdata["COB"] as! Double)
-                }
-                if let recbolusdata = lastLoopRecord["enacted"] as? [String: AnyObject],
-                   let insulinReq = recbolusdata["insulinReq"] as? Double {
-                    tableData[8].value = String(format: "%.2fU", insulinReq)
-                    UserDefaultsRepository.deviceRecBolus.value = insulinReq
+                guard let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] else {
+                    LoopStatusLabel.text = "↻"
+                    latestLoopStatusString = "↻"
+                    evaluateNotLooping(lastLoopTime: lastLoopTime)
+                    return
+                }
+                let wasEnacted = true
+
+                var determinedUnit: HKUnit = .milligramsPerDeciliter
+
+                // Determine the unit based on the threshold value since no unit is provided
+                if let enactedTargetValue = enacted["threshold"] as? Double {
+                    if enactedTargetValue < 40 {
+                        determinedUnit = .millimolesPerLiter
+                    }
+                }
+
+                // Updated
+                if let enactedTimestamp = enacted["timestamp"] as? String,
+                   let enactedTime = formatter.date(from: enactedTimestamp)?.timeIntervalSince1970 {
+                    let formattedTime = Localizer.formatTimestampToLocalString(enactedTime)
+                    infoManager.updateInfoData(type: .updated, value: formattedTime)
+                }
+
+                // ISF
+                let profileISF = profileManager.currentISF()
+                var enactedISF: HKQuantity?
+                if let enactedISFValue = enacted["ISF"] as? Double {
+                    enactedISF = HKQuantity(unit: determinedUnit, doubleValue: enactedISFValue)
+                }
+                if let profileISF = profileISF, let enactedISF = enactedISF, profileISF != enactedISF {
+                    infoManager.updateInfoData(type: .isf, firstValue: profileISF, secondValue: enactedISF, separator: .arrow)
+                } else if let profileISF = profileISF {
+                    infoManager.updateInfoData(type: .isf, value: profileISF)
+                }
+
+                // Carb Ratio (CR)
+                let profileCR = profileManager.currentCarbRatio()
+                var enactedCR: Double?
+                if let reasonString = enacted["reason"] as? String {
+                    let pattern = "CR: (\\d+(?:\\.\\d+)?)"
+                    if let regex = try? NSRegularExpression(pattern: pattern) {
+                        let nsString = reasonString as NSString
+                        if let match = regex.firstMatch(in: reasonString, range: NSRange(location: 0, length: nsString.length)) {
+                            let crString = nsString.substring(with: match.range(at: 1))
+                            enactedCR = Double(crString)
+                        }
+                    }
+                }
+
+                if let profileCR = profileCR, let enactedCR = enactedCR, profileCR != enactedCR {
+                    infoManager.updateInfoData(type: .carbRatio, value: profileCR, enactedValue: enactedCR, separator: .arrow)
+                } else if let profileCR = profileCR {
+                    infoManager.updateInfoData(type: .carbRatio, value: profileCR)
+                }
+
+                // IOB
+                if let iobMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") {
+                    infoManager.updateInfoData(type: .iob, value: iobMetric)
+                    latestIOB = iobMetric
+                }
+
+                // COB
+                if let cobMetric = CarbMetric(from: enacted, key: "COB") {
+                    infoManager.updateInfoData(type: .cob, value: cobMetric)
+                    latestCOB = cobMetric
+                }
+
+                // Insulin Required
+                if let insulinReqMetric = InsulinMetric(from: enacted, key: "insulinReq") {
+                    infoManager.updateInfoData(type: .recBolus, value: insulinReqMetric)
+                    UserDefaultsRepository.deviceRecBolus.value = insulinReqMetric.value
                 } else {
                 } else {
-                    tableData[8].value = "N/A"
                     UserDefaultsRepository.deviceRecBolus.value = 0
                     UserDefaultsRepository.deviceRecBolus.value = 0
                 }
                 }
-                
-                if let autosensdata = lastLoopRecord["enacted"] as? [String: AnyObject] {
-                    let sens = autosensdata["sensitivityRatio"] as! Double * 100.0
-                    tableData[11].value = String(format: "%.0f", sens) + "%"
+
+                // Autosens
+                if let sens = enacted["sensitivityRatio"] as? Double {
+                    let formattedSens = String(format: "%.0f", sens * 100.0) + "%"
+                    infoManager.updateInfoData(type: .autosens, value: formattedSens)
+                }
+
+                // Eventual BG
+                if let eventualBGValue = enacted["eventualBG"] as? Double {
+                    let eventualBGQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: eventualBGValue)
+                    PredictionLabel.text = Localizer.formatQuantity(eventualBGQuantity)
+                }
+
+                // Target
+                let profileTargetHigh = profileManager.currentTargetHigh()
+                var enactedTarget: HKQuantity?
+                if let enactedTargetValue = enacted["current_target"] as? Double {
+                    enactedTarget = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: enactedTargetValue)
                 }
                 }
-                
-                if let eventualdata = lastLoopRecord["enacted"] as? [String: AnyObject] {
-                    if let eventualBGValue = eventualdata["eventualBG"] as? NSNumber {
-                        let eventualBGStringValue = String(describing: eventualBGValue)
-                        PredictionLabel.text = bgUnits.toDisplayUnits(eventualBGStringValue)
+
+                if let profileTargetHigh = profileTargetHigh, let enactedTarget = enactedTarget {
+                    let profileTargetHighFormatted = Localizer.formatQuantity(profileTargetHigh)
+                    let enactedTargetFormatted = Localizer.formatQuantity(enactedTarget)
+
+                    // Compare formatted values to avoid issues with minor floating-point differences
+                    // Profile target could be in another unit than enacted target
+                    if profileTargetHighFormatted != enactedTargetFormatted {
+                        infoManager.updateInfoData(type: .target, firstValue: profileTargetHigh, secondValue: enactedTarget, separator: .arrow)
+                    } else {
+                        infoManager.updateInfoData(type: .target, value: profileTargetHigh)
                     }
                     }
                 }
                 }
-                
-                var predictioncolor = UIColor.systemGray
+
+                // TDD
+                if let tddMetric = InsulinMetric(from: enacted, key: "TDD") {
+                    infoManager.updateInfoData(type: .tdd, value: tddMetric)
+                }
+
+                let predictioncolor = UIColor.systemGray
                 PredictionLabel.textColor = predictioncolor
                 PredictionLabel.textColor = predictioncolor
                 topPredictionBG = UserDefaultsRepository.minBGScale.value
                 topPredictionBG = UserDefaultsRepository.minBGScale.value
-                if let enactdata = lastLoopRecord["enacted"] as? [String: AnyObject],
-                   let predbgdata = enactdata["predBGs"] as? [String: AnyObject] {
+                if let predbgdata = enacted["predBGs"] as? [String: AnyObject] {
                     let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [
                     let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [
                         ("ZT", "ZT", 12),
                         ("ZT", "ZT", 12),
                         ("IOB", "Insulin", 13),
                         ("IOB", "Insulin", 13),
                         ("COB", "LoopYellow", 14),
                         ("COB", "LoopYellow", 14),
                         ("UAM", "UAM", 15)
                         ("UAM", "UAM", 15)
                     ]
                     ]
-                    
+
                     var minPredBG = Double.infinity
                     var minPredBG = Double.infinity
                     var maxPredBG = -Double.infinity
                     var maxPredBG = -Double.infinity
-                    
+
                     for (type, colorName, dataIndex) in predictionTypes {
                     for (type, colorName, dataIndex) in predictionTypes {
                         var predictionData = [ShareGlucoseData]()
                         var predictionData = [ShareGlucoseData]()
                         if let graphdata = predbgdata[type] as? [Double] {
                         if let graphdata = predbgdata[type] as? [Double] {
                             var predictionTime = lastLoopTime
                             var predictionTime = lastLoopTime
                             let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
                             let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
-                            
+
                             for i in 0...toLoad {
                             for i in 0...toLoad {
                                 if i < graphdata.count {
                                 if i < graphdata.count {
                                     let predictionValue = graphdata[i]
                                     let predictionValue = graphdata[i]
                                     minPredBG = min(minPredBG, predictionValue)
                                     minPredBG = min(minPredBG, predictionValue)
                                     maxPredBG = max(maxPredBG, predictionValue)
                                     maxPredBG = max(maxPredBG, predictionValue)
-                                    
+
                                     let prediction = ShareGlucoseData(sgv: Int(round(predictionValue)), date: predictionTime, direction: "flat")
                                     let prediction = ShareGlucoseData(sgv: Int(round(predictionValue)), date: predictionTime, direction: "flat")
                                     predictionData.append(prediction)
                                     predictionData.append(prediction)
                                     predictionTime += 300
                                     predictionTime += 300
                                 }
                                 }
                             }
                             }
                         }
                         }
-                        
+
                         let color = UIColor(named: colorName) ?? UIColor.systemPurple
                         let color = UIColor(named: colorName) ?? UIColor.systemPurple
                         updatePredictionGraphGeneric(
                         updatePredictionGraphGeneric(
                             dataIndex: dataIndex,
                             dataIndex: dataIndex,
@@ -95,14 +169,15 @@ extension MainViewController {
                             color: color
                             color: color
                         )
                         )
                     }
                     }
-                    
+
                     if minPredBG != Double.infinity && maxPredBG != -Double.infinity {
                     if minPredBG != Double.infinity && maxPredBG != -Double.infinity {
-                        tableData[9].value = "\(bgUnits.toDisplayUnits(String(minPredBG)))/\(bgUnits.toDisplayUnits(String(maxPredBG)))"
+                        let value = "\(Localizer.toDisplayUnits(String(minPredBG)))/\(Localizer.toDisplayUnits(String(maxPredBG)))"
+                        infoManager.updateInfoData(type: .minMax, value: value)
                     } else {
                     } else {
-                        tableData[9].value = "N/A"
+                        infoManager.updateInfoData(type: .minMax, value: "N/A")
                     }
                     }
                 }
                 }
-                
+
                 if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String: AnyObject] {
                 if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String: AnyObject] {
                     if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
                     if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
                         var lastBGTime = lastLoopTime
                         var lastBGTime = lastLoopTime

+ 50 - 0
LoopFollow/Controllers/Nightscout/NSProfile.swift

@@ -0,0 +1,50 @@
+// NSProfile.swift
+// LoopFollow
+// Created by Jonas Björkert on 2024-07-12.
+// Copyright © 2024 Jon Fawcett. All rights reserved.
+
+import Foundation
+
+struct NSProfile: Decodable {
+    struct Store: Decodable {
+        struct BasalEntry: Decodable {
+            let value: Double
+            let time: String
+            let timeAsSeconds: Double
+        }
+        struct SensEntry: Decodable {
+            let value: Double
+            let time: String
+            let timeAsSeconds: Double
+        }
+        struct CarbRatioEntry: Decodable {
+            let value: Double
+            let time: String
+            let timeAsSeconds: Double
+        }
+        struct OverrideEntry: Decodable {
+            let name: String?
+            let targetRange: [Double]?
+            let duration: Int?
+            let insulinNeedsScaleFactor: Double?
+            let symbol: String?
+        }
+        struct TargetEntry: Decodable {
+            let value: Double
+            let time: String
+            let timeAsSeconds: Double
+        }
+
+        let basal: [BasalEntry]
+        let sens: [SensEntry]
+        let carbratio: [CarbRatioEntry]
+        let overrides: [OverrideEntry]?
+        let target_high: [TargetEntry]?
+        let target_low: [TargetEntry]?
+        let timezone: String
+    }
+
+    let store: [String: Store]
+    let defaultProfile: String
+    let units: String
+}

+ 3 - 4
LoopFollow/Controllers/Nightscout/Profile.swift

@@ -27,10 +27,9 @@ extension MainViewController {
         guard let store = profileData.store["default"] ?? profileData.store["Default"] else {
         guard let store = profileData.store["default"] ?? profileData.store["Default"] else {
             return
             return
         }
         }
-                
-        tableData[12].value = profileData.defaultProfile
-        infoTable.reloadData()
-
+        profileManager.loadProfile(from: profileData)
+        infoManager.updateInfoData(type: .profile, value: profileData.defaultProfile)
+        
         basalProfile.removeAll()
         basalProfile.removeAll()
         for basalEntry in store.basal {
         for basalEntry in store.basal {
             let entry = basalProfileStruct(value: basalEntry.value, time: basalEntry.time, timeAsSeconds: basalEntry.timeAsSeconds)
             let entry = basalProfileStruct(value: basalEntry.value, time: basalEntry.time, timeAsSeconds: basalEntry.timeAsSeconds)

+ 120 - 0
LoopFollow/Controllers/Nightscout/ProfileManager.swift

@@ -0,0 +1,120 @@
+// ProfileManager.swift
+// LoopFollow
+// Created by Jonas Björkert on 2024-07-12.
+// Copyright © 2024 Jon Fawcett. All rights reserved.
+
+import Foundation
+import HealthKit
+
+struct ProfileManager {
+    var isfSchedule: [TimeValue<HKQuantity>]
+    var basalSchedule: [TimeValue<Double>]
+    var carbRatioSchedule: [TimeValue<Double>]
+    var targetLowSchedule: [TimeValue<HKQuantity>]
+    var targetHighSchedule: [TimeValue<HKQuantity>]
+    var overrides: [Override]
+    var units: HKUnit
+    var timezone: String
+    var defaultProfile: String
+
+    struct TimeValue<T> {
+        let timeAsSeconds: Int
+        let value: T
+    }
+
+    struct Override {
+        let name: String
+        let targetRange: [HKQuantity]
+        let duration: Int
+        let insulinNeedsScaleFactor: Double
+        let symbol: String
+    }
+
+    init() {
+        self.isfSchedule = []
+        self.basalSchedule = []
+        self.carbRatioSchedule = []
+        self.targetLowSchedule = []
+        self.targetHighSchedule = []
+        self.overrides = []
+        self.units = .millimolesPerLiter
+        self.timezone = "UTC"
+        self.defaultProfile = ""
+    }
+
+    mutating func loadProfile(from profileData: NSProfile) {
+        guard let store = profileData.store["default"] ?? profileData.store["Default"] else {
+            return
+        }
+
+        self.units = profileData.units.lowercased() == "mg/dl" ? .milligramsPerDeciliter : .millimolesPerLiter
+        self.timezone = store.timezone
+        self.defaultProfile = profileData.defaultProfile
+
+        self.isfSchedule = store.sens.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) }
+        self.basalSchedule = store.basal.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) }
+        self.carbRatioSchedule = store.carbratio.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) }
+        self.targetLowSchedule = store.target_low?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? []
+        self.targetHighSchedule = store.target_high?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? []
+        if let overrides = store.overrides {
+            self.overrides = overrides.map { Override(name: $0.name ?? "", targetRange: $0.targetRange?.map { HKQuantity(unit: self.units, doubleValue: $0) } ?? [], duration: $0.duration ?? 0, insulinNeedsScaleFactor: $0.insulinNeedsScaleFactor ?? 1.0, symbol: $0.symbol ?? "") }
+        } else {
+            self.overrides = []
+        }
+    }
+
+    func currentISF() -> HKQuantity? {
+        return getCurrentValue(from: isfSchedule)
+    }
+    
+    func currentBasal() -> String? {
+        if let basal = getCurrentValue(from: basalSchedule) {
+            return Localizer.formatToLocalizedString(basal, maxFractionDigits: 2, minFractionDigits: 0)
+        }
+        return nil
+    }
+
+    func currentCarbRatio() -> Double? {
+        return getCurrentValue(from: carbRatioSchedule)
+    }
+
+    func currentTargetLow() -> HKQuantity? {
+        return getCurrentValue(from: targetLowSchedule)
+    }
+
+    func currentTargetHigh() -> HKQuantity? {
+        return getCurrentValue(from: targetHighSchedule)
+    }
+
+    private func getCurrentValue<T>(from schedule: [TimeValue<T>]) -> T? {
+        guard !schedule.isEmpty else { return nil }
+
+        let now = Date()
+        let calendar = Calendar.current
+        let currentTimeInSeconds = calendar.component(.hour, from: now) * 3600 +
+        calendar.component(.minute, from: now) * 60 +
+        calendar.component(.second, from: now)
+
+        var lastValue: T?
+        for timeValue in schedule {
+            if currentTimeInSeconds >= timeValue.timeAsSeconds {
+                lastValue = timeValue.value
+            } else {
+                break
+            }
+        }
+        return lastValue
+    }
+
+    mutating func clear() {
+        self.isfSchedule = []
+        self.basalSchedule = []
+        self.carbRatioSchedule = []
+        self.targetLowSchedule = []
+        self.targetHighSchedule = []
+        self.overrides = []
+        self.units = HKUnit.millimolesPerLiter
+        self.timezone = "UTC"
+        self.defaultProfile = ""
+    }
+}

+ 5 - 4
LoopFollow/Controllers/Nightscout/SAge.swift

@@ -34,7 +34,8 @@ extension MainViewController {
     
     
     // NS Sage Response Processor
     // NS Sage Response Processor
     func updateSage(data: [sageData]) {
     func updateSage(data: [sageData]) {
-        self.clearLastInfoData(index: 6)
+        infoManager.clearInfoData(type: .sage)
+
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process/Display: SAGE") }
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process/Display: SAGE") }
         if data.count == 0 {
         if data.count == 0 {
             return
             return
@@ -68,9 +69,9 @@ extension MainViewController {
             formatter.allowedUnits = [ .day, .hour] // Units to display in the formatted string
             formatter.allowedUnits = [ .day, .hour] // Units to display in the formatted string
             formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
             formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
             
             
-            let formattedDuration = formatter.string(from: secondsAgo)
-            tableData[6].value = formattedDuration ?? ""
+            if let formattedDuration = formatter.string(from: secondsAgo) {
+                infoManager.updateInfoData(type: .sage, value: formattedDuration)
+            }
         }
         }
-        infoTable.reloadData()
     }
     }
 }
 }

+ 10 - 8
LoopFollow/Controllers/Nightscout/Treatments/Basals.swift

@@ -10,7 +10,8 @@ import Foundation
 extension MainViewController {
 extension MainViewController {
     // NS Temp Basal Response Processor
     // NS Temp Basal Response Processor
     func processNSBasals(entries: [[String:AnyObject]]) {
     func processNSBasals(entries: [[String:AnyObject]]) {
-        self.clearLastInfoData(index: 2)
+        infoManager.clearInfoData(type: .basal)
+
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Basal") }
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Basal") }
         // due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being.
         // due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being.
         basalData.removeAll()
         basalData.removeAll()
@@ -136,12 +137,11 @@ extension MainViewController {
             //if i == tempArray.count - 1 && dateTimeStamp + duration <= dateTimeUtils.getNowTimeIntervalUTC() {
             //if i == tempArray.count - 1 && dateTimeStamp + duration <= dateTimeUtils.getNowTimeIntervalUTC() {
             if i == tempArray.count - 1 && duration == 0.0 {
             if i == tempArray.count - 1 && duration == 0.0 {
                 lastEndDot = dateTimeStamp + (30 * 60)
                 lastEndDot = dateTimeStamp + (30 * 60)
-                latestBasal = String(format:"%.2f", basalRate)
             } else {
             } else {
                 lastEndDot = dateTimeStamp + (duration * 60)
                 lastEndDot = dateTimeStamp + (duration * 60)
-                latestBasal = String(format:"%.2f", basalRate)
             }
             }
-            
+            latestBasal = Localizer.formatToLocalizedString(basalRate, maxFractionDigits: 2, minFractionDigits: 0)
+
             // Double check for overlaps of incorrectly ended TBRs and sent it to end when the next one starts if it finds a discrepancy
             // Double check for overlaps of incorrectly ended TBRs and sent it to end when the next one starts if it finds a discrepancy
             if i < tempArray.count - 1 {
             if i < tempArray.count - 1 {
                 let nextEntry = tempArray[i + 1] as [String : AnyObject]?
                 let nextEntry = tempArray[i + 1] as [String : AnyObject]?
@@ -186,7 +186,7 @@ extension MainViewController {
                 }
                 }
             }
             }
             
             
-            latestBasal = String(format:"%.2f", scheduled)
+            latestBasal = Localizer.formatToLocalizedString(scheduled, maxFractionDigits: 2, minFractionDigits: 0)
             // Make the starting dot at the last ending dot
             // Make the starting dot at the last ending dot
             let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))
             let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))
             basalData.append(startDot)
             basalData.append(startDot)
@@ -196,11 +196,13 @@ extension MainViewController {
             basalData.append(endDot)
             basalData.append(endDot)
             
             
         }
         }
-        tableData[2].value = latestBasal
-        infoTable.reloadData()
         if UserDefaultsRepository.graphBasal.value {
         if UserDefaultsRepository.graphBasal.value {
             updateBasalGraph()
             updateBasalGraph()
         }
         }
-        infoTable.reloadData()
+
+        if let profileBasal = profileManager.currentBasal(), profileBasal != latestBasal {
+            latestBasal = "\(profileBasal) → \(latestBasal)"
+        }
+        infoManager.updateInfoData(type: .basal, value: latestBasal)
     }
     }
 }
 }

+ 1 - 2
LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift

@@ -88,7 +88,6 @@ extension MainViewController {
         }
         }
         
         
         let resultString = String(format: "%.0f", totalCarbs)
         let resultString = String(format: "%.0f", totalCarbs)
-        tableData[10].value = resultString
-        infoTable.reloadData()
+        infoManager.updateInfoData(type: .carbsToday, value: resultString)
     }
     }
 }
 }

+ 1 - 1
LoopFollow/Controllers/StatsView.swift

@@ -33,7 +33,7 @@ extension MainViewController {
             statsLowPercent.text = String(format:"%.1f%", stats.percentLow) + "%"
             statsLowPercent.text = String(format:"%.1f%", stats.percentLow) + "%"
             statsInRangePercent.text = String(format:"%.1f%", stats.percentRange) + "%"
             statsInRangePercent.text = String(format:"%.1f%", stats.percentRange) + "%"
             statsHighPercent.text = String(format:"%.1f%", stats.percentHigh) + "%"
             statsHighPercent.text = String(format:"%.1f%", stats.percentHigh) + "%"
-            statsAvgBG.text = bgUnits.toDisplayUnits(String(format:"%.0f%", stats.avgBG))
+            statsAvgBG.text = Localizer.toDisplayUnits(String(format:"%.0f%", stats.avgBG))
             if UserDefaultsRepository.useIFCC.value {
             if UserDefaultsRepository.useIFCC.value {
                 statsEstA1C.text = String(format:"%.0f%", stats.a1C)
                 statsEstA1C.text = String(format:"%.0f%", stats.a1C)
             }
             }

+ 20 - 0
LoopFollow/Extensions/HKUnit+Extensions.swift

@@ -0,0 +1,20 @@
+//
+//  HKUnit+Extensions.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-07-15.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+extension HKUnit {
+    public static let milligramsPerDeciliter: HKUnit = {
+        return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci))
+    }()
+
+    public static let millimolesPerLiter: HKUnit = {
+        return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
+    }()
+}

+ 19 - 0
LoopFollow/InfoTable/InfoData.swift

@@ -0,0 +1,19 @@
+//
+//  InfoData.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-07-11.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+class InfoData {
+    var name: String
+    var value: String
+
+    init(name: String, value: String = "") {
+        self.name = name
+        self.value = value
+    }
+}

+ 15 - 0
LoopFollow/InfoTable/InfoDataSeparator.swift

@@ -0,0 +1,15 @@
+//
+//  InfoDataSeparator.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-07-16.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+enum InfoDataSeparator: String {
+    case arrow = "→"
+    case slash = "/"
+    case dash = "-"
+}

+ 78 - 0
LoopFollow/InfoTable/InfoManager.swift

@@ -0,0 +1,78 @@
+//
+//  InfoManager.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-07-11.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+import UIKit
+import HealthKit
+
+class InfoManager {
+    var tableData: [InfoData]
+    weak var tableView: UITableView?
+
+    init(tableView: UITableView) {
+        self.tableData = InfoType.allCases.map { InfoData(name: $0.name) }
+        self.tableView = tableView
+    }
+
+    func updateInfoData(type: InfoType, value: String) {
+        tableData[type.rawValue].value = value
+        tableView?.reloadData()
+    }
+
+    func updateInfoData(type: InfoType, value: HKQuantity) {
+        let formattedValue = Localizer.formatQuantity(value)
+        updateInfoData(type: type, value: formattedValue)
+    }
+
+    func updateInfoData(type: InfoType, firstValue: HKQuantity, secondValue: HKQuantity, separator: InfoDataSeparator) {
+        let formattedFirstValue = Localizer.formatQuantity(firstValue)
+        let formattedSecondValue = Localizer.formatQuantity(secondValue)
+        let combinedValue = "\(formattedFirstValue) \(separator.rawValue) \(formattedSecondValue)"
+        updateInfoData(type: type, value: combinedValue)
+    }
+
+    func updateInfoData(type: InfoType, value: Double, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) {
+        let formattedValue = Localizer.formatToLocalizedString(value, maxFractionDigits: maxFractionDigits, minFractionDigits: minFractionDigits)
+        updateInfoData(type: type, value: formattedValue)
+    }
+
+    func updateInfoData(type: InfoType, value: Double, enactedValue: Double, separator: InfoDataSeparator, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) {
+        let formattedValue = Localizer.formatToLocalizedString(value, maxFractionDigits: maxFractionDigits, minFractionDigits: minFractionDigits)
+        let formattedEnactedValue = Localizer.formatToLocalizedString(enactedValue, maxFractionDigits: maxFractionDigits, minFractionDigits: minFractionDigits)
+        let separatorString = separator.rawValue
+        let combinedValue = "\(formattedValue) \(separatorString) \(formattedEnactedValue)"
+        updateInfoData(type: type, value: combinedValue)
+    }
+
+    func updateInfoData(type: InfoType, value: Metric) {
+        let formattedValue = value.formattedValue()
+        updateInfoData(type: type, value: formattedValue)
+    }
+    
+    func clearInfoData(type: InfoType) {
+        tableData[type.rawValue].value = ""
+        tableView?.reloadData()
+    }
+
+    func clearInfoData(types: [InfoType]) {
+        for type in types {
+            tableData[type.rawValue].value = ""
+        }
+        tableView?.reloadData()
+    }
+
+    func numberOfRows() -> Int {
+        return UserDefaultsRepository.infoSort.value.filter { UserDefaultsRepository.infoVisible.value[$0] }.count
+    }
+
+    func dataForIndexPath(_ indexPath: IndexPath) -> InfoData {
+        let sortedAndVisibleIndexes = UserDefaultsRepository.infoSort.value.filter { UserDefaultsRepository.infoVisible.value[$0] }
+        let infoIndex = sortedAndVisibleIndexes[indexPath.row]
+        return tableData[infoIndex]
+    }
+}

+ 49 - 0
LoopFollow/InfoTable/InfoType.swift

@@ -0,0 +1,49 @@
+//
+//  InfoType.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-07-11.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+enum InfoType: Int, CaseIterable {
+    case iob, cob, basal, override, battery, pump, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio, updated, tdd
+
+    var name: String {
+        switch self {
+        case .iob: return "IOB"
+        case .cob: return "COB"
+        case .basal: return "Basal"
+        case .override: return "Override"
+        case .battery: return "Battery"
+        case .pump: return "Pump"
+        case .sage: return "SAGE"
+        case .cage: return "CAGE"
+        case .recBolus: return "Rec. Bolus"
+        case .minMax: return "Min/Max"
+        case .carbsToday: return "Carbs today"
+        case .autosens: return "Autosens"
+        case .profile: return "Profile"
+        case .target: return "Target"
+        case .isf: return "ISF"
+        case .carbRatio: return "CR"
+        case .updated: return "Updated"
+        case .tdd: return "TDD"
+        }
+    }
+
+    var defaultVisible: Bool {
+        switch self {
+        case .iob, .cob, .basal, .override, .battery, .pump, .sage, .cage, .recBolus, .minMax, .carbsToday:
+            return true
+        default:
+            return false
+        }
+    }
+
+    var sortOrder: Int {
+        return self.rawValue
+    }
+}

+ 25 - 0
LoopFollow/Metric/CarbMetric.swift

@@ -0,0 +1,25 @@
+//
+//  CarbMetric.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-07-17.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+class CarbMetric: Metric {
+    init?(from dictionary: [String: AnyObject], key: String) {
+        guard let value = dictionary[key] as? Double else {
+            return nil
+        }
+        super.init(value: value, maxFractionDigits: 0, minFractionDigits: 0)
+    }
+
+    init?(from object: AnyObject?, key: String) {
+        guard let dictionary = object as? [String: AnyObject], let value = dictionary[key] as? Double else {
+            return nil
+        }
+        super.init(value: value, maxFractionDigits: 0, minFractionDigits: 0)
+    }
+}

+ 25 - 0
LoopFollow/Metric/InsulinMetric.swift

@@ -0,0 +1,25 @@
+//
+//  InsulinMetric.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-07-17.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+class InsulinMetric: Metric {
+    init?(from dictionary: [String: AnyObject], key: String) {
+        guard let value = dictionary[key] as? Double else {
+            return nil
+        }
+        super.init(value: value, maxFractionDigits: 2, minFractionDigits: 0)
+    }
+
+    init?(from object: AnyObject?, key: String) {
+        guard let dictionary = object as? [String: AnyObject], let value = dictionary[key] as? Double else {
+            return nil
+        }
+        super.init(value: value, maxFractionDigits: 2, minFractionDigits: 0)
+    }
+}

+ 25 - 0
LoopFollow/Metric/Metric.swift

@@ -0,0 +1,25 @@
+//
+//  Metric.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-07-17.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+class Metric {
+    var value: Double
+    var maxFractionDigits: Int
+    var minFractionDigits: Int
+
+    init(value: Double, maxFractionDigits: Int, minFractionDigits: Int) {
+        self.value = value
+        self.maxFractionDigits = maxFractionDigits
+        self.minFractionDigits = minFractionDigits
+    }
+
+    func formattedValue() -> String {
+        return Localizer.formatToLocalizedString(value, maxFractionDigits: maxFractionDigits, minFractionDigits: minFractionDigits)
+    }
+}

+ 13 - 13
LoopFollow/ViewControllers/AlarmViewController.swift

@@ -605,7 +605,7 @@ class AlarmViewController: FormViewController {
                 row.value = Double(UserDefaultsRepository.alertTemporaryBG.value)
                 row.value = Double(UserDefaultsRepository.alertTemporaryBG.value)
                 row.displayValueFor = { value in
                 row.displayValueFor = { value in
                     guard let value = value else { return nil }
                     guard let value = value else { return nil }
-                    return bgUnits.toDisplayUnits(String(value))
+                    return Localizer.toDisplayUnits(String(value))
                 }
                 }
             }.onChange { [weak self] row in
             }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -655,7 +655,7 @@ class AlarmViewController: FormViewController {
             row.value = Double(UserDefaultsRepository.alertUrgentLowBG.value)
             row.value = Double(UserDefaultsRepository.alertUrgentLowBG.value)
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                 guard let value = value else { return nil }
                 guard let value = value else { return nil }
-                return bgUnits.toDisplayUnits(String(value))
+                return Localizer.toDisplayUnits(String(value))
             }
             }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -815,7 +815,7 @@ class AlarmViewController: FormViewController {
             row.value = Double(UserDefaultsRepository.alertLowBG.value)
             row.value = Double(UserDefaultsRepository.alertLowBG.value)
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                 guard let value = value else { return nil }
                 guard let value = value else { return nil }
-                return bgUnits.toDisplayUnits(String(value))
+                return Localizer.toDisplayUnits(String(value))
             }
             }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -843,7 +843,7 @@ class AlarmViewController: FormViewController {
                 row.value = Double(UserDefaultsRepository.alertLowPersistenceMax.value)
                 row.value = Double(UserDefaultsRepository.alertLowPersistenceMax.value)
                 row.displayValueFor = { value in
                 row.displayValueFor = { value in
                     guard let value = value else { return nil }
                     guard let value = value else { return nil }
-                    return bgUnits.toDisplayUnits(String(value))
+                    return Localizer.toDisplayUnits(String(value))
                 }
                 }
             }.onChange { [weak self] row in
             }.onChange { [weak self] row in
                     guard let value = row.value else { return }
                     guard let value = row.value else { return }
@@ -992,7 +992,7 @@ class AlarmViewController: FormViewController {
             row.value = Double(UserDefaultsRepository.alertHighBG.value)
             row.value = Double(UserDefaultsRepository.alertHighBG.value)
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                 guard let value = value else { return nil }
                 guard let value = value else { return nil }
-                return bgUnits.toDisplayUnits(String(value))
+                return Localizer.toDisplayUnits(String(value))
             }
             }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -1151,7 +1151,7 @@ class AlarmViewController: FormViewController {
             row.value = Double(UserDefaultsRepository.alertUrgentHighBG.value)
             row.value = Double(UserDefaultsRepository.alertUrgentHighBG.value)
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                 guard let value = value else { return nil }
                 guard let value = value else { return nil }
-                return bgUnits.toDisplayUnits(String(value))
+                return Localizer.toDisplayUnits(String(value))
             }
             }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -1295,7 +1295,7 @@ class AlarmViewController: FormViewController {
             row.value = Double(UserDefaultsRepository.alertFastDropDelta.value)
             row.value = Double(UserDefaultsRepository.alertFastDropDelta.value)
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                 guard let value = value else { return nil }
                 guard let value = value else { return nil }
-                return bgUnits.toDisplayUnits(String(value))
+                return Localizer.toDisplayUnits(String(value))
             }
             }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -1332,7 +1332,7 @@ class AlarmViewController: FormViewController {
             row.hidden = "$alertFastDropUseLimit == false"
             row.hidden = "$alertFastDropUseLimit == false"
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                     guard let value = value else { return nil }
                     guard let value = value else { return nil }
-                    return bgUnits.toDisplayUnits(String(value))
+                    return Localizer.toDisplayUnits(String(value))
                 }
                 }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -1477,7 +1477,7 @@ class AlarmViewController: FormViewController {
             row.value = Double(UserDefaultsRepository.alertFastRiseDelta.value)
             row.value = Double(UserDefaultsRepository.alertFastRiseDelta.value)
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                 guard let value = value else { return nil }
                 guard let value = value else { return nil }
-                return bgUnits.toDisplayUnits(String(value))
+                return Localizer.toDisplayUnits(String(value))
             }
             }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -1514,7 +1514,7 @@ class AlarmViewController: FormViewController {
             row.hidden = "$alertFastRiseUseLimit == false"
             row.hidden = "$alertFastRiseUseLimit == false"
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                     guard let value = value else { return nil }
                     guard let value = value else { return nil }
-                    return bgUnits.toDisplayUnits(String(value))
+                    return Localizer.toDisplayUnits(String(value))
                 }
                 }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -1827,7 +1827,7 @@ class AlarmViewController: FormViewController {
             row.hidden = "$alertNotLoopingUseLimits == false"
             row.hidden = "$alertNotLoopingUseLimits == false"
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                     guard let value = value else { return nil }
                     guard let value = value else { return nil }
-                    return bgUnits.toDisplayUnits(String(value))
+                    return Localizer.toDisplayUnits(String(value))
                 }
                 }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -1842,7 +1842,7 @@ class AlarmViewController: FormViewController {
             row.hidden = "$alertNotLoopingUseLimits == false"
             row.hidden = "$alertNotLoopingUseLimits == false"
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                     guard let value = value else { return nil }
                     guard let value = value else { return nil }
-                    return bgUnits.toDisplayUnits(String(value))
+                    return Localizer.toDisplayUnits(String(value))
                 }
                 }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -2041,7 +2041,7 @@ class AlarmViewController: FormViewController {
             row.value = Double(UserDefaultsRepository.alertMissedBolusLowGramsBG.value)
             row.value = Double(UserDefaultsRepository.alertMissedBolusLowGramsBG.value)
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                     guard let value = value else { return nil }
                     guard let value = value else { return nil }
-                    return bgUnits.toDisplayUnits(String(value))
+                    return Localizer.toDisplayUnits(String(value))
                 }
                 }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }

+ 3 - 3
LoopFollow/ViewControllers/GeneralSettingsViewController.swift

@@ -220,7 +220,7 @@ class GeneralSettingsViewController: FormViewController {
            row.value = Double(UserDefaultsRepository.speakLowBGLimit.value)
            row.value = Double(UserDefaultsRepository.speakLowBGLimit.value)
            row.displayValueFor = { value in
            row.displayValueFor = { value in
                guard let value = value else { return nil }
                guard let value = value else { return nil }
-               return bgUnits.toDisplayUnits(String(value))
+               return Localizer.toDisplayUnits(String(value))
            }
            }
            // Visibility depends on either 'speakLowBG' or 'speakProactiveLowBG' being true
            // Visibility depends on either 'speakLowBG' or 'speakProactiveLowBG' being true
            row.hidden = Condition.function(["speakLowBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in
            row.hidden = Condition.function(["speakLowBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in
@@ -243,7 +243,7 @@ class GeneralSettingsViewController: FormViewController {
            row.value = Double(UserDefaultsRepository.speakFastDropDelta.value)
            row.value = Double(UserDefaultsRepository.speakFastDropDelta.value)
            row.displayValueFor = { value in
            row.displayValueFor = { value in
                guard let value = value else { return nil }
                guard let value = value else { return nil }
-               return bgUnits.toDisplayUnits(String(value))
+               return Localizer.toDisplayUnits(String(value))
            }
            }
            // Visibility depends on 'speakProactiveLowBG' being true
            // Visibility depends on 'speakProactiveLowBG' being true
            row.hidden = Condition.function(["speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in
            row.hidden = Condition.function(["speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in
@@ -273,7 +273,7 @@ class GeneralSettingsViewController: FormViewController {
            row.value = Double(UserDefaultsRepository.speakHighBGLimit.value)
            row.value = Double(UserDefaultsRepository.speakHighBGLimit.value)
            row.displayValueFor = { value in
            row.displayValueFor = { value in
                guard let value = value else { return nil }
                guard let value = value else { return nil }
-               return bgUnits.toDisplayUnits(String(value))
+               return Localizer.toDisplayUnits(String(value))
            }
            }
            // Visibility depends on 'speakHighBG' or 'speakProactiveLowBG' being true
            // Visibility depends on 'speakHighBG' or 'speakProactiveLowBG' being true
            row.hidden = Condition.function(["speakHighBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in
            row.hidden = Condition.function(["speakHighBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in

+ 3 - 3
LoopFollow/ViewControllers/GraphSettingsViewController.swift

@@ -204,7 +204,7 @@ class GraphSettingsViewController: FormViewController {
             row.value = Double(UserDefaultsRepository.minBGScale.value)
             row.value = Double(UserDefaultsRepository.minBGScale.value)
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                 guard let value = value else { return nil }
                 guard let value = value else { return nil }
-                return bgUnits.toDisplayUnits(String(value))
+                return Localizer.toDisplayUnits(String(value))
                 
                 
             }
             }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
@@ -231,7 +231,7 @@ class GraphSettingsViewController: FormViewController {
             row.value = Double(UserDefaultsRepository.lowLine.value)
             row.value = Double(UserDefaultsRepository.lowLine.value)
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                 guard let value = value else { return nil }
                 guard let value = value else { return nil }
-                return bgUnits.toDisplayUnits(String(value))
+                return Localizer.toDisplayUnits(String(value))
             }
             }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }
@@ -254,7 +254,7 @@ class GraphSettingsViewController: FormViewController {
             row.value = Double(UserDefaultsRepository.highLine.value)
             row.value = Double(UserDefaultsRepository.highLine.value)
             row.displayValueFor = { value in
             row.displayValueFor = { value in
                 guard let value = value else { return nil }
                 guard let value = value else { return nil }
-                return bgUnits.toDisplayUnits(String(value))
+                return Localizer.toDisplayUnits(String(value))
             }
             }
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
                 guard let value = row.value else { return }
                 guard let value = row.value else { return }

+ 51 - 54
LoopFollow/ViewControllers/InfoDisplaySettingsViewController.swift

@@ -14,17 +14,16 @@ import EventKitUI
 class InfoDisplaySettingsViewController: FormViewController {
 class InfoDisplaySettingsViewController: FormViewController {
     var appStateController: AppStateController?
     var appStateController: AppStateController?
 
 
-    
     override func viewDidLoad() {
     override func viewDidLoad() {
         print("Display Load")
         print("Display Load")
         super.viewDidLoad()
         super.viewDidLoad()
         if UserDefaultsRepository.forceDarkMode.value {
         if UserDefaultsRepository.forceDarkMode.value {
             overrideUserInterfaceStyle = .dark
             overrideUserInterfaceStyle = .dark
         }
         }
-         
+
         createForm()
         createForm()
     }
     }
-    
+
     private func createForm() {
     private func createForm() {
         form
         form
         +++ Section("General")
         +++ Section("General")
@@ -33,64 +32,62 @@ class InfoDisplaySettingsViewController: FormViewController {
             row.tag = "hideInfoTable"
             row.tag = "hideInfoTable"
             row.value = UserDefaultsRepository.hideInfoTable.value
             row.value = UserDefaultsRepository.hideInfoTable.value
         }.onChange { [weak self] row in
         }.onChange { [weak self] row in
-                    guard let value = row.value else { return }
-                    UserDefaultsRepository.hideInfoTable.value = value           
+            guard let value = row.value else { return }
+            UserDefaultsRepository.hideInfoTable.value = value
+        }
+
+        +++ MultivaluedSection(multivaluedOptions: .Reorder, header: "Information Display Settings", footer: "Arrange/Enable Information Desired") {
+
+            $0.tag = "InfoDisplay"
+
+            for i in 0..<UserDefaultsRepository.infoSort.value.count {
+                let sortedIndex = UserDefaultsRepository.infoSort.value[i]
+                let infoType = InfoType(rawValue: sortedIndex)!
+
+                $0 <<< TextRow() { row in
+                    if(UserDefaultsRepository.infoVisible.value[sortedIndex]) {
+                        row.title = "\u{2713}\t\(infoType.name)"
+                    } else {
+                        row.title = "\u{2001}\t\(infoType.name)"
+                    }
+                }.onCellSelection { (cell, row) in
+                    let i = row.indexPath!.row
+                    let sortedIndex = UserDefaultsRepository.infoSort.value[i]
+                    UserDefaultsRepository.infoVisible.value[sortedIndex] = !UserDefaultsRepository.infoVisible.value[sortedIndex]
+
+                    self.tableView.reloadData()
+                }.cellSetup { (cell, row) in
+                    cell.textField.isUserInteractionEnabled = false
+                }.cellUpdate { (cell, row) in
+                    let sortedIndex = UserDefaultsRepository.infoSort.value[i]
+                    if(UserDefaultsRepository.infoVisible.value[sortedIndex]) {
+                        row.title = "\u{2713}\t\(infoType.name)"
+                    } else {
+                        row.title = "\u{2001}\t\(infoType.name)"
+                    }
+                    self.appStateController!.infoDataSettingsChanged = true
+                }
+            }
+        }
+
+        +++ ButtonRow() {
+            $0.title = "DONE"
+        }.onCellSelection { (row, arg)  in
+            self.dismiss(animated: true, completion: nil)
         }
         }
-        
-        +++ MultivaluedSection(multivaluedOptions: .Reorder, header: "Information Display Settings", footer: "Arrage/Enable Information Desired") {
-        
-           // TODO: add the other display values
-           $0.tag = "InfoDisplay"
-           
-            for i in 0..<UserDefaultsRepository.infoNames.value.count {
-              $0 <<< TextRow() { row in
-                if(UserDefaultsRepository.infoVisible.value[UserDefaultsRepository.infoSort.value[i]]) {
-                    row.title = "\u{2713}\t\(UserDefaultsRepository.infoNames.value[UserDefaultsRepository.infoSort.value[i]])"
-                 } else {
-                    row.title = "\u{2001}\t\(UserDefaultsRepository.infoNames.value[UserDefaultsRepository.infoSort.value[i]])"
-                 }
-              }.onCellSelection{(cell, row) in
-                let i = row.indexPath!.row
-                UserDefaultsRepository.infoVisible.value[UserDefaultsRepository.infoSort.value[i]] = !UserDefaultsRepository.infoVisible.value[UserDefaultsRepository.infoSort.value[i]]
-                
-                self.tableView.reloadData()
-                
-                //print("\(row.title)")
-                //print("\(row.indexPath?.row)")
-              }.cellSetup { (cell, row) in
-                 cell.textField.isUserInteractionEnabled = false
-              }.cellUpdate{ (cell, row) in
-                if(UserDefaultsRepository.infoVisible.value[UserDefaultsRepository.infoSort.value[i]]) {
-                    row.title = "\u{2713}\t\(UserDefaultsRepository.infoNames.value[UserDefaultsRepository.infoSort.value[i]])"
-                 } else {
-                    row.title = "\u{2001}\t\(UserDefaultsRepository.infoNames.value[UserDefaultsRepository.infoSort.value[i]])"
-                 }
-                 self.appStateController!.infoDataSettingsChanged = true
-              }
-           }
-       }
-    
-       +++ ButtonRow() {
-          $0.title = "DONE"
-       }.onCellSelection { (row, arg)  in
-          self.dismiss(animated:true, completion: nil)
-       }
     }
     }
-    
+
     override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
     override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
-        //let view = tableView
         let sourceIndex = sourceIndexPath.row
         let sourceIndex = sourceIndexPath.row
         let destIndex = destinationIndexPath.row
         let destIndex = destinationIndexPath.row
-        
+
         // new sort
         // new sort
         if(destIndex != sourceIndex ) {
         if(destIndex != sourceIndex ) {
-           self.appStateController!.infoDataSettingsChanged = true
-           
+            self.appStateController!.infoDataSettingsChanged = true
+
             let tmpVal = UserDefaultsRepository.infoSort.value[sourceIndex]
             let tmpVal = UserDefaultsRepository.infoSort.value[sourceIndex]
-            UserDefaultsRepository.infoSort.value.remove(at:sourceIndex)
-            UserDefaultsRepository.infoSort.value.insert(tmpVal, at:destIndex)
-       
+            UserDefaultsRepository.infoSort.value.remove(at: sourceIndex)
+            UserDefaultsRepository.infoSort.value.insert(tmpVal, at: destIndex)
         }
         }
-        
     }
     }
- }
+}

+ 28 - 111
LoopFollow/ViewControllers/MainViewController.swift

@@ -41,16 +41,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
 
 
     let speechSynthesizer = AVSpeechSynthesizer()
     let speechSynthesizer = AVSpeechSynthesizer()
 
 
-    // Data Table class
-    class infoData {
-        public var name: String
-        public var value: String
-        init(name: String, value: String) {
-            self.name = name
-            self.value = value
-        }
-    }
-    
     var appStateController: AppStateController?
     var appStateController: AppStateController?
 
 
     // Variables for BG Charts
     // Variables for BG Charts
@@ -83,7 +73,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     var minAgoTimer = Timer()
     var minAgoTimer = Timer()
     var minAgoTimeInterval: TimeInterval = 1.0
     var minAgoTimeInterval: TimeInterval = 1.0
     
     
-    
     // Check Alarms Timer
     // Check Alarms Timer
     // Don't check within 1 minute of alarm triggering to give the snoozer time to save data
     // Don't check within 1 minute of alarm triggering to give the snoozer time to save data
     var checkAlarmTimer = Timer()
     var checkAlarmTimer = Timer()
@@ -98,11 +87,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     var alarmTimer = Timer()
     var alarmTimer = Timer()
     var calendarTimer = Timer()
     var calendarTimer = Timer()
     var graphNowTimer = Timer()
     var graphNowTimer = Timer()
-    
+
     // Info Table Setup
     // Info Table Setup
-    var tableData : [infoData] = []
-    var derivedTableData: [infoData] = []
-    
+    var infoManager: InfoManager!
+    var profileManager = ProfileManager()
+
     var bgData: [ShareGlucoseData] = []
     var bgData: [ShareGlucoseData] = []
     var basalProfile: [basalProfileStruct] = []
     var basalProfile: [basalProfileStruct] = []
     var basalData: [basalGraphStruct] = []
     var basalData: [basalGraphStruct] = []
@@ -125,10 +114,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     var latestDeltaString = ""
     var latestDeltaString = ""
     var latestLoopStatusString = ""
     var latestLoopStatusString = ""
     var latestLoopTime: Double = 0
     var latestLoopTime: Double = 0
-    var latestCOB = ""
+    var latestCOB: CarbMetric?
     var latestBasal = ""
     var latestBasal = ""
     var latestPumpVolume: Double = 50.0
     var latestPumpVolume: Double = 50.0
-    var latestIOB = ""
+    var latestIOB: InsulinMetric?
     var lastOverrideStartTime: TimeInterval = 0
     var lastOverrideStartTime: TimeInterval = 0
     var lastOverrideEndTime: TimeInterval = 0
     var lastOverrideEndTime: TimeInterval = 0
     
     
@@ -157,54 +146,28 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     override func viewDidLoad() {
     override func viewDidLoad() {
         super.viewDidLoad()
         super.viewDidLoad()
 
 
-        // reset the infoTable names in case we add or delete items
-        UserDefaultsRepository.infoNames.value.removeAll()
-        UserDefaultsRepository.infoNames.value.append("IOB")
-        UserDefaultsRepository.infoNames.value.append("COB")
-        UserDefaultsRepository.infoNames.value.append("Basal")
-        UserDefaultsRepository.infoNames.value.append("Override")
-        UserDefaultsRepository.infoNames.value.append("Battery")
-        UserDefaultsRepository.infoNames.value.append("Pump")
-        UserDefaultsRepository.infoNames.value.append("SAGE")
-        UserDefaultsRepository.infoNames.value.append("CAGE")
-        UserDefaultsRepository.infoNames.value.append("Rec. Bolus")
-        UserDefaultsRepository.infoNames.value.append("Min/Max") //Previously "Pred."
-        UserDefaultsRepository.infoNames.value.append("Carbs today")
-        UserDefaultsRepository.infoNames.value.append("Autosens")
-        UserDefaultsRepository.infoNames.value.append("Profile")
+        // Synchronize info types to ensure arrays are the correct size
+        UserDefaultsRepository.synchronizeInfoTypes()
 
 
         // Reset deprecated settings
         // Reset deprecated settings
         UserDefaultsRepository.debugLog.value = false;
         UserDefaultsRepository.debugLog.value = false;
-        UserDefaultsRepository.alwaysDownloadAllBG.value = true;
         
         
-        // table view
-        //infoTable.layer.borderColor = UIColor.darkGray.cgColor
-        //infoTable.layer.borderWidth = 1.0
-        //infoTable.layer.cornerRadius = 6
         infoTable.rowHeight = 21
         infoTable.rowHeight = 21
         infoTable.dataSource = self
         infoTable.dataSource = self
-        infoTable.tableFooterView = UIView(frame: .zero) // get rid of extra rows
+        infoTable.tableFooterView = UIView(frame: .zero)
         infoTable.bounces = false
         infoTable.bounces = false
         infoTable.addBorder(toSide: .Left, withColor: UIColor.darkGray.cgColor, andThickness: 2)
         infoTable.addBorder(toSide: .Left, withColor: UIColor.darkGray.cgColor, andThickness: 2)
         
         
-        // initialize the tableData
-        self.tableData = []
-        for i in 0..<UserDefaultsRepository.infoNames.value.count {
-            self.tableData.append(infoData(name:UserDefaultsRepository.infoNames.value[i], value:""))
-        }
-        createDerivedData()
-        
+        self.infoManager = InfoManager(tableView: infoTable)
+
         smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value)
         smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value)
         self.view.layoutIfNeeded()
         self.view.layoutIfNeeded()
 
 
-        // TODO: need non-us server ?
         let shareUserName = UserDefaultsRepository.shareUserName.value
         let shareUserName = UserDefaultsRepository.shareUserName.value
         let sharePassword = UserDefaultsRepository.sharePassword.value
         let sharePassword = UserDefaultsRepository.sharePassword.value
         let shareServer = UserDefaultsRepository.shareServer.value == "US" ?KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue
         let shareServer = UserDefaultsRepository.shareServer.value == "US" ?KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue
         dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer )
         dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer )
         
         
-        //print("Share: \(dexShare)")
-        
         // setup show/hide small graph and stats
         // setup show/hide small graph and stats
         BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value
         BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value
         statsView.isHidden = !UserDefaultsRepository.showStats.value
         statsView.isHidden = !UserDefaultsRepository.showStats.value
@@ -216,13 +179,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             overrideUserInterfaceStyle = .dark
             overrideUserInterfaceStyle = .dark
             self.tabBarController?.overrideUserInterfaceStyle = .dark
             self.tabBarController?.overrideUserInterfaceStyle = .dark
         }
         }
-        // Disable the snoozer tab unless an alarm is active
-        //let tabBarControllerItems = self.tabBarController?.tabBar.items
-        //if let arrayOfTabBarItems = tabBarControllerItems as AnyObject as? NSArray{
-        //    snoozeTabItem = arrayOfTabBarItems[2] as! UITabBarItem
-        //}
-        //snoozeTabItem.isEnabled = false;
-        
+
         // Load the snoozer tab
         // Load the snoozer tab
         guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
         guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
         snoozer.loadViewIfNeeded()
         snoozer.loadViewIfNeeded()
@@ -379,7 +336,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
                 appState.generalSettingsChanges = 0
                 appState.generalSettingsChanges = 0
             }
             }
             if appState.infoDataSettingsChanged {
             if appState.infoDataSettingsChanged {
-                createDerivedData()
                 self.infoTable.reloadData()
                 self.infoTable.reloadData()
                 
                 
                 // reset
                 // reset
@@ -389,36 +345,18 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             // add more processing of the app state
             // add more processing of the app state
         }
         }
     }
     }
-    
-    private func createDerivedData() {
-        let currentCount = UserDefaultsRepository.infoSort.value.count
-        if currentCount < self.tableData.count {
-            for i in currentCount..<self.tableData.count {
-                UserDefaultsRepository.infoSort.value.append(i)
-            }
-        }
-        
-        self.derivedTableData = []
-        while UserDefaultsRepository.infoVisible.value.count < self.tableData.count {
-            UserDefaultsRepository.infoVisible.value.append(false)
-        }
-        for i in 0..<self.tableData.count {
-            if(UserDefaultsRepository.infoVisible.value[UserDefaultsRepository.infoSort.value[i]]) {
-                self.derivedTableData.append(self.tableData[UserDefaultsRepository.infoSort.value[i]])
-            }
-        }
-    }
-    
+
     // Info Table Functions
     // Info Table Functions
     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
-        //return tableData.count
-        return derivedTableData.count
-        
+        guard let infoManager = infoManager else {
+            return 0
+        }
+        return infoManager.numberOfRows()
     }
     }
-    
+
     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
         let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
-        let values = derivedTableData[indexPath.row]
+        let values = infoManager.dataForIndexPath(indexPath)
         cell.textLabel?.text = values.name
         cell.textLabel?.text = values.name
         cell.detailTextLabel?.text = values.value
         cell.detailTextLabel?.text = values.value
         return cell
         return cell
@@ -512,11 +450,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         showHideNSDetails()
         showHideNSDetails()
     }
     }
     
     
-    //Clear the info data before next pull. This ensures we aren't displaying old data if something fails.
-    func clearLastInfoData(index: Int){
-        tableData[index].value = ""
-    }
-    
     func stringFromTimeInterval(interval: TimeInterval) -> String {
     func stringFromTimeInterval(interval: TimeInterval) -> String {
         let interval = Int(interval)
         let interval = Int(interval)
         let minutes = (interval / 60) % 60
         let minutes = (interval / 60) % 60
@@ -557,14 +490,12 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     func updateBadge(val: Int) {
     func updateBadge(val: Int) {
         if UserDefaultsRepository.appBadge.value {
         if UserDefaultsRepository.appBadge.value {
             let latestBG = String(val)
             let latestBG = String(val)
-            UIApplication.shared.applicationIconBadgeNumber = Int(bgUnits.removePeriodAndCommaForBadge(bgUnits.toDisplayUnits(latestBG))) ?? val
+            UIApplication.shared.applicationIconBadgeNumber = Int(Localizer.removePeriodAndCommaForBadge(Localizer.toDisplayUnits(latestBG))) ?? val
         } else {
         } else {
             UIApplication.shared.applicationIconBadgeNumber = 0
             UIApplication.shared.applicationIconBadgeNumber = 0
         }
         }
     }
     }
-    
-    
-    
+
     func setBGTextColor() {
     func setBGTextColor() {
         if bgData.count > 0 {
         if bgData.count > 0 {
             guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
             guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
@@ -626,11 +557,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - self.bgData[self.bgData.count - 1].date) / 60
         let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - self.bgData[self.bgData.count - 1].date) / 60
         var deltaString = ""
         var deltaString = ""
         if deltaBG < 0 {
         if deltaBG < 0 {
-            deltaString = bgUnits.toDisplayUnits(String(deltaBG))
+            deltaString = Localizer.toDisplayUnits(String(deltaBG))
         }
         }
         else
         else
         {
         {
-            deltaString = "+" + bgUnits.toDisplayUnits(String(deltaBG))
+            deltaString = "+" + Localizer.toDisplayUnits(String(deltaBG))
         }
         }
         let direction = self.bgDirectionGraphic(self.bgData[self.bgData.count - 1].direction ?? "")
         let direction = self.bgDirectionGraphic(self.bgData[self.bgData.count - 1].direction ?? "")
         
         
@@ -641,7 +572,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         if (UserDefaultsRepository.watchLine2.value.count > 1) {
         if (UserDefaultsRepository.watchLine2.value.count > 1) {
             eventTitle += "\n" + UserDefaultsRepository.watchLine2.value
             eventTitle += "\n" + UserDefaultsRepository.watchLine2.value
         }
         }
-        eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: bgUnits.toDisplayUnits(String(self.bgData[self.bgData.count - 1].sgv)))
+        eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: Localizer.toDisplayUnits(String(self.bgData[self.bgData.count - 1].sgv)))
         eventTitle = eventTitle.replacingOccurrences(of: "%DIRECTION%", with: direction)
         eventTitle = eventTitle.replacingOccurrences(of: "%DIRECTION%", with: direction)
         eventTitle = eventTitle.replacingOccurrences(of: "%DELTA%", with: deltaString)
         eventTitle = eventTitle.replacingOccurrences(of: "%DELTA%", with: deltaString)
         if self.currentOverride != 1.0 {
         if self.currentOverride != 1.0 {
@@ -659,25 +590,15 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             minAgo = String(Int(deltaTime)) + " min"
             minAgo = String(Int(deltaTime)) + " min"
             eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60))
             eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60))
         }
         }
-        var cob = "0"
-        if self.latestCOB != "" {
-            cob = self.latestCOB
-        }
         var basal = "~"
         var basal = "~"
         if self.latestBasal != "" {
         if self.latestBasal != "" {
             basal = self.latestBasal
             basal = self.latestBasal
         }
         }
-        var iob = "0"
-        if self.latestIOB != "" {
-            iob = self.latestIOB
-        }
         eventTitle = eventTitle.replacingOccurrences(of: "%MINAGO%", with: minAgo)
         eventTitle = eventTitle.replacingOccurrences(of: "%MINAGO%", with: minAgo)
-        eventTitle = eventTitle.replacingOccurrences(of: "%IOB%", with: iob)
-        eventTitle = eventTitle.replacingOccurrences(of: "%COB%", with: cob)
+        eventTitle = eventTitle.replacingOccurrences(of: "%IOB%", with: latestIOB?.formattedValue() ?? "0")
+        eventTitle = eventTitle.replacingOccurrences(of: "%COB%", with: latestCOB?.formattedValue() ?? "0")
         eventTitle = eventTitle.replacingOccurrences(of: "%BASAL%", with: basal)
         eventTitle = eventTitle.replacingOccurrences(of: "%BASAL%", with: basal)
         
         
-        
-        
         // Delete Events from last 2 hours and 2 hours in future
         // Delete Events from last 2 hours and 2 hours in future
         var deleteStartDate = Date().addingTimeInterval(-60*60*2)
         var deleteStartDate = Date().addingTimeInterval(-60*60*2)
         var deleteEndDate = Date().addingTimeInterval(60*60*2)
         var deleteEndDate = Date().addingTimeInterval(60*60*2)
@@ -723,7 +644,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     {
     {
         if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 {
         if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 {
             guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
             guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
-            snoozer.sendNotification(self, bgVal: bgUnits.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString, deltaVal: bgUnits.toDisplayUnits(String(latestDeltaString)), minAgoVal: latestMinAgoString, alertLabelVal: "Latest BG")
+            snoozer.sendNotification(self, bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString, deltaVal: Localizer.toDisplayUnits(String(latestDeltaString)), minAgoVal: latestMinAgoString, alertLabelVal: "Latest BG")
         }
         }
     }
     }
     
     
@@ -737,12 +658,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             }
             }
             debug.debugTextView.text += logText
             debug.debugTextView.text += logText
         }
         }
-        
-        
-        
     }
     }
-    
-    
+
     // General Notifications
     // General Notifications
     
     
     func sendGeneralNotification(_ sender: Any, title: String, subtitle: String, body: String, timer: TimeInterval) {
     func sendGeneralNotification(_ sender: Any, title: String, subtitle: String, body: String, timer: TimeInterval) {

+ 1 - 1
LoopFollow/helpers/Chart.swift

@@ -66,7 +66,7 @@ final class ChartYOverrideValueFormatter: ValueFormatter {
 
 
 final class ChartYMMOLValueFormatter: AxisValueFormatter {
 final class ChartYMMOLValueFormatter: AxisValueFormatter {
     func stringForValue(_ value: Double, axis: AxisBase?) -> String {
     func stringForValue(_ value: Double, axis: AxisBase?) -> String {
-        return bgUnits.toDisplayUnits(String(value))
+        return Localizer.toDisplayUnits(String(value))
     }
     }
 }
 }
 
 

+ 117 - 0
LoopFollow/helpers/Localizer.swift

@@ -0,0 +1,117 @@
+//
+//  Units.swift
+//  LoopFollow
+//
+//  Created by Jon Fawcett on 6/22/20.
+//  Copyright © 2020 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+class Localizer {
+    static func formatToLocalizedString(_ value: Double, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) -> String {
+        let numberFormatter = NumberFormatter()
+        numberFormatter.numberStyle = .decimal
+        numberFormatter.maximumFractionDigits = maxFractionDigits
+        numberFormatter.minimumFractionDigits = minFractionDigits
+        numberFormatter.locale = Locale.current
+
+        let numberValue = NSNumber(value: value)
+        return numberFormatter.string(from: numberValue) ?? String(value)
+    }
+
+    static func formatQuantity(_ quantity: HKQuantity) -> String {
+        let unitPreference = UserDefaultsRepository.units.value
+
+        if unitPreference == "mg/dL" {
+            let valueInMgdL = quantity.doubleValue(for: .milligramsPerDeciliter)
+            return formatToLocalizedString(valueInMgdL, maxFractionDigits: 0, minFractionDigits: 0)
+        } else {
+            let valueInMmolL = quantity.doubleValue(for: .millimolesPerLiter)
+            return formatToLocalizedString(valueInMmolL, maxFractionDigits: 1, minFractionDigits: 1)
+        }
+    }
+
+    static func formatTimestampToLocalString(_ timestamp: TimeInterval) -> String {
+        let date = Date(timeIntervalSince1970: timestamp)
+        let dateFormatter = DateFormatter()
+        dateFormatter.setLocalizedDateFormatFromTemplate("jms")
+        dateFormatter.locale = Locale.current
+        dateFormatter.timeZone = TimeZone.current
+        return dateFormatter.string(from: date)
+    }
+
+    static func formatLocalDouble(_ value: Double, unit: String? = nil) -> String {
+        let numberFormatter = NumberFormatter()
+        numberFormatter.numberStyle = .decimal
+
+        let units = unit ?? UserDefaultsRepository.units.value
+
+        if units == "mg/dL" {
+            numberFormatter.maximumFractionDigits = 0 // No decimal places for mg/dL
+        } else {
+            numberFormatter.maximumFractionDigits = 1 // Always one decimal place for mmol/L
+            numberFormatter.minimumFractionDigits = 1 // This ensures even .0 is displayed
+        }
+
+        numberFormatter.locale = Locale.current
+
+        let numberValue = NSNumber(value: value)
+        return numberFormatter.string(from: numberValue) ?? String(value)
+    }
+
+    static func toDisplayUnits(_ value: String) -> String {
+        let numberFormatter = NumberFormatter()
+        numberFormatter.numberStyle = .decimal
+        
+        if UserDefaultsRepository.units.value == "mg/dL" {
+            numberFormatter.maximumFractionDigits = 0 // No decimal places for mg/dL
+        } else {
+            numberFormatter.maximumFractionDigits = 1 // Always one decimal place for mmol/L
+            numberFormatter.minimumFractionDigits = 1 // This ensures even .0 is displayed
+        }
+        
+        numberFormatter.locale = Locale.current
+        
+        if let number = Float(value) {
+            if UserDefaultsRepository.units.value == "mg/dL" {
+                let numberValue = NSNumber(value: number)
+                return numberFormatter.string(from: numberValue) ?? value
+            } else {
+                let mmolValue = Double(number) * GlucoseConversion.mgDlToMmolL  // Convert number to Double
+                let numberValue = NSNumber(value: mmolValue)
+                return numberFormatter.string(from: numberValue) ?? value
+            }
+        }
+        
+        return value
+    }
+
+    static func removePeriodAndCommaForBadge(_ value: String) -> String {
+        var modifiedValue = value
+        modifiedValue = modifiedValue.replacingOccurrences(of: ".", with: "")
+        modifiedValue = modifiedValue.replacingOccurrences(of: ",", with: "")
+        return modifiedValue
+    }
+}
+
+
+extension Float {
+    
+    // remove the decimal part of the float if it is ".0" and trim whitespaces
+    var cleanValue: String {
+        return self.truncatingRemainder(dividingBy: 1) == 0
+            ? String(format: "%5.0f", self).trimmingCharacters(in: CharacterSet.whitespaces)
+            : String(format: "%5.1f", self).trimmingCharacters(in: CharacterSet.whitespaces)
+    }
+    
+    var roundTo3f: Float {
+        return round(to: 3)
+    }
+    
+    func round(to places: Int) -> Float {
+        let divisor = pow(10.0, Float(places))
+        return (divisor * self).rounded() / divisor
+    }
+}

+ 0 - 67
LoopFollow/helpers/bgUnits.swift

@@ -1,67 +0,0 @@
-//
-//  Units.swift
-//  LoopFollow
-//
-//  Created by Jon Fawcett on 6/22/20.
-//  Copyright © 2020 Jon Fawcett. All rights reserved.
-//
-
-import Foundation
-
-
-class bgUnits {
-    
-    static func toDisplayUnits(_ value: String) -> String {
-        let numberFormatter = NumberFormatter()
-        numberFormatter.numberStyle = .decimal
-        
-        if UserDefaultsRepository.units.value == "mg/dL" {
-            numberFormatter.maximumFractionDigits = 0 // No decimal places for mg/dL
-        } else {
-            numberFormatter.maximumFractionDigits = 1 // Always one decimal place for mmol/L
-            numberFormatter.minimumFractionDigits = 1 // This ensures even .0 is displayed
-        }
-        
-        numberFormatter.locale = Locale.current
-        
-        if let number = Float(value) {
-            if UserDefaultsRepository.units.value == "mg/dL" {
-                let numberValue = NSNumber(value: number)
-                return numberFormatter.string(from: numberValue) ?? value
-            } else {
-                let mmolValue = Double(number) * GlucoseConversion.mgDlToMmolL  // Convert number to Double
-                let numberValue = NSNumber(value: mmolValue)
-                return numberFormatter.string(from: numberValue) ?? value
-            }
-        }
-        
-        return value
-    }
-
-    static func removePeriodAndCommaForBadge(_ value: String) -> String {
-        var modifiedValue = value
-        modifiedValue = modifiedValue.replacingOccurrences(of: ".", with: "")
-        modifiedValue = modifiedValue.replacingOccurrences(of: ",", with: "")
-        return modifiedValue
-    }
-}
-
-
-extension Float {
-    
-    // remove the decimal part of the float if it is ".0" and trim whitespaces
-    var cleanValue: String {
-        return self.truncatingRemainder(dividingBy: 1) == 0
-            ? String(format: "%5.0f", self).trimmingCharacters(in: CharacterSet.whitespaces)
-            : String(format: "%5.1f", self).trimmingCharacters(in: CharacterSet.whitespaces)
-    }
-    
-    var roundTo3f: Float {
-        return round(to: 3)
-    }
-    
-    func round(to places: Int) -> Float {
-        let divisor = pow(10.0, Float(places))
-        return (divisor * self).rounded() / divisor
-    }
-}

+ 24 - 23
LoopFollow/repository/UserDefaults.swift

@@ -10,33 +10,35 @@
 //
 //
 //
 //
 
 
-
-
 import Foundation
 import Foundation
 import UIKit
 import UIKit
 
 
 class UserDefaultsRepository {
 class UserDefaultsRepository {
-    
-    // DisplayValues total
-    static let infoDataTotal = UserDefaultsValue<Int>(key: "infoDataTotal", default: 0)
-    static let infoNames = UserDefaultsValue<[String]>(key: "infoNames", default: [
-        "IOB",
-        "COB",
-        "Basal",
-        "Override",
-        "Battery",
-        "Pump",
-        "SAGE",
-        "CAGE",
-        "Rec. Bolus",
-        "Min/Max", //Previously "Pred."
-        "Carbs today",
-        "Autosens",
-        "Profile"])
-    static let infoSort = UserDefaultsValue<[Int]>(key: "infoSort", default: [0,1,2,3,4,5,6,7,8,9,10,11,12])
-    static let infoVisible = UserDefaultsValue<[Bool]>(key: "infoVisible", default: [true,true,true,true,true,true,true,true,true,true,true,false,false])
+    static let infoSort = UserDefaultsValue<[Int]>(key: "infoSort", default: InfoType.allCases.map { $0.sortOrder })
+    static let infoVisible = UserDefaultsValue<[Bool]>(key: "infoVisible", default: InfoType.allCases.map { $0.defaultVisible })
+
+    static func synchronizeInfoTypes() {
+        // Ensure infoSort array is the correct size
+        var sortArray = infoSort.value
+        if sortArray.count != InfoType.allCases.count {
+            sortArray = InfoType.allCases.map { $0.sortOrder }
+            infoSort.value = sortArray
+        }
+
+        // Ensure infoVisible array is the correct size
+        var visibleArray = infoVisible.value
+        if visibleArray.count < InfoType.allCases.count {
+            for infoType in InfoType.allCases[visibleArray.count..<InfoType.allCases.count] {
+                visibleArray.append(infoType.defaultVisible)
+            }
+        } else if visibleArray.count > InfoType.allCases.count {
+            visibleArray = Array(visibleArray.prefix(InfoType.allCases.count))
+        }
+        infoVisible.value = visibleArray
+    }
+
     static let hideInfoTable = UserDefaultsValue<Bool>(key: "hideInfoTable", default: false)
     static let hideInfoTable = UserDefaultsValue<Bool>(key: "hideInfoTable", default: false)
-    
+
     // Nightscout Settings
     // Nightscout Settings
     static let showNS = UserDefaultsValue<Bool>(key: "showNS", default: false)
     static let showNS = UserDefaultsValue<Bool>(key: "showNS", default: false)
     static let url = UserDefaultsValue<String>(key: "url", default: "")
     static let url = UserDefaultsValue<String>(key: "url", default: "")
@@ -107,7 +109,6 @@ class UserDefaultsRepository {
     static let graphBolus = UserDefaultsValue<Bool>(key: "graphBolus", default: true)
     static let graphBolus = UserDefaultsValue<Bool>(key: "graphBolus", default: true)
     static let graphCarbs = UserDefaultsValue<Bool>(key: "graphCarbs", default: true)
     static let graphCarbs = UserDefaultsValue<Bool>(key: "graphCarbs", default: true)
     static let debugLog = UserDefaultsValue<Bool>(key: "debugLog", default: false)
     static let debugLog = UserDefaultsValue<Bool>(key: "debugLog", default: false)
-    static let alwaysDownloadAllBG = UserDefaultsValue<Bool>(key: "alwaysDownloadAllBG", default: true)
     static let bgUpdateDelay = UserDefaultsValue<Int>(key: "bgUpdateDelay", default: 10)
     static let bgUpdateDelay = UserDefaultsValue<Int>(key: "bgUpdateDelay", default: 10)
     static let downloadDays = UserDefaultsValue<Int>(key: "downloadDays", default: 1)
     static let downloadDays = UserDefaultsValue<Int>(key: "downloadDays", default: 1)