Преглед изворни кода

Merge pull request #53 from polscm32/small-fixes

Performance fixes, Fix creation of Calendar events, Refactor Treatments View
Deniz Cengiz пре 1 година
родитељ
комит
93c7879b07
25 измењених фајлова са 1522 додато и 1327 уклоњено
  1. 44 0
      FreeAPS.xcodeproj/project.pbxproj
  2. 99 0
      FreeAPS/Sources/Helpers/MainChartHelper.swift
  3. 76 51
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  4. 57 66
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  5. 55 63
      FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift
  6. 132 114
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  7. 249 0
      FreeAPS/Sources/Modules/Home/View/Chart/BasalChart.swift
  8. 63 0
      FreeAPS/Sources/Modules/Home/View/Chart/CarbView.swift
  9. 67 0
      FreeAPS/Sources/Modules/Home/View/Chart/CobChart.swift
  10. 54 0
      FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift
  11. 91 0
      FreeAPS/Sources/Modules/Home/View/Chart/ForecastView.swift
  12. 66 0
      FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift
  13. 42 0
      FreeAPS/Sources/Modules/Home/View/Chart/InsulinView.swift
  14. 63 0
      FreeAPS/Sources/Modules/Home/View/Chart/IobChart.swift
  15. 171 1001
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  16. 50 0
      FreeAPS/Sources/Modules/Home/View/Chart/OverrideView.swift
  17. 82 0
      FreeAPS/Sources/Modules/Home/View/Chart/TempTargets.swift
  18. 11 16
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  19. 3 9
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  20. 3 2
      FreeAPS/Sources/Services/Calendar/CalendarManager.swift
  21. 1 1
      FreeAPS/Sources/Services/UnlockManager/UnlockManager.swift
  22. 39 0
      FreeAPS/Sources/Views/ViewModifiers.swift
  23. 2 2
      Model/CoreDataStack.swift
  24. 1 1
      Model/Helper/GlucoseStored+helper.swift
  25. 1 1
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

+ 44 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -247,6 +247,10 @@
 		581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581516A82BCEEDF800BF67D7 /* NSPredicates.swift */; };
 		581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581516A82BCEEDF800BF67D7 /* NSPredicates.swift */; };
 		581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581AC4382BE22ED10038760C /* JSONConverter.swift */; };
 		581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581AC4382BE22ED10038760C /* JSONConverter.swift */; };
 		58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58237D9D2BCF0A6B00A47A79 /* PopupView.swift */; };
 		58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58237D9D2BCF0A6B00A47A79 /* PopupView.swift */; };
+		582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582DF9742C8CDB92001F516D /* GlucoseChartView.swift */; };
+		582DF9772C8CDBE7001F516D /* InsulinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582DF9762C8CDBE7001F516D /* InsulinView.swift */; };
+		582DF9792C8CE1E5001F516D /* MainChartHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582DF9782C8CE1E5001F516D /* MainChartHelper.swift */; };
+		582DF97B2C8CE209001F516D /* CarbView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582DF97A2C8CE209001F516D /* CarbView.swift */; };
 		582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582FAE422C05102C00D1C13F /* CoreDataError.swift */; };
 		582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582FAE422C05102C00D1C13F /* CoreDataError.swift */; };
 		583684062BD178DB00070A60 /* GlucoseStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684052BD178DB00070A60 /* GlucoseStored+helper.swift */; };
 		583684062BD178DB00070A60 /* GlucoseStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684052BD178DB00070A60 /* GlucoseStored+helper.swift */; };
 		583684082BD195A700070A60 /* Determination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684072BD195A700070A60 /* Determination.swift */; };
 		583684082BD195A700070A60 /* Determination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684072BD195A700070A60 /* Determination.swift */; };
@@ -255,6 +259,13 @@
 		5864E8592C42CFAE00294306 /* DeterminationStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864E8582C42CFAE00294306 /* DeterminationStorage.swift */; };
 		5864E8592C42CFAE00294306 /* DeterminationStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864E8582C42CFAE00294306 /* DeterminationStorage.swift */; };
 		587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */; };
 		587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */; };
 		5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5887527B2BD986E1008B081D /* OpenAPSBattery.swift */; };
 		5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5887527B2BD986E1008B081D /* OpenAPSBattery.swift */; };
+		58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B212C8DAA8E00AA37D3 /* OverrideView.swift */; };
+		58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B2F2C8DEA7500AA37D3 /* ForecastView.swift */; };
+		58D08B322C8DF88900AA37D3 /* DummyCharts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B312C8DF88900AA37D3 /* DummyCharts.swift */; };
+		58D08B342C8DF9A700AA37D3 /* CobChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B332C8DF9A700AA37D3 /* CobChart.swift */; };
+		58D08B362C8DFAC600AA37D3 /* IobChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B352C8DFAC600AA37D3 /* IobChart.swift */; };
+		58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B372C8DFB6000AA37D3 /* BasalChart.swift */; };
+		58D08B3A2C8DFECD00AA37D3 /* TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B392C8DFECD00AA37D3 /* TempTargets.swift */; };
 		58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F107732BD1A4D000B1A680 /* Determination+helper.swift */; };
 		58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F107732BD1A4D000B1A680 /* Determination+helper.swift */; };
 		5A2325522BFCBF55003518CA /* NightscoutUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2325512BFCBF55003518CA /* NightscoutUploadView.swift */; };
 		5A2325522BFCBF55003518CA /* NightscoutUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2325512BFCBF55003518CA /* NightscoutUploadView.swift */; };
 		5A2325542BFCBF66003518CA /* NightscoutFetchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */; };
 		5A2325542BFCBF66003518CA /* NightscoutFetchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */; };
@@ -887,6 +898,10 @@
 		581516A82BCEEDF800BF67D7 /* NSPredicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPredicates.swift; sourceTree = "<group>"; };
 		581516A82BCEEDF800BF67D7 /* NSPredicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPredicates.swift; sourceTree = "<group>"; };
 		581AC4382BE22ED10038760C /* JSONConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONConverter.swift; sourceTree = "<group>"; };
 		581AC4382BE22ED10038760C /* JSONConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONConverter.swift; sourceTree = "<group>"; };
 		58237D9D2BCF0A6B00A47A79 /* PopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupView.swift; sourceTree = "<group>"; };
 		58237D9D2BCF0A6B00A47A79 /* PopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupView.swift; sourceTree = "<group>"; };
+		582DF9742C8CDB92001F516D /* GlucoseChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseChartView.swift; sourceTree = "<group>"; };
+		582DF9762C8CDBE7001F516D /* InsulinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinView.swift; sourceTree = "<group>"; };
+		582DF9782C8CE1E5001F516D /* MainChartHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartHelper.swift; sourceTree = "<group>"; };
+		582DF97A2C8CE209001F516D /* CarbView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbView.swift; sourceTree = "<group>"; };
 		582FAE422C05102C00D1C13F /* CoreDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataError.swift; sourceTree = "<group>"; };
 		582FAE422C05102C00D1C13F /* CoreDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataError.swift; sourceTree = "<group>"; };
 		583684052BD178DB00070A60 /* GlucoseStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+helper.swift"; sourceTree = "<group>"; };
 		583684052BD178DB00070A60 /* GlucoseStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+helper.swift"; sourceTree = "<group>"; };
 		583684072BD195A700070A60 /* Determination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Determination.swift; sourceTree = "<group>"; };
 		583684072BD195A700070A60 /* Determination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Determination.swift; sourceTree = "<group>"; };
@@ -895,6 +910,13 @@
 		5864E8582C42CFAE00294306 /* DeterminationStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterminationStorage.swift; sourceTree = "<group>"; };
 		5864E8582C42CFAE00294306 /* DeterminationStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterminationStorage.swift; sourceTree = "<group>"; };
 		587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRowView.swift; sourceTree = "<group>"; };
 		587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRowView.swift; sourceTree = "<group>"; };
 		5887527B2BD986E1008B081D /* OpenAPSBattery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPSBattery.swift; sourceTree = "<group>"; };
 		5887527B2BD986E1008B081D /* OpenAPSBattery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPSBattery.swift; sourceTree = "<group>"; };
+		58D08B212C8DAA8E00AA37D3 /* OverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideView.swift; sourceTree = "<group>"; };
+		58D08B2F2C8DEA7500AA37D3 /* ForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastView.swift; sourceTree = "<group>"; };
+		58D08B312C8DF88900AA37D3 /* DummyCharts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyCharts.swift; sourceTree = "<group>"; };
+		58D08B332C8DF9A700AA37D3 /* CobChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CobChart.swift; sourceTree = "<group>"; };
+		58D08B352C8DFAC600AA37D3 /* IobChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobChart.swift; sourceTree = "<group>"; };
+		58D08B372C8DFB6000AA37D3 /* BasalChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalChart.swift; sourceTree = "<group>"; };
+		58D08B392C8DFECD00AA37D3 /* TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargets.swift; sourceTree = "<group>"; };
 		58F107732BD1A4D000B1A680 /* Determination+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Determination+helper.swift"; sourceTree = "<group>"; };
 		58F107732BD1A4D000B1A680 /* Determination+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Determination+helper.swift"; sourceTree = "<group>"; };
 		5A2325512BFCBF55003518CA /* NightscoutUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUploadView.swift; sourceTree = "<group>"; };
 		5A2325512BFCBF55003518CA /* NightscoutUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUploadView.swift; sourceTree = "<group>"; };
 		5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutFetchView.swift; sourceTree = "<group>"; };
 		5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutFetchView.swift; sourceTree = "<group>"; };
@@ -1754,6 +1776,16 @@
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
 				BD3CC0712B0B89D50013189E /* MainChartView.swift */,
 				BD3CC0712B0B89D50013189E /* MainChartView.swift */,
+				582DF9742C8CDB92001F516D /* GlucoseChartView.swift */,
+				582DF9762C8CDBE7001F516D /* InsulinView.swift */,
+				582DF97A2C8CE209001F516D /* CarbView.swift */,
+				58D08B212C8DAA8E00AA37D3 /* OverrideView.swift */,
+				58D08B2F2C8DEA7500AA37D3 /* ForecastView.swift */,
+				58D08B312C8DF88900AA37D3 /* DummyCharts.swift */,
+				58D08B332C8DF9A700AA37D3 /* CobChart.swift */,
+				58D08B352C8DFAC600AA37D3 /* IobChart.swift */,
+				58D08B372C8DFB6000AA37D3 /* BasalChart.swift */,
+				58D08B392C8DFECD00AA37D3 /* TempTargets.swift */,
 			);
 			);
 			path = Chart;
 			path = Chart;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -1941,6 +1973,7 @@
 				581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */,
 				581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */,
 				DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */,
 				DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */,
 				DD6B7CB12C7B6F0800B75029 /* Rounding.swift */,
 				DD6B7CB12C7B6F0800B75029 /* Rounding.swift */,
+				582DF9782C8CE1E5001F516D /* MainChartHelper.swift */,
 			);
 			);
 			path = Helpers;
 			path = Helpers;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -3129,6 +3162,7 @@
 				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				38E4453C274E411700EC9A94 /* Disk+Codable.swift in Sources */,
 				38E4453C274E411700EC9A94 /* Disk+Codable.swift in Sources */,
