polscm32 1 год назад
Родитель
Сommit
db4d2359e5

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -261,6 +261,7 @@
 		5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5887527B2BD986E1008B081D /* OpenAPSBattery.swift */; };
 		58CE8B892C8C6B62007A6A10 /* GradientStops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE8B882C8C6B62007A6A10 /* GradientStops.swift */; };
 		58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B212C8DAA8E00AA37D3 /* OverrideView.swift */; };
+		58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B2F2C8DEA7500AA37D3 /* ForecastView.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 */; };
 		5A2325542BFCBF66003518CA /* NightscoutFetchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */; };
@@ -909,6 +910,7 @@
 		5887527B2BD986E1008B081D /* OpenAPSBattery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPSBattery.swift; sourceTree = "<group>"; };
 		58CE8B882C8C6B62007A6A10 /* GradientStops.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientStops.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>"; };
 		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>"; };
 		5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutFetchView.swift; sourceTree = "<group>"; };
@@ -1774,6 +1776,7 @@
 				582DF9762C8CDBE7001F516D /* InsulinView.swift */,
 				582DF97A2C8CE209001F516D /* CarbView.swift */,
 				58D08B212C8DAA8E00AA37D3 /* OverrideView.swift */,
+				58D08B2F2C8DEA7500AA37D3 /* ForecastView.swift */,
 			);
 			path = Chart;
 			sourceTree = "<group>";
@@ -3356,6 +3359,7 @@
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
+				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */,

+ 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 ?? ""))
+            }
+        }
+    }
+}

+ 8 - 74
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -251,11 +251,14 @@ extension MainChartView {
                     viewContext: context
                 )
 
-                if state.forecastDisplayType == .lines {
-                    drawForecastsLines()
-                } else {
-                    drawForecastsCone()
-                }
+                ForecastView(
+                    preprocessedData: state.preprocessedData,
+                    minForecast: state.minForecast,
+                    maxForecast: state.maxForecast,
+                    units: state.units,
+                    maxValue: maxValue,
+                    forecastDisplayType: state.forecastDisplayType
+                )
 
                 /// show glucose value when hovering over it
                 if #available(iOS 17, *) {
@@ -476,75 +479,6 @@ extension MainChartView {
 // MARK: - Calculations
 
 extension MainChartView {
-    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(state.minForecast.count, state.maxForecast.count), id: \.self) { index in
-            if index < state.minForecast.count, index < state.maxForecast.count {
-                let yMinMaxDelta = Decimal(state.minForecast[index] - state.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(state.minForecast[index] - 1) :
-                        Decimal(state.minForecast[index] - 1)
-                        .asMmolL
-                    let yMaxValue = units == .mgdL ? Decimal(state.minForecast[index] + 1) :
-                        Decimal(state.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(state.minForecast[index]) : Decimal(state.minForecast[index]).asMmolL
-                    let yMaxValue = units == .mgdL ? Decimal(state.maxForecast[index]) : Decimal(state.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(state.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 ?? ""))
-            }
-        }
-    }
-
     private func drawCurrentTimeMarker() -> some ChartContent {
         RuleMark(
             x: .value(