+				58D08B322C8DF88900AA37D3 /* DummyCharts.swift in Sources */,
 				19E1F7EF29D08EBA005C8D20 /* IconConfigRootWiew.swift in Sources */,
 				19E1F7EF29D08EBA005C8D20 /* IconConfigRootWiew.swift in Sources */,
 				1967DFC229D053D300759F30 /* IconImage.swift in Sources */,
 				1967DFC229D053D300759F30 /* IconImage.swift in Sources */,
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,
@@ -3252,10 +3286,12 @@
 				DD57C4BD2C4C7103001A5B28 /* PumpEventStored+CoreDataProperties.swift in Sources */,
 				DD57C4BD2C4C7103001A5B28 /* PumpEventStored+CoreDataProperties.swift in Sources */,
 				DD57C4BE2C4C7103001A5B28 /* TempBasalStored+CoreDataClass.swift in Sources */,
 				DD57C4BE2C4C7103001A5B28 /* TempBasalStored+CoreDataClass.swift in Sources */,
 				DD57C4BF2C4C7103001A5B28 /* TempBasalStored+CoreDataProperties.swift in Sources */,
 				DD57C4BF2C4C7103001A5B28 /* TempBasalStored+CoreDataProperties.swift in Sources */,
+				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
 				DD57C4C02C4C7103001A5B28 /* TempTargetsSlider+CoreDataClass.swift in Sources */,
 				DD57C4C02C4C7103001A5B28 /* TempTargetsSlider+CoreDataClass.swift in Sources */,
 				DD57C4C12C4C7103001A5B28 /* TempTargetsSlider+CoreDataProperties.swift in Sources */,
 				DD57C4C12C4C7103001A5B28 /* TempTargetsSlider+CoreDataProperties.swift in Sources */,
 				DD57C4C22C4C7103001A5B28 /* Forecast+CoreDataClass.swift in Sources */,
 				DD57C4C22C4C7103001A5B28 /* Forecast+CoreDataClass.swift in Sources */,
+				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
 				DD57C4C32C4C7103001A5B28 /* Forecast+CoreDataProperties.swift in Sources */,
 				DD57C4C32C4C7103001A5B28 /* Forecast+CoreDataProperties.swift in Sources */,
 				DD57C4C42C4C7103001A5B28 /* BolusStored+CoreDataClass.swift in Sources */,
 				DD57C4C42C4C7103001A5B28 /* BolusStored+CoreDataClass.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
@@ -3263,6 +3299,7 @@
 				DD57C4C62C4C7103001A5B28 /* CarbEntryStored+CoreDataClass.swift in Sources */,
 				DD57C4C62C4C7103001A5B28 /* CarbEntryStored+CoreDataClass.swift in Sources */,
 				DD57C4C72C4C7103001A5B28 /* CarbEntryStored+CoreDataProperties.swift in Sources */,
 				DD57C4C72C4C7103001A5B28 /* CarbEntryStored+CoreDataProperties.swift in Sources */,
 				DD57C4C82C4C7103001A5B28 /* OpenAPS_Battery+CoreDataClass.swift in Sources */,
 				DD57C4C82C4C7103001A5B28 /* OpenAPS_Battery+CoreDataClass.swift in Sources */,
+				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
 				DD57C4C92C4C7103001A5B28 /* OpenAPS_Battery+CoreDataProperties.swift in Sources */,
 				DD57C4C92C4C7103001A5B28 /* OpenAPS_Battery+CoreDataProperties.swift in Sources */,
 				118DF76D2C5ECBC60067FEB7 /* OverridePresetEntity.swift in Sources */,
 				118DF76D2C5ECBC60067FEB7 /* OverridePresetEntity.swift in Sources */,
 				DD57C4CA2C4C7103001A5B28 /* GlucoseStored+CoreDataClass.swift in Sources */,
 				DD57C4CA2C4C7103001A5B28 /* GlucoseStored+CoreDataClass.swift in Sources */,
@@ -3282,6 +3319,7 @@
 				CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */,
 				CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */,
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */,
 				19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */,
+				582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
@@ -3327,6 +3365,7 @@
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
+				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */,
 				DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */,
@@ -3336,6 +3375,7 @@
 				BDCD47AF2C1F3F1700F8BCD5 /* OverrideStored+helper.swift in Sources */,
 				BDCD47AF2C1F3F1700F8BCD5 /* OverrideStored+helper.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
 				CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */,
 				CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */,
+				58D08B362C8DFAC600AA37D3 /* IobChart.swift in Sources */,
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
@@ -3374,6 +3414,7 @@
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
+				58D08B342C8DF9A700AA37D3 /* CobChart.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
 				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
 				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
@@ -3388,6 +3429,7 @@
 				BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */,
 				BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */,
 				38E44536274E411700EC9A94 /* Disk.swift in Sources */,
 				38E44536274E411700EC9A94 /* Disk.swift in Sources */,
 				19A910362A24D6D700C8951B /* DateFilter.swift in Sources */,
 				19A910362A24D6D700C8951B /* DateFilter.swift in Sources */,
+				582DF9792C8CE1E5001F516D /* MainChartHelper.swift in Sources */,
 				E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */,
 				E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */,
 				38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */,
 				38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */,
 				389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */,
 				389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */,
@@ -3425,6 +3467,7 @@
 				BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */,
 				BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */,
 				BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */,
 				BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
+				582DF9772C8CDBE7001F516D /* InsulinView.swift in Sources */,
 				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,
 				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,
 				DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */,
 				DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */,
 				5864E8592C42CFAE00294306 /* DeterminationStorage.swift in Sources */,
 				5864E8592C42CFAE00294306 /* DeterminationStorage.swift in Sources */,
@@ -3444,6 +3487,7 @@
 				CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */,
 				CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */,
 				110AEDED2C51A0AE00615CC9 /* ShortcutsConfigProvider.swift in Sources */,
 				110AEDED2C51A0AE00615CC9 /* ShortcutsConfigProvider.swift in Sources */,
 				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
 				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
+				58D08B3A2C8DFECD00AA37D3 /* TempTargets.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
 				DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */,
 				DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */,

+ 99 - 0
FreeAPS/Sources/Helpers/MainChartHelper.swift

@@ -0,0 +1,99 @@
+import CoreData
+import Foundation
+
+enum MainChartHelper {
+    // Calculates the glucose value thats the nearest to parameter 'time'
+    /// -Returns: A NSManagedObject of GlucoseStored
+    /// it is thread safe as everything is executed on the main thread
+    static func timeToNearestGlucose(glucoseValues: [GlucoseStored], time: TimeInterval) -> GlucoseStored? {
+        guard !glucoseValues.isEmpty else {
+            return nil
+        }
+
+        var low = 0
+        var high = glucoseValues.count - 1
+        var closestGlucose: GlucoseStored?
+
+        // binary search to find next glucose
+        while low <= high {
+            let mid = low + (high - low) / 2
+            let midTime = glucoseValues[mid].date?.timeIntervalSince1970 ?? 0
+
+            if midTime == time {
+                return glucoseValues[mid]
+            } else if midTime < time {
+                low = mid + 1
+            } else {
+                high = mid - 1
+            }
+
+            // update if necessary
+            if closestGlucose == nil || abs(midTime - time) < abs(closestGlucose!.date?.timeIntervalSince1970 ?? 0 - time) {
+                closestGlucose = glucoseValues[mid]
+            }
+        }
+
+        return closestGlucose
+    }
+
+    enum Config {
+        static let bolusSize: CGFloat = 5
+        static let bolusScale: CGFloat = 1.8
+        static let carbsSize: CGFloat = 5
+        static let maxCarbSize: CGFloat = 30
+        static let carbsScale: CGFloat = 0.3
+        static let fpuSize: CGFloat = 10
+        static let maxGlucose = 270
+        static let minGlucose = 45
+    }
+
+    static var bolusFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.minimumIntegerDigits = 0
+        formatter.maximumFractionDigits = 2
+        formatter.decimalSeparator = "."
+        return formatter
+    }
+
+    static var carbsFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        return formatter
+    }
+
+    static func bolusOffset(units: GlucoseUnits) -> Decimal {
+        units == .mgdL ? 30 : 1.66
+    }
+
+    static func calculateDuration(objectID: NSManagedObjectID, context: NSManagedObjectContext) -> TimeInterval? {
+        do {
+            if let override = try context.existingObject(with: objectID) as? OverrideStored,
+               let overrideDuration = override.duration as? Double, overrideDuration != 0
+            {
+                return TimeInterval(overrideDuration * 60) // return seconds
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate Override Target with error: \(error.localizedDescription)"
+            )
+        }
+        return nil
+    }
+
+    static func calculateTarget(objectID: NSManagedObjectID, context: NSManagedObjectContext) -> Decimal? {
+        do {
+            if let override = try context.existingObject(with: objectID) as? OverrideStored,
+               let overrideTarget = override.target, overrideTarget != 0
+            {
+                return overrideTarget.decimalValue
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate Override Target with error: \(error.localizedDescription)"
+            )
+        }
+        return nil
+    }
+}

+ 76 - 51
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -109,7 +109,8 @@ extension Bolus {
         @Published var maxForecast: [Int] = []
         @Published var maxForecast: [Int] = []
         @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
         @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
         @Published var forecastDisplayType: ForecastDisplayType = .cone
         @Published var forecastDisplayType: ForecastDisplayType = .cone
-        @Published var smooth: Bool = false
+        @Published var isSmoothingEnabled: Bool = false
+        @Published var stops: [Gradient.Stop] = []
 
 
         let now = Date.now
         let now = Date.now
 
 
@@ -121,51 +122,38 @@ extension Bolus {
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
 
 
         override func subscribe() {
         override func subscribe() {
-            setupGlucoseNotification()
-            coreDataObserver = CoreDataObserver()
-            registerHandlers()
-            setupGlucoseArray()
-
             Task {
             Task {
-                async let getAllSettingsDefaults: () = getAllSettingsValues()
-                async let setupDeterminations: () = setupDeterminationsArray()
-
-                await getAllSettingsDefaults
-                await setupDeterminations
-
-                // Determination has updated, so we can use this to draw the initial Forecast Chart
-                let forecastData = await mapForecastsForChart()
-                await updateForecasts(with: forecastData)
-            }
-
-            broadcaster.register(DeterminationObserver.self, observer: self)
-            broadcaster.register(BolusFailureObserver.self, observer: self)
-            units = settingsManager.settings.units
-            fraction = settings.settings.overrideFactor
-            fattyMeals = settings.settings.fattyMeals
-            fattyMealFactor = settings.settings.fattyMealFactor
-            sweetMeals = settings.settings.sweetMeals
-            sweetMealFactor = settings.settings.sweetMealFactor
-            displayPresets = settings.settings.displayPresets
-
-            forecastDisplayType = settings.settings.forecastDisplayType
-
-            lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
-            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+                await withTaskGroup(of: Void.self) { group in
+                    group.addTask {
+                        self.setupGlucoseNotification()
+                    }
+                    group.addTask {
+                        self.registerHandlers()
+                    }
+                    group.addTask {
+                        self.setupGlucoseArray()
+                    }
+                    group.addTask {
+                        self.setupDeterminationsAndForecasts()
+                    }
+                    group.addTask {
+                        await self.setupSettings()
+                    }
+                    group.addTask {
+                        self.registerObservers()
+                    }
 
 
-            maxCarbs = settings.settings.maxCarbs
-            maxFat = settings.settings.maxFat
-            maxProtein = settings.settings.maxProtein
-            useFPUconversion = settingsManager.settings.useFPUconversion
-            smooth = settingsManager.settings.smoothGlucose
-
-            if waitForSuggestionInitial {
-                Task {
-                    let ok = await apsManager.determineBasal()
-                    if !ok {
-                        self.waitForSuggestion = false
-                        self.insulinRequired = 0
-                        self.insulinRecommended = 0
+                    if self.waitForSuggestionInitial {
+                        group.addTask {
+                            let isDetermineBasalSuccessful = await self.apsManager.determineBasal()
+                            if !isDetermineBasalSuccessful {
+                                await MainActor.run {
+                                    self.waitForSuggestion = false
+                                    self.insulinRequired = 0
+                                    self.insulinRecommended = 0
+                                }
+                            }
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -203,6 +191,43 @@ extension Bolus {
             }
             }
         }
         }
 
 
+        private func setupDeterminationsAndForecasts() {
+            Task {
+                async let getAllSettingsDefaults: () = getAllSettingsValues()
+                async let setupDeterminations: () = setupDeterminationsArray()
+
+                await getAllSettingsDefaults
+                await setupDeterminations
+
+                // Determination has updated, so we can use this to draw the initial Forecast Chart
+                let forecastData = await mapForecastsForChart()
+                await updateForecasts(with: forecastData)
+            }
+        }
+
+        private func registerObservers() {
+            broadcaster.register(DeterminationObserver.self, observer: self)
+            broadcaster.register(BolusFailureObserver.self, observer: self)
+        }
+
+        @MainActor private func setupSettings() async {
+            units = settingsManager.settings.units
+            fraction = settings.settings.overrideFactor
+            fattyMeals = settings.settings.fattyMeals
+            fattyMealFactor = settings.settings.fattyMealFactor
+            sweetMeals = settings.settings.sweetMeals
+            sweetMealFactor = settings.settings.sweetMealFactor
+            displayPresets = settings.settings.displayPresets
+            forecastDisplayType = settings.settings.forecastDisplayType
+            lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
+            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+            maxCarbs = settings.settings.maxCarbs
+            maxFat = settings.settings.maxFat
+            maxProtein = settings.settings.maxProtein
+            useFPUconversion = settingsManager.settings.useFPUconversion
+            isSmoothingEnabled = settingsManager.settings.smoothGlucose
+        }
+
         private func getCurrentSettingValue(for type: SettingType) async {
         private func getCurrentSettingValue(for type: SettingType) async {
             let now = Date()
             let now = Date()
             let calendar = Calendar.current
             let calendar = Calendar.current
@@ -724,20 +749,20 @@ extension Bolus.StateModel {
         minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
         minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
         guard minCount > 0 else { return }
         guard minCount > 0 else { return }
 
 
-        let (minResult, maxResult) = await Task.detached {
-            let minForecast = (0 ..< self.minCount).map { index in
+        async let minForecastResult = Task.detached {
+            (0 ..< self.minCount).map { index in
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
             }
             }
+        }.value
 
 
-            let maxForecast = (0 ..< self.minCount).map { index in
+        async let maxForecastResult = Task.detached {
+            (0 ..< self.minCount).map { index in
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
             }
             }
-
-            return (minForecast, maxForecast)
         }.value
         }.value
 
 
-        minForecast = minResult
-        maxForecast = maxResult
+        minForecast = await minForecastResult
+        maxForecast = await maxForecastResult
     }
     }
 }
 }
 
 

+ 57 - 66
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -171,39 +171,48 @@ extension Bolus {
                 VStack {
                 VStack {
                     Form {
                     Form {
                         Section {
                         Section {
+                            ForeCastChart(state: state, units: $state.units)
+                                .padding(.vertical)
+                        }.listRowBackground(Color.chart)
+
+                        Section {
                             carbsTextField()
                             carbsTextField()
 
 
-                            if state.useFPUconversion {
-                                proteinAndFat()
-                            }
+                            DisclosureGroup("Extras") {
+                                if state.useFPUconversion {
+                                    proteinAndFat()
+                                }
 
 
-                            // Time
-                            HStack {
-                                Text("Time").foregroundStyle(Color.secondary)
-                                Spacer()
-                                if !pushed {
-                                    Button {
-                                        pushed = true
-                                    } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary)
-                                        .padding(.trailing, 5)
-                                } else {
-                                    Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
-                                    label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
-                                    DatePicker(
-                                        "Time",
-                                        selection: $state.date,
-                                        displayedComponents: [.hourAndMinute]
-                                    ).controlSize(.mini)
-                                        .labelsHidden()
-                                    Button {
-                                        state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
+                                // Time
+                                HStack {
+                                    Text("Time").foregroundStyle(Color.secondary)
+                                    Spacer()
+                                    if !pushed {
+                                        Button {
+                                            pushed = true
+                                        } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary)
+                                            .padding(.trailing, 5)
+                                    } else {
+                                        Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
+                                        label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
+                                        DatePicker(
+                                            "Time",
+                                            selection: $state.date,
+                                            displayedComponents: [.hourAndMinute]
+                                        ).controlSize(.mini)
+                                            .labelsHidden()
+                                        Button {
+                                            state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
+                                        }
+                                        label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
                                     }
                                     }
-                                    label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
                                 }
                                 }
-                            }
-                            HStack {
-                                Image(systemName: "square.and.pencil").foregroundColor(.secondary)
-                                TextFieldWithToolBarString(text: $state.note, placeholder: "", maxLength: 25)
+
+                                // Notes
+                                HStack {
+                                    Image(systemName: "square.and.pencil").foregroundColor(.secondary)
+                                    TextFieldWithToolBarString(text: $state.note, placeholder: "", maxLength: 25)
+                                }
                             }
                             }
                         }.listRowBackground(Color.chart)
                         }.listRowBackground(Color.chart)
 
 
@@ -293,15 +302,10 @@ extension Bolus {
                             }
                             }
                         }.listRowBackground(Color.chart)
                         }.listRowBackground(Color.chart)
 
 
-                        Section {
-                            ForeCastChart(state: state, units: $state.units)
-                                .padding(.vertical)
-                        }.listRowBackground(Color.chart)
+                        treatmentButton
                     }
                     }
                 }
                 }
-                .safeAreaInset(edge: .bottom, spacing: 0) {
-                    stickyButton
-                }.blur(radius: state.waitForSuggestion ? 5 : 0)
+                .blur(radius: state.waitForSuggestion ? 5 : 0)
 
 
                 if state.waitForSuggestion {
                 if state.waitForSuggestion {
                     CustomProgressView(text: progressText.rawValue)
                     CustomProgressView(text: progressText.rawValue)
@@ -365,46 +369,33 @@ extension Bolus {
             }
             }
         }
         }
 
 
-        var stickyButton: some View {
-            ZStack {
-                Rectangle()
-                    .frame(width: UIScreen.main.bounds.width, height: 120).offset(y: 40)
-                    .shadow(
-                        color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
-                            Color.black.opacity(0.33),
-                        radius: 3
-                    )
-                    .foregroundStyle(Color.chart)
-
-                Button {
-                    state.invokeTreatmentsTask()
-                } label: {
-                    taskButtonLabel
-                        .font(.headline)
-                        .foregroundStyle(Color.white)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .frame(minHeight: 50)
-                }
-                .disabled(disableTaskButton)
-                .background(limitExceeded ? Color(.systemRed) : Color(.systemBlue))
-                .shadow(radius: 3)
-                .clipShape(RoundedRectangle(cornerRadius: 8))
-                .padding()
-                .offset(y: 20)
+        var treatmentButton: some View {
+            Button {
+                state.invokeTreatmentsTask()
+            } label: {
+                taskButtonLabel
+                    .font(.headline)
+                    .foregroundStyle(Color.white)
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .frame(height: 35)
             }
             }
+            .disabled(disableTaskButton)
+            .listRowBackground(limitExceeded ? Color(.systemRed) : Color(.systemBlue))
+            .shadow(radius: 3)
+            .clipShape(RoundedRectangle(cornerRadius: 8))
         }
         }
 
 
         private var taskButtonLabel: some View {
         private var taskButtonLabel: some View {
             if pumpBolusLimitExceeded {
             if pumpBolusLimitExceeded {
-                return Text("Max Bolus of \(state.maxBolus) U Exceeded")
+                return Text("Max Bolus of \(state.maxBolus.description) U Exceeded")
             } else if externalBolusLimitExceeded {
             } else if externalBolusLimitExceeded {
-                return Text("Max External Bolus of \(state.maxExternal) U Exceeded")
+                return Text("Max External Bolus of \(state.maxExternal.description) U Exceeded")
             } else if carbLimitExceeded {
             } else if carbLimitExceeded {
-                return Text("Max Carbs of \(state.maxCarbs) g Exceeded")
+                return Text("Max Carbs of \(state.maxCarbs.description) g Exceeded")
             } else if fatLimitExceeded {
             } else if fatLimitExceeded {
-                return Text("Max Fat of \(state.maxFat) g Exceeded")
+                return Text("Max Fat of \(state.maxFat.description) g Exceeded")
             } else if proteinLimitExceeded {
             } else if proteinLimitExceeded {
-                return Text("Max Protein of \(state.maxProtein) g Exceeded")
+                return Text("Max Protein of \(state.maxProtein.description) g Exceeded")
             }
             }
 
 
             let hasInsulin = state.amount > 0
             let hasInsulin = state.amount > 0

+ 55 - 63
FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift

@@ -35,6 +35,42 @@ struct ForeCastChart: View {
 
 
     var body: some View {
     var body: some View {
         VStack {
         VStack {
+            HStack {
+                HStack {
+                    Text("Added carbs: ")
+                        .font(.footnote)
+                        .fontWeight(.bold)
+                        .foregroundStyle(.orange)
+
+                    Text("\(state.carbs.description) g")
+                        .font(.footnote)
+                        .foregroundStyle(.orange)
+                }
+                .padding(8)
+                .background {
+                    RoundedRectangle(cornerRadius: 10)
+                        .fill(Color.orange.opacity(0.2))
+                }
+
+                Spacer()
+
+                HStack {
+                    Text("Added insulin: ")
+                        .font(.footnote)
+                        .fontWeight(.bold)
+                        .foregroundStyle(.blue)
+
+                    Text("\(state.amount.description) U")
+                        .font(.footnote)
+                        .foregroundStyle(.blue)
+                }
+                .padding(8)
+                .background {
+                    RoundedRectangle(cornerRadius: 10)
+                        .fill(Color.blue.opacity(0.2))
+                }
+            }
+
             forecastChart
             forecastChart
                 .padding(.vertical, 3)
                 .padding(.vertical, 3)
             HStack {
             HStack {
@@ -83,74 +119,30 @@ struct ForeCastChart: View {
         .backport.chartForegroundStyleScale(state: state)
         .backport.chartForegroundStyleScale(state: state)
     }
     }
 
 
-    private var stops: [Gradient.Stop] {
-        let low = Double(state.lowGlucose)
-        let high = Double(state.highGlucose)
-
-        let glucoseValues = state.glucoseFromPersistence
-            .map { units == .mgdL ? Decimal($0.glucose) : Decimal($0.glucose).asMmolL }
-
-        let minimum = glucoseValues.min() ?? 0.0
-        let maximum = glucoseValues.max() ?? 0.0
-
-        // Calculate positions for gradient
-        let lowPosition = (low - Double(truncating: minimum as NSNumber)) /
-            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
-        let highPosition = (high - Double(truncating: minimum as NSNumber)) /
-            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
-
-        // Ensure positions are in bounds [0, 1]
-        let clampedLowPosition = max(0.0, min(lowPosition, 1.0))
-        let clampedHighPosition = max(0.0, min(highPosition, 1.0))
-
-        // Ensure lowPosition is less than highPosition
-        let sortedPositions = [clampedLowPosition, clampedHighPosition].sorted()
-
-        return [
-            Gradient.Stop(color: .red, location: 0.0),
-            Gradient.Stop(color: .red, location: sortedPositions[0]), // draw red gradient till lowGlucose
-            Gradient.Stop(color: .green, location: sortedPositions[0] + 0.0001), // draw green above lowGlucose till highGlucose
-            Gradient.Stop(color: .green, location: sortedPositions[1]),
-            Gradient.Stop(color: .orange, location: sortedPositions[1] + 0.0001), // draw orange above highGlucose
-            Gradient.Stop(color: .orange, location: 1.0)
-        ]
-    }
-
     private func drawGlucose() -> some ChartContent {
     private func drawGlucose() -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
         ForEach(state.glucoseFromPersistence) { item in
-            let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
-
-            if state.smooth {
-                LineMark(
-                    x: .value("Time", item.date ?? Date()),
+            let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
+            let pointMarkColor: Color = glucoseToDisplay > state.highGlucose ? Color.orange :
+                glucoseToDisplay < state.lowGlucose ? Color.red :
+                Color.green
+
+            if !state.isSmoothingEnabled {
+                PointMark(
+                    x: .value("Time", item.date ?? Date(), unit: .second),
                     y: .value("Value", glucoseToDisplay)
                     y: .value("Value", glucoseToDisplay)
                 )
                 )
-                .foregroundStyle(
-                    .linearGradient(stops: stops, startPoint: .bottom, endPoint: .top)
-                )
-                .symbol(.circle).symbolSize(34)
+                .foregroundStyle(pointMarkColor)
+                .symbolSize(20)
             } else {
             } else {
-                if item.glucose > Int(state.highGlucose) {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", glucoseToDisplay)
-                    )
-                    .foregroundStyle(Color.orange.gradient)
-                    .symbolSize(20)
-                } else if item.glucose < Int(state.lowGlucose) {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", glucoseToDisplay)
-                    )
-                    .foregroundStyle(Color.red.gradient)
-                    .symbolSize(20)
-                } else {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", glucoseToDisplay)
-                    )
-                    .foregroundStyle(Color.green.gradient)
-                    .symbolSize(20)
+                PointMark(
+                    x: .value("Time", item.date ?? Date(), unit: .second),
+                    y: .value("Value", glucoseToDisplay)
+                )
+                .symbol {
+                    Image(systemName: "record.circle.fill")
+                        .font(.system(size: 8))
+                        .bold()
+                        .foregroundStyle(pointMarkColor)
                 }
                 }
             }
             }
         }
         }

+ 132 - 114
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -44,7 +44,7 @@ extension Home {
         @Published var pumpDisplayState: PumpDisplayState?
         @Published var pumpDisplayState: PumpDisplayState?
         @Published var alarm: GlucoseAlarm?
         @Published var alarm: GlucoseAlarm?
         @Published var manualTempBasal = false
         @Published var manualTempBasal = false
-        @Published var smooth = false
+        @Published var isSmoothingEnabled = false
         @Published var maxValue: Decimal = 1.2
         @Published var maxValue: Decimal = 1.2
         @Published var lowGlucose: Decimal = 70
         @Published var lowGlucose: Decimal = 70
         @Published var highGlucose: Decimal = 180
         @Published var highGlucose: Decimal = 180
@@ -59,12 +59,11 @@ extension Home {
         @Published var isLegendPresented: Bool = false
         @Published var isLegendPresented: Bool = false
         @Published var legendSheetDetent = PresentationDetent.large
         @Published var legendSheetDetent = PresentationDetent.large
         @Published var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         @Published var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
-        @Published var isTempTargetActive: Bool = false
         @Published var roundedTotalBolus: String = ""
         @Published var roundedTotalBolus: String = ""
         @Published var selectedTab: Int = 0
         @Published var selectedTab: Int = 0
         @Published var waitForSuggestion: Bool = false
         @Published var waitForSuggestion: Bool = false
         @Published var glucoseFromPersistence: [GlucoseStored] = []
         @Published var glucoseFromPersistence: [GlucoseStored] = []
-        @Published var manualGlucoseFromPersistence: [GlucoseStored] = []
+        @Published var latestTwoGlucoseValues: [GlucoseStored] = []
         @Published var carbsFromPersistence: [CarbEntryStored] = []
         @Published var carbsFromPersistence: [CarbEntryStored] = []
         @Published var fpusFromPersistence: [CarbEntryStored] = []
         @Published var fpusFromPersistence: [CarbEntryStored] = []
         @Published var determinationsFromPersistence: [OrefDetermination] = []
         @Published var determinationsFromPersistence: [OrefDetermination] = []
@@ -96,49 +95,116 @@ extension Home {
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
 
 
         override func subscribe() {
         override func subscribe() {
-            setupNotification()
             coreDataObserver = CoreDataObserver()
             coreDataObserver = CoreDataObserver()
-            registerHandlers()
-            setupGlucoseArray()
-            setupManualGlucoseArray()
-            setupCarbsArray()
-            setupFPUsArray()
-            setupDeterminationsArray()
-            setupInsulinArray()
-            setupLastBolus()
-            setupBatteryArray()
-            setupPumpSettings()
-            setupBasalProfile()
-            setupTempTargets()
-            setupReservoir()
-            setupAnnouncements()
-            setupCurrentPumpTimezone()
-            setupOverrides()
-            setupOverrideRunStored()
-
-            // TODO: isUploadEnabled the right var here??
-            uploadStats = settingsManager.settings.isUploadEnabled
-            units = settingsManager.settings.units
-            allowManualTemp = !settingsManager.settings.closedLoop
-            closedLoop = settingsManager.settings.closedLoop
-            lastLoopDate = apsManager.lastLoopDate
-            alarm = provider.glucoseStorage.alarm
-            manualTempBasal = apsManager.isManualTempBasal
-            setupCurrentTempTarget()
-            smooth = settingsManager.settings.smoothGlucose
-            maxValue = settingsManager.preferences.autosensMax
-            lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
-            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
-            overrideUnit = settingsManager.settings.overrideHbA1cUnit
-            displayXgridLines = settingsManager.settings.xGridLines
-            displayYgridLines = settingsManager.settings.yGridLines
-            thresholdLines = settingsManager.settings.rulerMarks
-            totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
-            cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
-            showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
 
 
-            forecastDisplayType = settingsManager.settings.forecastDisplayType
+            // Parallelize Setup functions
+            setupHomeViewConcurrently()
+        }
 
 
+        private func setupHomeViewConcurrently() {
+            Task {
+                await withTaskGroup(of: Void.self) { group in
+                    group.addTask {
+                        self.setupNotification()
+                    }
+                    group.addTask {
+                        self.registerHandlers()
+                    }
+                    group.addTask {
+                        self.setupGlucoseArray()
+                    }
+                    group.addTask {
+                        self.setupCarbsArray()
+                    }
+                    group.addTask {
+                        self.setupFPUsArray()
+                    }
+                    group.addTask {
+                        self.setupDeterminationsArray()
+                    }
+                    group.addTask {
+                        self.setupInsulinArray()
+                    }
+                    group.addTask {
+                        self.setupLastBolus()
+                    }
+                    group.addTask {
+                        self.setupBatteryArray()
+                    }
+                    group.addTask {
+                        self.setupPumpSettings()
+                    }
+                    group.addTask {
+                        self.setupBasalProfile()
+                    }
+                    group.addTask {
+                        self.setupTempTargets()
+                    }
+                    group.addTask {
+                        self.setupReservoir()
+                    }
+                    group.addTask {
+                        self.setupAnnouncements()
+                    }
+                    group.addTask {
+                        self.setupCurrentPumpTimezone()
+                    }
+                    group.addTask {
+                        self.setupOverrides()
+                    }
+                    group.addTask {
+                        self.setupOverrideRunStored()
+                    }
+                    group.addTask {
+                        await self.setupSettings()
+                    }
+                    group.addTask {
+                        self.registerObservers()
+                    }
+                }
+            }
+        }
+
+        private func registerHandlers() {
+            coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+                guard let self = self else { return }
+                self.setupDeterminationsArray()
+            }
+
+            coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+                guard let self = self else { return }
+                self.setupGlucoseArray()
+            }
+
+            coreDataObserver?.registerHandler(for: "CarbEntryStored") { [weak self] in
+                guard let self = self else { return }
+                self.setupCarbsArray()
+            }
+
+            coreDataObserver?.registerHandler(for: "PumpEventStored") { [weak self] in
+                guard let self = self else { return }
+                self.setupInsulinArray()
+                self.setupLastBolus()
+                self.displayPumpStatusHighlightMessage()
+            }
+
+            coreDataObserver?.registerHandler(for: "OpenAPS_Battery") { [weak self] in
+                guard let self = self else { return }
+                self.setupBatteryArray()
+            }
+
+            coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
+                guard let self = self else { return }
+                self.setupOverrides()
+            }
+
+            coreDataObserver?.registerHandler(for: "OverrideRunStored") { [weak self] in
+                guard let self = self else { return }
+                self.setupOverrideRunStored()
+            }
+        }
+
+        private func registerObservers() {
             broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
@@ -213,44 +279,26 @@ extension Home {
                 .store(in: &lifetime)
                 .store(in: &lifetime)
         }
         }
 
 
-        private func registerHandlers() {
-            coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
-                guard let self = self else { return }
-                self.setupDeterminationsArray()
-            }
-
-            coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
-                guard let self = self else { return }
-                self.setupGlucoseArray()
-                self.setupManualGlucoseArray()
-            }
-
-            coreDataObserver?.registerHandler(for: "CarbEntryStored") { [weak self] in
-                guard let self = self else { return }
-                self.setupCarbsArray()
-            }
-
-            coreDataObserver?.registerHandler(for: "PumpEventStored") { [weak self] in
-                guard let self = self else { return }
-                self.setupInsulinArray()
-                self.setupLastBolus()
-                self.displayPumpStatusHighlightMessage()
-            }
-
-            coreDataObserver?.registerHandler(for: "OpenAPS_Battery") { [weak self] in
-                guard let self = self else { return }
-                self.setupBatteryArray()
-            }
-
-            coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
-                guard let self = self else { return }
-                self.setupOverrides()
-            }
-
-            coreDataObserver?.registerHandler(for: "OverrideRunStored") { [weak self] in
-                guard let self = self else { return }
-                self.setupOverrideRunStored()
-            }
+        @MainActor private func setupSettings() async {
+            units = settingsManager.settings.units
+            allowManualTemp = !settingsManager.settings.closedLoop
+            closedLoop = settingsManager.settings.closedLoop
+            lastLoopDate = apsManager.lastLoopDate
+            alarm = provider.glucoseStorage.alarm
+            manualTempBasal = apsManager.isManualTempBasal
+            setupCurrentTempTarget()
+            isSmoothingEnabled = settingsManager.settings.smoothGlucose
+            maxValue = settingsManager.preferences.autosensMax
+            lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
+            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+            overrideUnit = settingsManager.settings.overrideHbA1cUnit
+            displayXgridLines = settingsManager.settings.xGridLines
+            displayYgridLines = settingsManager.settings.yGridLines
+            thresholdLines = settingsManager.settings.rulerMarks
+            totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
+            cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
+            showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
+            forecastDisplayType = settingsManager.settings.forecastDisplayType
         }
         }
 
 
         func addPump(_ type: PumpConfig.PumpType) {
         func addPump(_ type: PumpConfig.PumpType) {
@@ -445,7 +493,7 @@ extension Home.StateModel:
         closedLoop = settingsManager.settings.closedLoop
         closedLoop = settingsManager.settings.closedLoop
         units = settingsManager.settings.units
         units = settingsManager.settings.units
         manualTempBasal = apsManager.isManualTempBasal
         manualTempBasal = apsManager.isManualTempBasal
-        smooth = settingsManager.settings.smoothGlucose
+        isSmoothingEnabled = settingsManager.settings.smoothGlucose
         lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
         lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
         highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
         highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
         overrideUnit = settingsManager.settings.overrideHbA1cUnit
         overrideUnit = settingsManager.settings.overrideHbA1cUnit
@@ -566,7 +614,7 @@ extension Home.StateModel {
             onContext: context,
             onContext: context,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
             key: "date",
             key: "date",
-            ascending: false,
+            ascending: true,
             fetchLimit: 288
             fetchLimit: 288
         )
         )
 
 
@@ -579,37 +627,7 @@ extension Home.StateModel {
 
 
     @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
     @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
         glucoseFromPersistence = objects
         glucoseFromPersistence = objects
-    }
-
-    // Setup Manual Glucose
-    private func setupManualGlucoseArray() {
-        Task {
-            let ids = await self.fetchManualGlucose()
-            let manualGlucoseObjects: [GlucoseStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateManualGlucoseArray(with: manualGlucoseObjects)
-        }
-    }
-
-    private func fetchManualGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: context,
-            predicate: NSPredicate.manualGlucose,
-            key: "date",
-            ascending: false,
-            fetchLimit: 288
-        )
-
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateManualGlucoseArray(with objects: [GlucoseStored]) {
-        manualGlucoseFromPersistence = objects
+        latestTwoGlucoseValues = Array(objects.suffix(2))
     }
     }
 
 
     // Setup Carbs
     // Setup Carbs

+ 249 - 0
FreeAPS/Sources/Modules/Home/View/Chart/BasalChart.swift

@@ -0,0 +1,249 @@
+import Charts
+import Foundation
+import SwiftUI
+
+struct BasalProfile: Hashable {
+    let amount: Double
+    var isOverwritten: Bool
+    let startDate: Date
+    let endDate: Date?
+    init(amount: Double, isOverwritten: Bool, startDate: Date, endDate: Date? = nil) {
+        self.amount = amount
+        self.isOverwritten = isOverwritten
+        self.startDate = startDate
+        self.endDate = endDate
+    }
+}
+
+extension MainChartView {
+    var basalChart: some View {
+        VStack {
+            Chart {
+                drawStartRuleMark()
+                drawEndRuleMark()
+                drawCurrentTimeMarker()
+                drawTempBasals(dummy: false)
+                drawBasalProfile()
+                drawSuspensions()
+            }.onChange(of: state.tempBasals) { _ in
+                calculateBasals()
+                calculateTempBasalsInBackground()
+            }
+            .onChange(of: state.maxBasal) { _ in
+                calculateBasals()
+            }
+            .onChange(of: state.autotunedBasalProfile) { _ in
+                calculateBasals()
+            }
+            .onChange(of: state.basalProfile) { _ in
+                calculateBasals()
+            }
+            .frame(minHeight: geo.size.height * 0.05)
+            .frame(width: fullWidth(viewWidth: screenSize.width))
+            .chartXScale(domain: startMarker ... endMarker)
+            .chartXAxis { basalChartXAxis }
+            .chartXAxis(.hidden)
+            .chartYAxis(.hidden)
+            .chartPlotStyle { basalChartPlotStyle($0) }
+        }
+    }
+}
+
+// MARK: - Draw functions
+
+extension MainChartView {
+    func drawTempBasals(dummy: Bool) -> some ChartContent {
+        ForEach(preparedTempBasals, id: \.rate) { basal in
+            if dummy {
+                RectangleMark(
+                    xStart: .value("start", basal.start),
+                    xEnd: .value("end", basal.end),
+                    yStart: .value("rate-start", 0),
+                    yEnd: .value("rate-end", basal.rate)
+                ).foregroundStyle(Color.clear)
+
+                LineMark(x: .value("Start Date", basal.start), y: .value("Amount", basal.rate))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.clear)
+
+                LineMark(x: .value("End Date", basal.end), y: .value("Amount", basal.rate))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.clear)
+            } else {
+                RectangleMark(
+                    xStart: .value("start", basal.start),
+                    xEnd: .value("end", basal.end),
+                    yStart: .value("rate-start", 0),
+                    yEnd: .value("rate-end", basal.rate)
+                ).foregroundStyle(
+                    LinearGradient(
+                        gradient: Gradient(
+                            colors: [
+                                Color.insulin.opacity(0.6),
+                                Color.insulin.opacity(0.1)
+                            ]
+                        ),
+                        startPoint: .top,
+                        endPoint: .bottom
+                    )
+                )
+
+                LineMark(x: .value("Start Date", basal.start), y: .value("Amount", basal.rate))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+
+                LineMark(x: .value("End Date", basal.end), y: .value("Amount", basal.rate))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+            }
+        }
+    }
+
+    func drawBasalProfile() -> some ChartContent {
+        /// dashed profile line
+        ForEach(basalProfiles, id: \.self) { profile in
+            LineMark(
+                x: .value("Start Date", profile.startDate),
+                y: .value("Amount", profile.amount),
+                series: .value("profile", "profile")
+            ).lineStyle(.init(lineWidth: 2, dash: [2, 4])).foregroundStyle(Color.insulin)
+            LineMark(
+                x: .value("End Date", profile.endDate ?? endMarker),
+                y: .value("Amount", profile.amount),
+                series: .value("profile", "profile")
+            ).lineStyle(.init(lineWidth: 2.5, dash: [2, 4])).foregroundStyle(Color.insulin)
+        }
+    }
+
+    func drawSuspensions() -> some ChartContent {
+        let suspensions = state.suspensions
+        return ForEach(suspensions) { suspension in
+            let now = Date()
+
+            if let type = suspension.type, type == EventType.pumpSuspend.rawValue, let suspensionStart = suspension.timestamp {
+                let suspensionEnd = min(
+                    (
+                        suspensions
+                            .first(where: {
+                                $0.timestamp ?? now > suspensionStart && $0.type == EventType.pumpResume.rawValue })?
+                            .timestamp
+                    ) ?? now,
+                    now
+                )
+
+                let basalProfileDuringSuspension = basalProfiles.first(where: { $0.startDate <= suspensionStart })
+                let suspensionMarkHeight = basalProfileDuringSuspension?.amount ?? 1
+
+                RectangleMark(
+                    xStart: .value("start", suspensionStart),
+                    xEnd: .value("end", suspensionEnd),
+                    yStart: .value("suspend-start", 0),
+                    yEnd: .value("suspend-end", suspensionMarkHeight)
+                )
+                .foregroundStyle(Color.loopGray.opacity(colorScheme == .dark ? 0.3 : 0.8))
+            }
+        }
+    }
+}
+
+// MARK: - Calculation
+
+extension MainChartView {
+    func calculateTempBasalsInBackground() {
+        Task {
+            let basals = await prepareTempBasals()
+            await MainActor.run {
+                preparedTempBasals = basals
+            }
+        }
+    }
+
+    func prepareTempBasals() async -> [(start: Date, end: Date, rate: Double)] {
+        let now = Date()
+        let tempBasals = state.tempBasals
+
+        return tempBasals.compactMap { temp -> (start: Date, end: Date, rate: Double)? in
+            let duration = temp.tempBasal?.duration ?? 0
+            let timestamp = temp.timestamp ?? Date()
+            let end = min(timestamp + duration.minutes, now)
+            let isInsulinSuspended = state.suspensions.contains { $0.timestamp ?? now >= timestamp && $0.timestamp ?? now <= end }
+
+            let rate = Double(truncating: temp.tempBasal?.rate ?? Decimal.zero as NSDecimalNumber) * (isInsulinSuspended ? 0 : 1)
+
+            // Check if there's a subsequent temp basal to determine the end time
+            guard let nextTemp = state.tempBasals.first(where: { $0.timestamp ?? .distantPast > timestamp }) else {
+                return (timestamp, end, rate)
+            }
+            return (timestamp, nextTemp.timestamp ?? Date(), rate)
+        }
+    }
+
+    func findRegularBasalPoints(
+        timeBegin: TimeInterval,
+        timeEnd: TimeInterval,
+        autotuned: Bool
+    ) async -> [BasalProfile] {
+        guard timeBegin < timeEnd else { return [] }
+
+        let beginDate = Date(timeIntervalSince1970: timeBegin)
+        let startOfDay = Calendar.current.startOfDay(for: beginDate)
+        let profile = autotuned ? state.autotunedBasalProfile : state.basalProfile
+        var basalPoints: [BasalProfile] = []
+
+        // Iterate over the next three days, multiplying the time intervals
+        for dayOffset in 0 ..< 3 {
+            let dayTimeOffset = TimeInterval(dayOffset * 24 * 60 * 60) // One Day in seconds
+            for entry in profile {
+                let basalTime = startOfDay.addingTimeInterval(entry.minutes.minutes.timeInterval + dayTimeOffset)
+                let basalTimeInterval = basalTime.timeIntervalSince1970
+
+                // Only append points within the timeBegin and timeEnd range
+                if basalTimeInterval >= timeBegin, basalTimeInterval < timeEnd {
+                    basalPoints.append(BasalProfile(
+                        amount: Double(entry.rate),
+                        isOverwritten: false,
+                        startDate: basalTime
+                    ))
+                }
+            }
+        }
+
+        return basalPoints
+    }
+
+    func calculateBasals() {
+        Task {
+            let dayAgoTime = Date().addingTimeInterval(-1.days.timeInterval).timeIntervalSince1970
+
+            // Get Regular and Autotuned Basal parallel
+            async let getRegularBasalPoints = findRegularBasalPoints(
+                timeBegin: dayAgoTime,
+                timeEnd: endMarker.timeIntervalSince1970,
+                autotuned: false
+            )
+
+            async let getAutotunedBasalPoints = findRegularBasalPoints(
+                timeBegin: dayAgoTime,
+                timeEnd: endMarker.timeIntervalSince1970,
+                autotuned: true
+            )
+
+            let (regularPoints, autotunedBasalPoints) = await (getRegularBasalPoints, getAutotunedBasalPoints)
+
+            var totalBasal = regularPoints + autotunedBasalPoints
+            totalBasal.sort {
+                $0.startDate.timeIntervalSince1970 < $1.startDate.timeIntervalSince1970
+            }
+
+            var basals: [BasalProfile] = []
+            totalBasal.indices.forEach { index in
+                basals.append(BasalProfile(
+                    amount: totalBasal[index].amount,
+                    isOverwritten: totalBasal[index].isOverwritten,
+                    startDate: totalBasal[index].startDate,
+                    endDate: totalBasal.count > index + 1 ? totalBasal[index + 1].startDate : endMarker
+                ))
+            }
+
+            await MainActor.run {
+                basalProfiles = basals
+            }
+        }
+    }
+}

+ 63 - 0
FreeAPS/Sources/Modules/Home/View/Chart/CarbView.swift

@@ -0,0 +1,63 @@
+import Charts
+import Foundation
+import SwiftUI
+
+struct CarbView: ChartContent {
+    let glucoseData: [GlucoseStored]
+    let units: GlucoseUnits
+    let carbData: [CarbEntryStored]
+    let fpuData: [CarbEntryStored]
+    let minValue: Decimal
+
+    var body: some ChartContent {
+        drawCarbs()
+        drawFpus()
+    }
+
+    private func drawCarbs() -> some ChartContent {
+        ForEach(carbData) { carb in
+            let carbAmount = carb.carbs
+            let carbDate = carb.date ?? Date()
+
+            if let glucose = MainChartHelper.timeToNearestGlucose(
+                glucoseValues: glucoseData,
+                time: carbDate.timeIntervalSince1970
+            )?.glucose {
+                let yPosition = (units == .mgdL ? Decimal(glucose) : Decimal(glucose).asMmolL) - MainChartHelper
+                    .bolusOffset(units: units)
+                let size = min(
+                    MainChartHelper.Config.carbsSize + CGFloat(carbAmount) * MainChartHelper.Config.carbsScale,
+                    MainChartHelper.Config.maxCarbSize
+                )
+
+                PointMark(
+                    x: .value("Time", carbDate, unit: .second),
+                    y: .value("Value", yPosition)
+                )
+                .symbol {
+                    Image(systemName: "arrowtriangle.down.fill").font(.system(size: size)).foregroundStyle(Color.orange)
+                        .rotationEffect(.degrees(180))
+                }
+                .annotation(position: .bottom) {
+                    Text(MainChartHelper.carbsFormatter.string(from: carbAmount as NSNumber)!).font(.caption2)
+                        .foregroundStyle(Color.primary)
+                }
+            }
+        }
+    }
+
+    private func drawFpus() -> some ChartContent {
+        ForEach(fpuData, id: \.id) { fpu in
+            let fpuAmount = fpu.carbs
+            let size = (MainChartHelper.Config.fpuSize + CGFloat(fpuAmount) * MainChartHelper.Config.carbsScale) * 1.8
+            let yPosition = minValue
+
+            PointMark(
+                x: .value("Time", fpu.date ?? Date(), unit: .second),
+                y: .value("Value", yPosition)
+            )
+            .symbolSize(size)
+            .foregroundStyle(Color.brown)
+        }
+    }
+}

+ 67 - 0
FreeAPS/Sources/Modules/Home/View/Chart/CobChart.swift

@@ -0,0 +1,67 @@
+import Charts
+import Foundation
+import SwiftUI
+
+extension MainChartView {
+    var cobChart: some View {
+        Chart {
+            drawCurrentTimeMarker()
+            drawCOB(dummy: false)
+
+            if #available(iOS 17, *) {
+                if let selectedCOBValue {
+                    PointMark(
+                        x: .value("Time", selectedCOBValue.deliverAt ?? now, unit: .minute),
+                        y: .value("Value", selectedCOBValue.cob)
+                    )
+                    .symbolSize(CGSize(width: 15, height: 15))
+                    .foregroundStyle(Color.orange.opacity(0.8))
+
+                    PointMark(
+                        x: .value("Time", selectedCOBValue.deliverAt ?? now, unit: .minute),
+                        y: .value("Value", selectedCOBValue.cob)
+                    )
+                    .symbolSize(CGSize(width: 6, height: 6))
+                    .foregroundStyle(Color.primary)
+                }
+            }
+        }
+        .frame(minHeight: geo.size.height * 0.12)
+        .frame(width: fullWidth(viewWidth: screenSize.width))
+        .chartXScale(domain: startMarker ... endMarker)
+        .backport.chartXSelection(value: $selection)
+        .chartXAxis { basalChartXAxis }
+        .chartYAxis { cobChartYAxis }
+        .chartYScale(domain: minValueCobChart ... maxValueCobChart)
+    }
+
+    func drawCOB(dummy: Bool) -> some ChartContent {
+        ForEach(state.enactedAndNonEnactedDeterminations) { cob in
+            let amount = Int(cob.cob)
+            let date: Date = cob.deliverAt ?? Date()
+
+            if dummy {
+                LineMark(x: .value("Time", date), y: .value("Value", amount))
+                    .foregroundStyle(Color.clear)
+                AreaMark(x: .value("Time", date), y: .value("Value", amount)).foregroundStyle(
+                    Color.clear
+                )
+            } else {
+                LineMark(x: .value("Time", date), y: .value("Value", amount))
+                    .foregroundStyle(Color.orange.gradient)
+                AreaMark(x: .value("Time", date), y: .value("Value", amount)).foregroundStyle(
+                    LinearGradient(
+                        gradient: Gradient(
+                            colors: [
+                                Color.orange.opacity(0.8),
+                                Color.orange.opacity(0.01)
+                            ]
+                        ),
+                        startPoint: .top,
+                        endPoint: .bottom
+                    )
+                )
+            }
+        }
+    }
+}

+ 54 - 0
FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift

@@ -0,0 +1,54 @@
+import Charts
+import Foundation
+import SwiftUI
+
+extension MainChartView {
+    /// empty chart that just shows the Y axis and Y grid lines. Created separately from `mainChart` to allow main chart to scroll horizontally while having a fixed Y axis
+    var staticYAxisChart: some View {
+        Chart {
+            /// high and low threshold lines
+            if thresholdLines {
+                RuleMark(y: .value("High", highGlucose)).foregroundStyle(Color.loopYellow)
+                    .lineStyle(.init(lineWidth: 1, dash: [5]))
+                RuleMark(y: .value("Low", lowGlucose)).foregroundStyle(Color.loopRed)
+                    .lineStyle(.init(lineWidth: 1, dash: [5]))
+            }
+        }
+        .id("DummyMainChart")
+        .frame(minHeight: geo.size.height * 0.28)
+        .frame(width: screenSize.width - 10)
+        .chartXAxis { mainChartXAxis }
+        .chartXScale(domain: startMarker ... endMarker)
+        .chartXAxis(.hidden)
+        .chartYAxis { mainChartYAxis }
+        .chartYScale(domain: units == .mgdL ? minValue ... maxValue : minValue.asMmolL ... maxValue.asMmolL)
+        .chartLegend(.hidden)
+    }
+
+    var dummyBasalChart: some View {
+        Chart {}
+            .id("DummyBasalChart")
+            .frame(minHeight: geo.size.height * 0.05)
+            .frame(width: screenSize.width - 10)
+            .chartXAxis { basalChartXAxis }
+            .chartXAxis(.hidden)
+            .chartYAxis(.hidden)
+            .chartLegend(.hidden)
+    }
+
+    var dummyCobChart: some View {
+        Chart {
+            drawCOB(dummy: true)
+        }
+        .id("DummyCobChart")
+        .frame(minHeight: geo.size.height * 0.12)
+        .frame(width: screenSize.width - 10)
+        .chartXScale(domain: startMarker ... endMarker)
+        .chartXAxis { basalChartXAxis }
+        .chartXAxis(.hidden)
+        .chartYAxis { cobChartYAxis }
+        .chartYAxis(.hidden)
+        .chartYScale(domain: minValueCobChart ... maxValueCobChart)
+        .chartLegend(.hidden)
+    }
+}

+ 91 - 0
FreeAPS/Sources/Modules/Home/View/Chart/ForecastView.swift

@@ -0,0 +1,91 @@
+import Charts
+import Foundation
+import SwiftUI
+
+struct ForecastView: ChartContent {
+    let preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]
+    let minForecast: [Int]
+    let maxForecast: [Int]
+    let units: GlucoseUnits
+    let maxValue: Decimal
+    let forecastDisplayType: ForecastDisplayType
+
+    var body: some ChartContent {
+        if forecastDisplayType == .lines {
+            drawForecastsLines()
+        } else {
+            drawForecastsCone()
+        }
+    }
+
+    private func timeForIndex(_ index: Int32) -> Date {
+        let currentTime = Date()
+        let timeInterval = TimeInterval(index * 300)
+        return currentTime.addingTimeInterval(timeInterval)
+    }
+
+    private func drawForecastsCone() -> some ChartContent {
+        // Draw AreaMark for the forecast bounds
+        ForEach(0 ..< max(minForecast.count, maxForecast.count), id: \.self) { index in
+            if index < minForecast.count, index < maxForecast.count {
+                let yMinMaxDelta = Decimal(minForecast[index] - maxForecast[index])
+                let xValue = timeForIndex(Int32(index))
+
+                // if distance between respective min and max is 0, provide a default range
+                if yMinMaxDelta == 0 {
+                    let yMinValue = units == .mgdL ? Decimal(minForecast[index] - 1) :
+                        Decimal(minForecast[index] - 1)
+                        .asMmolL
+                    let yMaxValue = units == .mgdL ? Decimal(minForecast[index] + 1) :
+                        Decimal(minForecast[index] + 1)
+                        .asMmolL
+
+                    if xValue <= Date(timeIntervalSinceNow: TimeInterval(hours: 2.5)) {
+                        AreaMark(
+                            x: .value("Time", xValue),
+                            // maxValue is already parsed to user units, no need to parse
+                            yStart: .value("Min Value", yMinValue <= maxValue ? yMinValue : maxValue),
+                            yEnd: .value("Max Value", yMaxValue <= maxValue ? yMaxValue : maxValue)
+                        )
+                        .foregroundStyle(Color.blue.opacity(0.5))
+                        .interpolationMethod(.catmullRom)
+                    }
+                } else {
+                    let yMinValue = units == .mgdL ? Decimal(minForecast[index]) : Decimal(minForecast[index])
+                        .asMmolL
+                    let yMaxValue = units == .mgdL ? Decimal(maxForecast[index]) : Decimal(maxForecast[index])
+                        .asMmolL
+
+                    if xValue <= Date(timeIntervalSinceNow: TimeInterval(hours: 2.5)) {
+                        AreaMark(
+                            x: .value("Time", xValue),
+                            // maxValue is already parsed to user units, no need to parse
+                            yStart: .value("Min Value", yMinValue <= maxValue ? yMinValue : maxValue),
+                            yEnd: .value("Max Value", yMaxValue <= maxValue ? yMaxValue : maxValue)
+                        )
+                        .foregroundStyle(Color.blue.opacity(0.5))
+                        .interpolationMethod(.catmullRom)
+                    }
+                }
+            }
+        }
+    }
+
+    private func drawForecastsLines() -> some ChartContent {
+        ForEach(preprocessedData, id: \.id) { tuple in
+            let forecastValue = tuple.forecastValue
+            let forecast = tuple.forecast
+            let valueAsDecimal = Decimal(forecastValue.value)
+            let displayValue = units == .mmolL ? valueAsDecimal.asMmolL : valueAsDecimal
+            let xValue = timeForIndex(forecastValue.index)
+
+            if xValue <= Date(timeIntervalSinceNow: TimeInterval(hours: 2.5)) {
+                LineMark(
+                    x: .value("Time", xValue),
+                    y: .value("Value", displayValue)
+                )
+                .foregroundStyle(by: .value("Predictions", forecast.type ?? ""))
+            }
+        }
+    }
+}

+ 66 - 0
FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift

@@ -0,0 +1,66 @@
+import Charts
+import Foundation
+import SwiftUI
+
+struct GlucoseChartView: ChartContent {
+    let glucoseData: [GlucoseStored]
+    let units: GlucoseUnits
+    let highGlucose: Decimal
+    let lowGlucose: Decimal
+    let isSmoothingEnabled: Bool
+
+    var body: some ChartContent {
+        drawGlucoseChart()
+    }
+
+    private func drawGlucoseChart() -> some ChartContent {
+        ForEach(glucoseData) { item in
+            let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
+            let pointMarkColor: Color = glucoseToDisplay > highGlucose ? Color.orange :
+                glucoseToDisplay < lowGlucose ? Color.red :
+                Color.green
+
+            if !isSmoothingEnabled {
+                PointMark(
+                    x: .value("Time", item.date ?? Date(), unit: .second),
+                    y: .value("Value", glucoseToDisplay)
+                )
+                .foregroundStyle(pointMarkColor)
+                .symbolSize(20)
+                .symbol {
+                    if item.isManual {
+                        Image(systemName: "drop.fill")
+                            .font(.system(size: 10))
+                            .symbolRenderingMode(.monochrome)
+                            .bold()
+                            .foregroundStyle(.red)
+                    } else {
+                        Image(systemName: "circle.fill")
+                            .font(.system(size: 5))
+                            .bold()
+                            .foregroundStyle(pointMarkColor)
+                    }
+                }
+            } else {
+                PointMark(
+                    x: .value("Time", item.date ?? Date(), unit: .second),
+                    y: .value("Value", glucoseToDisplay)
+                )
+                .symbol {
+                    if item.isManual {
+                        Image(systemName: "drop.fill")
+                            .font(.system(size: 10))
+                            .symbolRenderingMode(.monochrome)
+                            .bold()
+                            .foregroundStyle(.red)
+                    } else {
+                        Image(systemName: "record.circle.fill")
+                            .font(.system(size: 8))
+                            .bold()
+                            .foregroundStyle(pointMarkColor)
+                    }
+                }
+            }
+        }
+    }
+}

+ 42 - 0
FreeAPS/Sources/Modules/Home/View/Chart/InsulinView.swift

@@ -0,0 +1,42 @@
+import Charts
+import Foundation
+import SwiftUI
+
+struct InsulinView: ChartContent {
+    let glucoseData: [GlucoseStored]
+    let insulinData: [PumpEventStored]
+    let units: GlucoseUnits
+
+    var body: some ChartContent {
+        drawBoluses()
+    }
+
+    private func drawBoluses() -> some ChartContent {
+        ForEach(insulinData) { insulin in
+            let amount = insulin.bolus?.amount ?? 0 as NSDecimalNumber
+            let bolusDate = insulin.timestamp ?? Date()
+
+            if amount != 0, let glucose = MainChartHelper.timeToNearestGlucose(
+                glucoseValues: glucoseData,
+                time: bolusDate.timeIntervalSince1970
+            )?.glucose {
+                let yPosition = (units == .mgdL ? Decimal(glucose) : Decimal(glucose).asMmolL) + MainChartHelper
+                    .bolusOffset(units: units)
+                let size = (MainChartHelper.Config.bolusSize + CGFloat(truncating: amount) * MainChartHelper.Config.bolusScale)
+
+                PointMark(
+                    x: .value("Time", bolusDate, unit: .second),
+                    y: .value("Value", yPosition)
+                )
+                .symbol {
+                    Image(systemName: "arrowtriangle.down.fill").font(.system(size: size)).foregroundStyle(Color.insulin)
+                }
+                .annotation(position: .top) {
+                    Text(MainChartHelper.bolusFormatter.string(from: amount) ?? "")
+                        .font(.caption2)
+                        .foregroundStyle(Color.primary)
+                }
+            }
+        }
+    }
+}

+ 63 - 0
FreeAPS/Sources/Modules/Home/View/Chart/IobChart.swift

@@ -0,0 +1,63 @@
+import Charts
+import Foundation
+import SwiftUI
+
+extension MainChartView {
+    var iobChart: some View {
+        VStack {
+            Chart {
+                drawIOB()
+
+                if #available(iOS 17, *) {
+                    if let selectedIOBValue {
+                        PointMark(
+                            x: .value("Time", selectedIOBValue.deliverAt ?? now, unit: .minute),
+                            y: .value("Value", Int(truncating: selectedIOBValue.iob ?? 0))
+                        )
+                        .symbolSize(CGSize(width: 15, height: 15))
+                        .foregroundStyle(Color.darkerBlue.opacity(0.8))
+
+                        PointMark(
+                            x: .value("Time", selectedIOBValue.deliverAt ?? now, unit: .minute),
+                            y: .value("Value", Int(truncating: selectedIOBValue.iob ?? 0))
+                        )
+                        .symbolSize(CGSize(width: 6, height: 6))
+                        .foregroundStyle(Color.primary)
+                    }
+                }
+            }
+            .frame(minHeight: geo.size.height * 0.12)
+            .frame(width: fullWidth(viewWidth: screenSize.width))
+            .chartXScale(domain: startMarker ... endMarker)
+            .backport.chartXSelection(value: $selection)
+            .chartXAxis { basalChartXAxis }
+            .chartYAxis { cobChartYAxis }
+            .chartYScale(domain: minValueIobChart ... maxValueIobChart)
+            .chartYAxis(.hidden)
+        }
+    }
+
+    func drawIOB() -> some ChartContent {
+        ForEach(state.enactedAndNonEnactedDeterminations) { iob in
+            let rawAmount = iob.iob?.doubleValue ?? 0
+            let amount: Double = rawAmount > 0 ? rawAmount : rawAmount * 2 // weigh negative iob with factor 2
+            let date: Date = iob.deliverAt ?? Date()
+
+            LineMark(x: .value("Time", date), y: .value("Amount", amount))
+                .foregroundStyle(Color.darkerBlue)
+            AreaMark(x: .value("Time", date), y: .value("Amount", amount))
+                .foregroundStyle(
+                    LinearGradient(
+                        gradient: Gradient(
+                            colors: [
+                                Color.darkerBlue.opacity(0.8),
+                                Color.darkerBlue.opacity(0.01)
+                            ]
+                        ),
+                        startPoint: .top,
+                        endPoint: .bottom
+                    )
+                )
+        }
+    }
+}

Разлика између датотеке није приказан због своје велике величине
+ 171 - 1001
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift


+ 50 - 0
FreeAPS/Sources/Modules/Home/View/Chart/OverrideView.swift

@@ -0,0 +1,50 @@
+import Charts
+import CoreData
+import Foundation
+import SwiftUI
+
+struct OverrideView: ChartContent {
+    let overrides: [OverrideStored]
+    let overrideRunStored: [OverrideRunStored]
+    let units: GlucoseUnits
+    let viewContext: NSManagedObjectContext
+
+    var body: some ChartContent {
+        drawActiveOverrides()
+        drawOverrideRunStored()
+    }
+
+    private func drawActiveOverrides() -> some ChartContent {
+        ForEach(overrides) { override in
+            if let duration = MainChartHelper.calculateDuration(objectID: override.objectID, context: viewContext) {
+                let start: Date = override.date ?? .distantPast
+                let end: Date = start.addingTimeInterval(duration)
+
+                if let target = MainChartHelper.calculateTarget(objectID: override.objectID, context: viewContext) {
+                    RuleMark(
+                        xStart: .value("Start", start, unit: .second),
+                        xEnd: .value("End", end, unit: .second),
+                        y: .value("Value", units == .mgdL ? target : target.asMmolL)
+                    )
+                    .foregroundStyle(Color.purple.opacity(0.4))
+                    .lineStyle(.init(lineWidth: 8))
+                }
+            }
+        }
+    }
+
+    private func drawOverrideRunStored() -> some ChartContent {
+        ForEach(overrideRunStored) { overrideRunStored in
+            let start: Date = overrideRunStored.startDate ?? .distantPast
+            let end: Date = overrideRunStored.endDate ?? Date()
+            let target = overrideRunStored.target?.decimalValue ?? 100
+            RuleMark(
+                xStart: .value("Start", start, unit: .second),
+                xEnd: .value("End", end, unit: .second),
+                y: .value("Value", units == .mgdL ? target : target.asMmolL)
+            )
+            .foregroundStyle(Color.purple.opacity(0.25))
+            .lineStyle(.init(lineWidth: 8))
+        }
+    }
+}

+ 82 - 0
FreeAPS/Sources/Modules/Home/View/Chart/TempTargets.swift

@@ -0,0 +1,82 @@
+import Charts
+import Foundation
+import SwiftUI
+
+struct ChartTempTarget: Hashable {
+    let amount: Decimal
+    let start: Date
+    let end: Date
+}
+
+extension MainChartView {
+    func drawTempTargets() -> some ChartContent {
+        ForEach(chartTempTargets, id: \.self) { target in
+            let targetLimited = min(max(target.amount, 0), upperLimit)
+
+            RuleMark(
+                xStart: .value("Start", target.start),
+                xEnd: .value("End", target.end),
+                y: .value("Value", targetLimited)
+            )
+            .foregroundStyle(Color.purple.opacity(0.75)).lineStyle(.init(lineWidth: 8))
+        }
+    }
+
+    // Calculations for temp target bar mark
+    func calculateTempTargets() async {
+        // Perform calculations off the main thread
+        let calculatedTTs = await Task.detached { () -> [ChartTempTarget] in
+            var groupedPackages: [[TempTarget]] = []
+            var currentPackage: [TempTarget] = []
+            var calculatedTTs: [ChartTempTarget] = []
+
+            for target in await tempTargets {
+                if target.duration > 0 {
+                    if !currentPackage.isEmpty {
+                        groupedPackages.append(currentPackage)
+                        currentPackage = []
+                    }
+                    currentPackage.append(target)
+                } else if let lastNonZeroTempTarget = currentPackage.last(where: { $0.duration > 0 }) {
+                    // Ensure this cancel target is within the valid time range
+                    if target.createdAt >= lastNonZeroTempTarget.createdAt,
+                       target.createdAt <= lastNonZeroTempTarget.createdAt
+                       .addingTimeInterval(TimeInterval(lastNonZeroTempTarget.duration * 60))
+                    {
+                        currentPackage.append(target)
+                    }
+                }
+            }
+
+            // Append the last group, if any
+            if !currentPackage.isEmpty {
+                groupedPackages.append(currentPackage)
+            }
+
+            for package in groupedPackages {
+                guard let firstNonZeroTarget = package.first(where: { $0.duration > 0 }) else { continue }
+
+                var end = firstNonZeroTarget.createdAt.addingTimeInterval(TimeInterval(firstNonZeroTarget.duration * 60))
+
+                let earliestCancelTarget = package.filter({ $0.duration == 0 }).min(by: { $0.createdAt < $1.createdAt })
+
+                if let earliestCancelTarget = earliestCancelTarget {
+                    end = min(earliestCancelTarget.createdAt, end)
+                }
+
+                if let targetTop = firstNonZeroTarget.targetTop {
+                    let adjustedTarget = await units == .mgdL ? targetTop : targetTop.asMmolL
+                    calculatedTTs
+                        .append(ChartTempTarget(amount: adjustedTarget, start: firstNonZeroTarget.createdAt, end: end))
+                }
+            }
+
+            return calculatedTTs
+        }.value
+
+        // Update chartTempTargets on the main thread
+        await MainActor.run {
+            self.chartTempTargets = calculatedTTs
+        }
+    }
+}

+ 11 - 16
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -9,8 +9,7 @@ struct CurrentGlucoseView: View {
     @Binding var highGlucose: Decimal
     @Binding var highGlucose: Decimal
     @Binding var cgmAvailable: Bool
     @Binding var cgmAvailable: Bool
 
 
-    var glucose: [GlucoseStored]
-    var manualGlucose: [GlucoseStored]
+    var glucose: [GlucoseStored] // This contains the last two glucose values, no matter if its manual or a cgm reading
 
 
     @State private var rotationDegrees: Double = 0.0
     @State private var rotationDegrees: Double = 0.0
     @State private var angularGradient = AngularGradient(colors: [
     @State private var angularGradient = AngularGradient(colors: [
@@ -24,12 +23,6 @@ struct CurrentGlucoseView: View {
 
 
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
 
 
-    private var combinedGlucoseValues: [GlucoseStored] {
-        // Combine and sort the glucose values
-        let combined = (glucose + manualGlucose).sorted { $0.date ?? Date() > $1.date ?? Date() }
-        return combined
-    }
-
     private var glucoseFormatter: NumberFormatter {
     private var glucoseFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
         formatter.numberStyle = .decimal
@@ -83,7 +76,7 @@ struct CurrentGlucoseView: View {
 
 
                 VStack(alignment: .center) {
                 VStack(alignment: .center) {
                     HStack {
                     HStack {
-                        if let glucoseValue = combinedGlucoseValues.first?.glucose {
+                        if let glucoseValue = glucose.last?.glucose {
                             let displayGlucose = units == .mgdL ? Decimal(glucoseValue).description : Decimal(glucoseValue)
                             let displayGlucose = units == .mgdL ? Decimal(glucoseValue).description : Decimal(glucoseValue)
                                 .formattedAsMmolL
                                 .formattedAsMmolL
                             Text(
                             Text(
@@ -98,7 +91,7 @@ struct CurrentGlucoseView: View {
                         }
                         }
                     }
                     }
                     HStack {
                     HStack {
-                        let minutesAgo = -1 * (combinedGlucoseValues.first?.date?.timeIntervalSinceNow ?? 0) / 60
+                        let minutesAgo = -1 * (glucose.last?.date?.timeIntervalSinceNow ?? 0) / 60
                         let text = timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
                         let text = timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
                         Text(
                         Text(
                             minutesAgo <= 1 ? "< 1 " + NSLocalizedString("min", comment: "Short form for minutes") : (
                             minutesAgo <= 1 ? "< 1 " + NSLocalizedString("min", comment: "Short form for minutes") : (
@@ -115,7 +108,7 @@ struct CurrentGlucoseView: View {
                     }.frame(alignment: .top)
                     }.frame(alignment: .top)
                 }
                 }
             }
             }
-            .onChange(of: combinedGlucoseValues.first?.directionEnum) { newDirection in
+            .onChange(of: glucose.last?.directionEnum) { newDirection in
                 withAnimation {
                 withAnimation {
                     switch newDirection {
                     switch newDirection {
                     case .doubleUp,
                     case .doubleUp,
@@ -156,20 +149,22 @@ struct CurrentGlucoseView: View {
     }
     }
 
 
     private var delta: String {
     private var delta: String {
-        guard combinedGlucoseValues.count >= 2 else {
+        guard glucose.count >= 2 else {
             return "--"
             return "--"
         }
         }
 
 
-        let lastGlucose = combinedGlucoseValues.first?.glucose ?? 0
-        let secondLastGlucose = combinedGlucoseValues.dropFirst().first?.glucose ?? 0
+        let lastGlucose = glucose.last?.glucose ?? 0
+        let secondLastGlucose = glucose.first?.glucose ?? 0
         let delta = lastGlucose - secondLastGlucose
         let delta = lastGlucose - secondLastGlucose
         let deltaAsDecimal = units == .mmolL ? Decimal(delta).asMmolL : Decimal(delta)
         let deltaAsDecimal = units == .mmolL ? Decimal(delta).asMmolL : Decimal(delta)
         return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
         return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
     }
     }
 
 
     var glucoseDisplayColor: Color {
     var glucoseDisplayColor: Color {
-        // Fetch the first glucose reading and convert it to Int for comparison
-        let whichGlucose = Int(combinedGlucoseValues.first?.glucose ?? 0)
+        guard let lastGlucose = glucose.last?.glucose else { return .primary }
+
+        // Convert the lastest glucose value to Int for comparison
+        let whichGlucose = Int(lastGlucose)
 
 
         // Define default color based on the color scheme
         // Define default color based on the color scheme
         let defaultColor: Color = colorScheme == .dark ? .white : .black
         let defaultColor: Color = colorScheme == .dark ? .white : .black

+ 3 - 9
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -136,8 +136,7 @@ extension Home {
                 lowGlucose: $state.lowGlucose,
                 lowGlucose: $state.lowGlucose,
                 highGlucose: $state.highGlucose,
                 highGlucose: $state.highGlucose,
                 cgmAvailable: $state.cgmAvailable,
                 cgmAvailable: $state.cgmAvailable,
-                glucose: state.glucoseFromPersistence,
-                manualGlucose: state.manualGlucoseFromPersistence
+                glucose: state.latestTwoGlucoseValues
             ).scaleEffect(0.9)
             ).scaleEffect(0.9)
                 .onTapGesture {
                 .onTapGesture {
                     state.openCGM()
                     state.openCGM()
@@ -338,20 +337,14 @@ extension Home {
                 MainChartView(
                 MainChartView(
                     geo: geo,
                     geo: geo,
                     units: $state.units,
                     units: $state.units,
-                    announcement: $state.announcement,
                     hours: .constant(state.filteredHours),
                     hours: .constant(state.filteredHours),
-                    maxBasal: $state.maxBasal,
-                    autotunedBasalProfile: $state.autotunedBasalProfile,
-                    basalProfile: $state.basalProfile,
                     tempTargets: $state.tempTargets,
                     tempTargets: $state.tempTargets,
-                    smooth: $state.smooth,
                     highGlucose: $state.highGlucose,
                     highGlucose: $state.highGlucose,
                     lowGlucose: $state.lowGlucose,
                     lowGlucose: $state.lowGlucose,
                     screenHours: $state.hours,
                     screenHours: $state.hours,
                     displayXgridLines: $state.displayXgridLines,
                     displayXgridLines: $state.displayXgridLines,
                     displayYgridLines: $state.displayYgridLines,
                     displayYgridLines: $state.displayYgridLines,
                     thresholdLines: $state.thresholdLines,
                     thresholdLines: $state.thresholdLines,
-                    isTempTargetActive: $state.isTempTargetActive,
                     state: state
                     state: state
                 )
                 )
             }
             }
@@ -957,7 +950,8 @@ extension Home {
                         Text("Invalid CGM reading (HIGH).").font(.callout).bold().foregroundColor(.loopRed).padding(.top, 8)
                         Text("Invalid CGM reading (HIGH).").font(.callout).bold().foregroundColor(.loopRed).padding(.top, 8)
                         Text("SMBs and High Temps Disabled.").font(.caption).foregroundColor(.white).padding(.bottom, 4)
                         Text("SMBs and High Temps Disabled.").font(.caption).foregroundColor(.white).padding(.bottom, 4)
                     } else {
                     } else {
-                        var tags = !state.smooth ? determination.reasonParts : determination.reasonParts + ["Smoothing: On"]
+                        let tags = !state.isSmoothingEnabled ? determination.reasonParts : determination
+                            .reasonParts + ["Smoothing: On"]
                         TagCloudView(
                         TagCloudView(
                             tags: tags,
                             tags: tags,
                             shouldParseToMmolL: state.units == .mmolL
                             shouldParseToMmolL: state.units == .mmolL

+ 3 - 2
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -180,7 +180,7 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             propertiesToFetch: ["timestamp", "cob", "iob", "objectID"]
             propertiesToFetch: ["timestamp", "cob", "iob", "objectID"]
         )
         )
 
 
-        guard let fetchedResults = results as? [[String: Any]], !fetchedResults.isEmpty else { return nil }
+        guard let fetchedResults = results as? [[String: Any]] /* , !fetchedResults.isEmpty */ else { return nil }
 
 
         return await backgroundContext.perform {
         return await backgroundContext.perform {
             return fetchedResults.first?["objectID"] as? NSManagedObjectID
             return fetchedResults.first?["objectID"] as? NSManagedObjectID
@@ -193,7 +193,8 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor30MinAgo,
             predicate: NSPredicate.predicateFor30MinAgo,
             key: "date",
             key: "date",
-            ascending: false
+            ascending: false,
+            propertiesToFetch: ["objectID", "glucose"]
         )
         )
 
 
         guard let fetchedResults = results as? [[String: Any]] else { return [] }
         guard let fetchedResults = results as? [[String: Any]] else { return [] }

+ 1 - 1
FreeAPS/Sources/Services/UnlockManager/UnlockManager.swift

@@ -10,7 +10,7 @@ struct UnlockError: Error {
 }
 }
 
 
 final class BaseUnlockManager: UnlockManager {
 final class BaseUnlockManager: UnlockManager {
-    func unlock() async throws -> Bool {
+    @MainActor func unlock() async throws -> Bool {
         let context = LAContext()
         let context = LAContext()
         let reason = "We need to make sure you are the owner of the device."
         let reason = "We need to make sure you are the owner of the device."
 
 

+ 39 - 0
FreeAPS/Sources/Views/ViewModifiers.swift

@@ -162,4 +162,43 @@ extension View {
     }
     }
 
 
     func asAny() -> AnyView { .init(self) }
     func asAny() -> AnyView { .init(self) }
+
+    var backport: Backport<Self> { Backport(content: self) }
+}
+
+struct Backport<Content: View> {
+    let content: Content
+}
+
+extension Backport {
+    @ViewBuilder func chartXSelection(value: Binding<Date?>) -> some View {
+        if #available(iOS 17, *) {
+            content.chartXSelection(value: value)
+        } else {
+            content
+        }
+    }
+
+    @ViewBuilder func chartForegroundStyleScale(state: any StateModel) -> some View {
+        if (state as? Bolus.StateModel)?.forecastDisplayType == ForecastDisplayType.lines ||
+            (state as? Home.StateModel)?.forecastDisplayType == ForecastDisplayType.lines
+        {
+            let modifiedContent = content
+                .chartForegroundStyleScale([
+                    "iob": .blue,
+                    "uam": Color.uam,
+                    "zt": Color.zt,
+                    "cob": .orange
+                ])
+
+            if state is Home.StateModel {
+                modifiedContent
+                    .chartLegend(.hidden)
+            } else {
+                modifiedContent
+            }
+        } else {
+            content
+        }
+    }
 }
 }

+ 2 - 2
Model/CoreDataStack.swift

@@ -409,7 +409,7 @@ extension CoreDataStack {
         with ids: [NSManagedObjectID],
         with ids: [NSManagedObjectID],
         context: NSManagedObjectContext
         context: NSManagedObjectContext
     ) async -> [T] {
     ) async -> [T] {
-        await Task { () -> [T] in
+        await context.perform {
             var objects = [T]()
             var objects = [T]()
             do {
             do {
                 for id in ids {
                 for id in ids {
@@ -421,7 +421,7 @@ extension CoreDataStack {
                 debugPrint("Failed to fetch objects: \(error.localizedDescription)")
                 debugPrint("Failed to fetch objects: \(error.localizedDescription)")
             }
             }
             return objects
             return objects
-        }.value
+        }
     }
     }
 }
 }
 
 

+ 1 - 1
Model/Helper/GlucoseStored+helper.swift

@@ -32,7 +32,7 @@ extension GlucoseStored {
 extension NSPredicate {
 extension NSPredicate {
     static var glucose: NSPredicate {
     static var glucose: NSPredicate {
         let date = Date.oneDayAgo
         let date = Date.oneDayAgo
-        return NSPredicate(format: "isManual == %@ AND date >= %@", false as NSNumber, date as NSDate)
+        return NSPredicate(format: "date >= %@", date as NSDate)
     }
     }
 
 
     static var manualGlucose: NSPredicate {
     static var manualGlucose: NSPredicate {

+ 1 - 1
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
 {
-  "originHash" : "f5c836c216c4ca7d356e3777e58d6d4f9502b03f3974891349eb775f4c4cf750",
+  "originHash" : "59ac7eba66375d6eb406e758cb0b9964f4b3b0ae45c5665596f00384c32262b9",
   "pins" : [
   "pins" : [
     {
     {
       "identity" : "cryptoswift",
       "identity" : "cryptoswift",