Просмотр исходного кода

Mainchart rework, some Fixes for current dev (#10)

* carbs/fpus refactoring

* remove uncommented code

* refactor boluses

* predictions refactoring

* legendpanel refactoring

* move filtering for carbs and fpus to state

* logic fix for adding fpu

* fix deleting of manual glucose/carbs/fpus

* re-add custom progress view when deleting insulin/ carb entries from history

* ensure that externalinsulin function runs on the main thread, chart labels in Color.primary

* bigger chart labels, revert label color, rule mark in systemGray2

* padding in home screen

* add error handling when user cancels face id check after enacting bolus

* add rectangle to button

* make fonts for chart labels dynamic

* adjust font and color of bolus entry textfield

* extract logic for button in combined view, refactoring

* refactoring

* more refactoring

* fix chart offset

* use enum for custom progress view loading text

* rename variable in mainchart

* add 'g' to badge to indicate grams, remove background in tabbar by using nav stack also for mainview

* Address review comments from dnzxy

* Adhere to Apple's font choices

* Remove obsolete property from custom control DecimalTextField

---------

Co-authored-by: Andreas Stokholm <andreas@stokholm.me>
Co-authored-by: dnzxy <d.c.cengiz@googlemail.com>
polscm32 2 лет назад
Родитель
Сommit
baf863cbfb

+ 8 - 0
FreeAPS/Sources/Helpers/CustomProgressView.swift

@@ -42,3 +42,11 @@ struct CustomProgressView: View {
         }
     }
 }
+
+enum ProgressText: String {
+    case updatingIOB = "Updating IOB ..."
+    case updatingCOB = "Updating COB ..."
+    case updatingHistory = "Updating History ..."
+    case updatingTreatments = "Updating Treatments ..."
+    case updatingIOBandCOB = "Updating IOB and COB ..."
+}

+ 39 - 4
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -238,7 +238,30 @@ extension Bolus {
                 .roundBolus(amount: max(insulinCalculated, 0))
         }
 
-        func add() async {
+        @MainActor func invokeTreatmentsTask() {
+            Task {
+                if amount > 0 {
+                    if !externalInsulin {
+                        await add()
+                    } else {
+                        do {
+                            await addExternalInsulin()
+                        }
+                    }
+                    waitForSuggestion = true
+                } else {
+                    if carbs > 0 {
+                        waitForSuggestion = true
+                    } else {
+                        hideModal()
+                    }
+                }
+                addCarbs()
+                addButtonPressed = true
+            }
+        }
+
+        @MainActor func add() async {
             guard amount > 0 else {
                 showModal(for: nil)
                 return
@@ -254,7 +277,13 @@ extension Bolus {
                     print("authentication failed")
                 }
             } catch {
-                print("authentication error: \(error.localizedDescription)")
+                print("authentication error for pump bolus: \(error.localizedDescription)")
+                DispatchQueue.main.async {
+                    self.waitForSuggestion = false
+                    if self.addButtonPressed {
+                        self.hideModal()
+                    }
+                }
             }
         }
 
@@ -474,7 +503,7 @@ extension Bolus {
 
         // MARK: EXTERNAL INSULIN
 
-        func addExternalInsulin() async {
+        @MainActor func addExternalInsulin() async {
             guard amount > 0 else {
                 showModal(for: nil)
                 return
@@ -490,7 +519,13 @@ extension Bolus {
                     print("authentication failed")
                 }
             } catch {
-                print("authentication error: \(error.localizedDescription)")
+                print("authentication error for external insulin: \(error.localizedDescription)")
+                DispatchQueue.main.async {
+                    self.waitForSuggestion = false
+                    if self.addButtonPressed {
+                        self.hideModal()
+                    }
+                }
             }
         }
 

+ 53 - 86
FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift

@@ -1,5 +1,6 @@
 import Charts
 import CoreData
+import LoopKitUI
 import SwiftUI
 import Swinject
 
@@ -11,7 +12,6 @@ extension Bolus {
 
         @State private var showInfo = false
         @State private var showAlert = false
-        @State private var exceededMaxBolus = false
         @State private var autofocus: Bool = true
         @State private var calculatorDetent = PresentationDetent.medium
         @State var pushed = false
@@ -423,17 +423,11 @@ extension Bolus {
                                     value: $state.amount,
                                     formatter: formatter,
                                     autofocus: false,
-                                    cleanInput: true
+                                    cleanInput: true,
+                                    textColor: .systemBlue
                                 )
                                 Text(" U").foregroundColor(.secondary)
                             }
-                            .onChange(of: state.amount) { newValue in
-                                if newValue > state.maxBolus {
-                                    exceededMaxBolus = true
-                                } else {
-                                    exceededMaxBolus = false
-                                }
-                            }
                         }.listRowBackground(Color.chart)
 
                         if state.amount > 0 {
@@ -451,7 +445,7 @@ extension Bolus {
                 }.blur(radius: state.waitForSuggestion ? 5 : 0)
 
                 if state.waitForSuggestion {
-                    CustomProgressView(text: progressText)
+                    CustomProgressView(text: progressText.rawValue)
                 }
             }
             .scrollContentBackground(.hidden).background(color)
@@ -484,70 +478,50 @@ extension Bolus {
             }
         }
 
-        var progressText: String {
+        var progressText: ProgressText {
             switch (state.amount > 0, state.carbs > 0) {
             case (true, true):
-                return "Updating COB and IOB..."
+                return .updatingIOBandCOB
             case (false, true):
-                return "Updating COB..."
+                return .updatingCOB
             case (true, false):
-                return "Updating IOB..."
+                return .updatingIOB
             default:
-                return "Updating Treatments..."
+                return .updatingTreatments
             }
         }
 
         var stickyButton: some View {
-            Section {
-                Button {
-                    if state.amount > 0 {
-                        if !state.externalInsulin {
-                            Task {
-                                await state.add()
-                                state.waitForSuggestion = true
-                            }
-                        } else {
-                            Task {
-                                do {
-                                    await state.addExternalInsulin()
-                                    state.waitForSuggestion = true
-                                }
-                            }
-                        }
-                        state.addCarbs()
-                        state.addButtonPressed = true
-                    } else {
-                        // show loading bar only when carbs are actually added
-                        if state.carbs > 0 {
-                            state.addCarbs()
-                            state.waitForSuggestion = true
-                        } else {
-                            // hide modal because its otherwise only hidden after a suggestion update, see StateModal
-                            state.hideModal()
-                        }
-                        state.addButtonPressed = true
-                    }
-                } label: {
-                    if state.amount > 0 {
-                        Text(
-                            !state
-                                .externalInsulin ? (exceededMaxBolus ? "Max Bolus exceeded!" : "Enact bolus") :
-                                (exceededMaxBolus ? "Max Bolus exceeded!" : "Log external insulin")
-                        ).font(.system(size: 17, design: .rounded))
-                    } else {
-                        Text("Continue without bolus").font(.system(size: 17, design: .rounded))
+            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)
+
+                Section {
+                    Button {
+                        state.invokeTreatmentsTask()
+                    } label: {
+                        taskButtonLabel
                     }
-                }
-                .frame(maxWidth: .infinity, alignment: .center)
-                .frame(minHeight: 50)
-                .disabled(state.amount > 0 ? (state.externalInsulin ? limitManualBolus : limitPumpBolus) : false)
-                .background(state.amount > 0 ? logExternalInsulinBackground : Color(.systemBlue))
-                .shadow(radius: 3)
-                .clipShape(RoundedRectangle(cornerRadius: 8))
-                .foregroundStyle(state.amount > 0 ? logExternalInsulinForeground : .white)
-                .padding()
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .frame(minHeight: 50)
+                    .disabled(disableTaskButton)
+                    .background(
+                        (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) ? Color(.systemRed) :
+                            Color(.systemBlue)
+                    )
+                    .shadow(radius: 3)
+                    .clipShape(RoundedRectangle(cornerRadius: 8))
+                    .foregroundStyle(Color.white)
+                    .padding()
+                }.offset(y: 20)
+                    .listRowBackground(Color.chart)
             }
-            .listRowBackground(Color.chart)
         }
 
         var calcSettingsFirstRow: some View {
@@ -983,7 +957,7 @@ extension Bolus {
                         .padding(.top)
                 }
                 .padding([.horizontal, .bottom])
-                .font(.system(size: 15))
+                .font(.subheadline)
             }
         }
 
@@ -992,34 +966,27 @@ extension Bolus {
             return Decimal(floor(100 * toRound) / 100)
         }
 
-        private var limitPumpBolus: Bool {
-            state.amount <= 0 || state.amount > state.maxBolus
+        private var taskButtonLabel: some View {
+            if state.amount > 0 {
+                Text(
+                    !state.externalInsulin ? (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Enact bolus") :
+                        (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log external insulin")
+                ).font(.headline)
+            } else {
+                Text("Continue without bolus").font(.headline)
+            }
         }
 
-        // MARK: DEFINITIONS FOR ADDING EXTERNAL INSULIN
-
-        private var limitManualBolus: Bool {
-            state.amount <= 0 || state.amount > state.maxBolus * 3
+        private var pumpBolusLimit: Bool {
+            state.amount > state.maxBolus
         }
 
-        private var logExternalInsulinBackground: Color {
-            if state.amount > state.maxBolus {
-                return Color.red
-            } else if state.amount <= 0 || state.amount > state.maxBolus * 3 {
-                return Color(.systemGray4)
-            } else {
-                return Color(.systemBlue)
-            }
+        private var externalBolusLimit: Bool {
+            state.amount > state.maxBolus * 3
         }
 
-        private var logExternalInsulinForeground: Color {
-            if state.amount > state.maxBolus {
-                return Color.white
-            } else if state.amount <= 0 || state.amount > state.maxBolus * 3 {
-                return Color.secondary
-            } else {
-                return Color.white
-            }
+        private var disableTaskButton: Bool {
+            state.amount > 0 ? (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) : false
         }
     }
 

+ 19 - 5
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -19,8 +19,9 @@ extension DataTable {
         @Published var manualGlucose: Decimal = 0
         @Published var maxBolus: Decimal = 0
         @Published var waitForSuggestion: Bool = false
-        @Published var showExternalInsulin: Bool = false
-        @Published var addButtonPressed: Bool = false
+
+        @Published var insulinEntryDeleted: Bool = false
+        @Published var carbEntryDeleted: Bool = false
 
         var units: GlucoseUnits = .mmolL
         var historyLayout: HistoryLayout = .twoTabs
@@ -159,11 +160,27 @@ extension DataTable {
             }
         }
 
+        func invokeCarbDeletionTask(_ treatment: Treatment) {
+            carbEntryDeleted = true
+            waitForSuggestion = true
+            deleteCarbs(treatment)
+        }
+
         func deleteCarbs(_ treatment: Treatment) {
             provider.deleteCarbs(treatment)
             apsManager.determineBasalSync()
         }
 
+        @MainActor func invokeInsulinDeletionTask(_ treatment: Treatment) {
+            Task {
+                do {
+                    await deleteInsulin(treatment)
+                    insulinEntryDeleted = true
+                    waitForSuggestion = true
+                }
+            }
+        }
+
         func deleteInsulin(_ treatment: Treatment) async {
             do {
                 let authenticated = try await unlockmanager.unlock()
@@ -263,9 +280,6 @@ extension DataTable.StateModel: SuggestionObserver {
     func suggestionDidUpdate(_: Suggestion) {
         DispatchQueue.main.async {
             self.waitForSuggestion = false
-            if self.addButtonPressed {
-                self.showExternalInsulin = false
-            }
         }
     }
 }

+ 49 - 32
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -74,34 +74,45 @@ extension DataTable {
         }
 
         var body: some View {
-            VStack {
-                Picker("Mode", selection: $state.mode) {
-                    ForEach(
-                        Mode.allCases.filter({ state.historyLayout == .twoTabs ? $0 != .meals : true }).indexed(),
-                        id: \.1
-                    ) { index, item in
-                        if state.historyLayout == .threeTabs && item == .treatments {
-                            Text("Insulin")
-                                .tag(index)
-                        } else {
-                            Text(item.name)
-                                .tag(index)
+            ZStack(alignment: .center, content: {
+                VStack {
+                    Picker("Mode", selection: $state.mode) {
+                        ForEach(
+                            Mode.allCases.filter({ state.historyLayout == .twoTabs ? $0 != .meals : true }).indexed(),
+                            id: \.1
+                        ) { index, item in
+                            if state.historyLayout == .threeTabs && item == .treatments {
+                                Text("Insulin")
+                                    .tag(index)
+                            } else {
+                                Text(item.name)
+                                    .tag(index)
+                            }
                         }
                     }
+                    .pickerStyle(SegmentedPickerStyle())
+                    .padding(.horizontal)
+
+                    Form {
+                        switch state.mode {
+                        case .treatments: treatmentsList
+                        case .glucose: glucoseList
+                        case .meals: state.historyLayout == .threeTabs ? AnyView(mealsList) : AnyView(EmptyView())
+                        }
+                    }.scrollContentBackground(.hidden)
+                        .background(color)
+                }.blur(radius: state.waitForSuggestion ? 8 : 0)
+
+                if state.waitForSuggestion {
+                    CustomProgressView(text: progressText.rawValue)
                 }
-                .pickerStyle(SegmentedPickerStyle())
-                .padding(.horizontal)
-
-                Form {
-                    switch state.mode {
-                    case .treatments: treatmentsList
-                    case .glucose: glucoseList
-                    case .meals: state.historyLayout == .threeTabs ? AnyView(mealsList) : AnyView(EmptyView())
-                    }
-                }.scrollContentBackground(.hidden)
-                    .background(color)
-            }.background(color)
+            })
+                .background(color)
                 .onAppear(perform: configureView)
+                .onDisappear {
+                    state.carbEntryDeleted = false
+                    state.insulinEntryDeleted = false
+                }
                 .navigationTitle("History")
                 .navigationBarTitleDisplayMode(.large)
                 .toolbar {
@@ -134,6 +145,17 @@ extension DataTable {
             )
         }
 
+        private var progressText: ProgressText {
+            switch (state.carbEntryDeleted, state.insulinEntryDeleted) {
+            case (true, false):
+                return .updatingCOB
+            case(false, true):
+                return .updatingIOB
+            default:
+                return .updatingHistory
+            }
+        }
+
         private var treatmentsList: some View {
             List {
                 HStack {
@@ -315,13 +337,9 @@ extension DataTable {
                     }
 
                     if state.historyLayout == .twoTabs, treatmentToDelete.type == .carbs || treatmentToDelete.type == .fpus {
-                        state.deleteCarbs(treatmentToDelete)
+                        state.invokeCarbDeletionTask(treatmentToDelete)
                     } else {
-                        Task {
-                            do {
-                                await state.deleteInsulin(treatmentToDelete)
-                            }
-                        }
+                        state.invokeInsulinDeletionTask(treatmentToDelete)
                     }
                 }
             } message: {
@@ -377,8 +395,7 @@ extension DataTable {
                         debug(.default, "Cannot gracefully unwrap alertTreatmentToDelete!")
                         return
                     }
-
-                    state.deleteCarbs(treatmentToDelete)
+                    state.invokeCarbDeletionTask(treatmentToDelete)
                 }
             } message: {
                 Text("\n" + NSLocalizedString(alertMessage, comment: ""))

+ 44 - 15
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -74,6 +74,9 @@ extension Home {
 
         @Published var waitForSuggestion: Bool = false
 
+        @Published var carbsForChart: [CarbsEntry] = []
+        @Published var fpusForChart: [CarbsEntry] = []
+
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
         override func subscribe() {
@@ -89,6 +92,8 @@ extension Home {
             setupReservoir()
             setupAnnouncements()
             setupCurrentPumpTimezone()
+            filterCarbs()
+            filterFpus()
 
             suggestion = provider.suggestion
             uploadStats = settingsManager.settings.uploadStats
@@ -211,6 +216,28 @@ extension Home {
                 .store(in: &lifetime)
         }
 
+        func filterCarbs() {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                let allCarbs = self.provider.carbs(hours: self.filteredHours)
+                let filteredCarbs = allCarbs.filter { !($0.isFPU ?? false) }
+
+                self.carbsForChart.removeAll()
+                self.carbsForChart.append(contentsOf: filteredCarbs)
+            }
+        }
+
+        func filterFpus() {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                let allCarbs = self.provider.carbs(hours: self.filteredHours)
+                let filteredFpus = allCarbs.filter { $0.isFPU ?? false }
+
+                self.fpusForChart.removeAll()
+                self.fpusForChart.append(contentsOf: filteredFpus)
+            }
+        }
+
         func runLoop() {
             provider.heartbeatNow()
         }
@@ -232,25 +259,25 @@ extension Home {
         }
 
         private func setupGlucose() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                let filteredGlucose = self.provider.filteredGlucose(hours: self.filteredHours)
+                  DispatchQueue.main.async { [weak self] in
+                      guard let self = self else { return }
+                      let filteredGlucose = self.provider.filteredGlucose(hours: self.filteredHours)
 
-                self.glucose = filteredGlucose
-                self.manualGlucose = filteredGlucose.filter { $0.type == GlucoseType.manual.rawValue }
+                      self.glucose = filteredGlucose
+                      self.manualGlucose = filteredGlucose.filter { $0.type == GlucoseType.manual.rawValue }
 
-                self.recentGlucose = self.glucose.last
+                      self.recentGlucose = self.glucose.last
 
-                if self.glucose.count >= 2 {
-                    self.glucoseDelta = (self.recentGlucose?.glucose ?? 0) - (self.glucose[self.glucose.count - 2].glucose ?? 0)
-                } else {
-                    self.glucoseDelta = nil
-                }
-
-                self.alarm = self.provider.glucoseStorage.alarm
-            }
-        }
+                      if self.glucose.count >= 2 {
+                          self.glucoseDelta = (self.recentGlucose?.glucose ?? 0) - (self.glucose[self.glucose.count - 2].glucose ?? 0)
+                      } else {
+                          self.glucoseDelta = nil
+                      }
 
+                      self.alarm = self.provider.glucoseStorage.alarm
+                  }
+              }
+        
         private func setupBasals() {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
@@ -491,6 +518,8 @@ extension Home.StateModel:
 
     func carbsDidUpdate(_: [CarbsEntry]) {
         setupCarbs()
+        filterFpus()
+        filterCarbs()
     }
 
     func enactedSuggestionDidUpdate(_ suggestion: Suggestion) {

+ 69 - 168
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -23,18 +23,6 @@ private struct Prediction: Hashable {
     let type: PredictionType
 }
 
-private struct Carb: Hashable {
-    let amount: Decimal
-    let timestamp: Date
-}
-
-private struct ChartBolus: Hashable {
-    let amount: Decimal
-    let timestamp: Date
-    let nearestGlucose: BloodGlucose
-    let yPosition: Decimal
-}
-
 private struct ChartTempTarget: Hashable {
     let amount: Decimal
     let start: Date
@@ -61,6 +49,8 @@ struct MainChartView: View {
 
     @Binding var glucose: [BloodGlucose]
     @Binding var manualGlucose: [BloodGlucose]
+    @Binding var carbsForChart: [CarbsEntry]
+    @Binding var fpusForChart: [CarbsEntry]
     @Binding var units: GlucoseUnits
     @Binding var eventualBG: Int?
     @Binding var suggestion: Suggestion?
@@ -73,7 +63,7 @@ struct MainChartView: View {
     @Binding var autotunedBasalProfile: [BasalProfileEntry]
     @Binding var basalProfile: [BasalProfileEntry]
     @Binding var tempTargets: [TempTarget]
-    @Binding var carbs: [CarbsEntry]
+//    @Binding var carbs: [CarbsEntry]
     @Binding var smooth: Bool
     @Binding var highGlucose: Decimal
     @Binding var lowGlucose: Decimal
@@ -90,9 +80,6 @@ struct MainChartView: View {
     @State private var TempBasals: [PumpHistoryEvent] = []
     @State private var ChartTempTargets: [ChartTempTarget] = []
     @State private var Predictions: [Prediction] = []
-    @State private var ChartCarbs: [Carb] = []
-    @State private var ChartFpus: [Carb] = []
-    @State private var ChartBoluses: [ChartBolus] = []
     @State private var count: Decimal = 1
     @State private var startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
     @State private var endMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 + 10800))
@@ -189,7 +176,7 @@ extension MainChartView {
                         Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
                         unit: .second
                     )
-                ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color.insulin)
+                ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
                 RuleMark(
                     x: .value(
                         "",
@@ -205,47 +192,51 @@ extension MainChartView {
                     )
                 ).foregroundStyle(Color.clear)
                 /// carbs
-                ForEach(ChartCarbs, id: \.self) { carb in
-                    let carbAmount = carb.amount
+                ForEach(carbsForChart) { carb in
+                    let carbAmount = carb.carbs
                     let yPosition = units == .mgdL ? 60 : 3.33
 
                     PointMark(
-                        x: .value("Time", carb.timestamp, unit: .second),
+                        x: .value("Time", carb.actualDate ?? Date(), unit: .second),
                         y: .value("Value", yPosition)
                     )
                     .symbolSize((Config.carbsSize + CGFloat(carbAmount) * Config.carbsScale) * 10)
                     .foregroundStyle(Color.orange)
                     .annotation(position: .bottom) {
-                        Text(carbsFormatter.string(from: carbAmount as NSNumber)!).font(.caption2).foregroundStyle(Color.orange)
+                        Text(carbsFormatter.string(from: carbAmount as NSNumber)!).font(.caption2)
+                            .foregroundStyle(Color.orange)
                     }
                 }
                 /// fpus
-                ForEach(ChartFpus, id: \.self) { fpu in
-                    let fpuAmount = fpu.amount
+                ForEach(fpusForChart) { fpu in
+                    let fpuAmount = fpu.carbs
                     let size = (Config.fpuSize + CGFloat(fpuAmount) * Config.carbsScale) * 1.8
                     let yPosition = units == .mgdL ? 60 : 3.33
 
                     PointMark(
-                        x: .value("Time", fpu.timestamp, unit: .second),
+                        x: .value("Time", fpu.actualDate ?? Date(), unit: .second),
                         y: .value("Value", yPosition)
                     )
                     .symbolSize(size)
                     .foregroundStyle(Color.brown)
                 }
                 /// smbs in triangle form
-                ForEach(ChartBoluses, id: \.self) { bolus in
-                    let bolusAmount = bolus.amount
+                ForEach(boluses) { bolus in
+                    let bolusAmount = bolus.amount ?? 0
+                    let glucose = timeToNearestGlucose(time: bolus.timestamp.timeIntervalSince1970)
+                    let yPosition = (Decimal(glucose.sgv ?? defaultBolusPosition) * conversionFactor) + bolusOffset
                     let size = (Config.bolusSize + CGFloat(bolusAmount) * Config.bolusScale) * 1.8
 
                     PointMark(
                         x: .value("Time", bolus.timestamp, unit: .second),
-                        y: .value("Value", bolus.yPosition)
+                        y: .value("Value", yPosition)
                     )
                     .symbol {
                         Image(systemName: "arrowtriangle.down.fill").font(.system(size: size)).foregroundStyle(Color.insulin)
                     }
                     .annotation(position: .top) {
-                        Text(bolusFormatter.string(from: bolusAmount as NSNumber)!).font(.caption2).foregroundStyle(Color.insulin)
+                        Text(bolusFormatter.string(from: bolusAmount as NSNumber)!).font(.caption2)
+                            .foregroundStyle(Color.insulin)
                     }
                 }
                 /// temp targets
@@ -351,14 +342,8 @@ extension MainChartView {
             }.id("MainChart")
                 .onChange(of: glucose) { _ in
                     calculatePredictions()
-                    calculateFpus()
-                }
-                .onChange(of: carbs) { _ in
-                    calculateCarbs()
-                    calculateFpus()
                 }
                 .onChange(of: boluses) { _ in
-                    calculateBoluses()
                     state.roundedTotalBolus = state.calculateTINS()
                 }
                 .onChange(of: tempTargets) { _ in
@@ -388,6 +373,7 @@ extension MainChartView {
                             AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
                         }
                         AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                            .font(.footnote)
                     }
                 }
                 .chartYAxis {
@@ -405,7 +391,7 @@ extension MainChartView {
                             if units == .mmolL {
                                 AxisTick(length: 7, stroke: .init(lineWidth: 7)).foregroundStyle(Color.clear)
                             }
-                            AxisValueLabel()
+                            AxisValueLabel().font(.footnote)
                         }
                     }
                 }
@@ -421,7 +407,7 @@ extension MainChartView {
                         Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
                         unit: .second
                     )
-                ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color.insulin)
+                ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
                 RuleMark(
                     x: .value(
                         "",
@@ -552,51 +538,27 @@ extension MainChartView {
             }
             .chartYAxis {
                 AxisMarks(position: .trailing) { _ in
-                    AxisTick(length: 25, stroke: .init(lineWidth: 4))
-                        .foregroundStyle(Color.clear)
+                    AxisTick(length: units == .mmolL ? 25 : 27, stroke: .init(lineWidth: 4))
+                        .foregroundStyle(Color.clear).font(.footnote)
                 }
             }
         }
     }
 
     var legendPanel: some View {
-        ZStack {
-            HStack(alignment: .center) {
-                Spacer()
-
-                Group {
-                    Circle().fill(Color.loopGreen).frame(width: 8, height: 8)
-                    Text("BG")
-                        .font(.system(size: 10, weight: .bold)).foregroundColor(.loopGreen)
-                }
-                Group {
-                    Circle().fill(Color.insulin).frame(width: 8, height: 8)
-                        .padding(.leading, 8)
-                    Text("IOB")
-                        .font(.system(size: 10, weight: .bold)).foregroundColor(.insulin)
-                }
-                Group {
-                    Circle().fill(Color.zt).frame(width: 8, height: 8)
-                        .padding(.leading, 8)
-                    Text("ZT")
-                        .font(.system(size: 10, weight: .bold)).foregroundColor(.zt)
-                }
-                Group {
-                    Circle().fill(Color.loopYellow).frame(width: 8, height: 8).padding(.leading, 8)
-                    Text("COB")
-                        .font(.system(size: 10, weight: .bold)).foregroundColor(.loopYellow)
-                }
-                Group {
-                    Circle().fill(Color.uam).frame(width: 8, height: 8)
-                        .padding(.leading, 8)
-                    Text("UAM")
-                        .font(.system(size: 10, weight: .bold)).foregroundColor(.uam)
-                }
-                Spacer()
-            }
-            .padding(.horizontal, 10)
-            .frame(maxWidth: .infinity)
+        HStack(spacing: 10) {
+            Spacer()
+
+            LegendItem(color: .loopGreen, label: "BG")
+            LegendItem(color: .insulin, label: "IOB")
+            LegendItem(color: .zt, label: "ZT")
+            LegendItem(color: .loopYellow, label: "COB")
+            LegendItem(color: .uam, label: "UAM")
+
+            Spacer()
         }
+        .padding(.horizontal, 10)
+        .frame(maxWidth: .infinity)
     }
 }
 
@@ -636,48 +598,6 @@ extension MainChartView {
         viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
     }
 
-    private func calculateCarbs() {
-        var calculatedCarbs: [Carb] = []
-
-        /// check if carbs are not fpus before adding them to the chart
-        /// this solves the problem of a first CARB entry with the amount of the single fpu entries that was made at current time when adding ONLY fpus
-        let realCarbs = carbs.filter { !($0.isFPU ?? false) }
-
-        realCarbs.forEach { carb in
-            calculatedCarbs.append(Carb(amount: carb.carbs, timestamp: carb.actualDate ?? carb.createdAt))
-        }
-        ChartCarbs = calculatedCarbs
-    }
-
-    private func calculateFpus() {
-        var calculatedFpus: [Carb] = []
-
-        /// check for only fpus
-        let fpus = carbs.filter { $0.isFPU ?? false }
-
-        fpus.forEach { fpu in
-            calculatedFpus
-                .append(Carb(amount: fpu.carbs, timestamp: fpu.actualDate ?? Date()))
-        }
-        ChartFpus = calculatedFpus
-    }
-
-    private func calculateBoluses() {
-        var calculatedBoluses: [ChartBolus] = []
-        boluses.forEach { bolus in
-            let bg = timeToNearestGlucose(time: bolus.timestamp.timeIntervalSince1970)
-            let yPosition = (Decimal(bg.sgv ?? defaultBolusPosition) * conversionFactor) + bolusOffset
-            calculatedBoluses
-                .append(ChartBolus(
-                    amount: bolus.amount ?? 0,
-                    timestamp: bolus.timestamp,
-                    nearestGlucose: bg,
-                    yPosition: yPosition
-                ))
-        }
-        ChartBoluses = calculatedBoluses
-    }
-
     /// calculations for temp target bar mark
     private func calculateTTs() {
         var groupedPackages: [[TempTarget]] = []
@@ -737,69 +657,38 @@ extension MainChartView {
         ChartTempTargets = calculatedTTs
     }
 
-    private func calculatePredictions() {
+    private func addPredictions(_ predictions: [Int], type: PredictionType, deliveredAt: Date, endMarker: Date) -> [Prediction] {
         var calculatedPredictions: [Prediction] = []
-        let uam = suggestion?.predictions?.uam ?? []
-        let iob = suggestion?.predictions?.iob ?? []
-        let cob = suggestion?.predictions?.cob ?? []
-        let zt = suggestion?.predictions?.zt ?? []
-        guard let deliveredAt = suggestion?.deliverAt else {
-            return
-        }
-        uam.indices.forEach { index in
-            let predTime = Date(
-                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
-                    .timeInterval
-            )
-            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
-                calculatedPredictions.append(
-                    Prediction(amount: uam[index], timestamp: predTime, type: .uam)
-                )
-            }
-        }
-        iob.indices.forEach { index in
-            let predTime = Date(
-                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
-                    .timeInterval
-            )
-            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
-                calculatedPredictions.append(
-                    Prediction(amount: iob[index], timestamp: predTime, type: .iob)
-                )
-            }
-        }
-        cob.indices.forEach { index in
+        predictions.indices.forEach { index in
             let predTime = Date(
-                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
-                    .timeInterval
+                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes.timeInterval
             )
             if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
                 calculatedPredictions.append(
-                    Prediction(amount: cob[index], timestamp: predTime, type: .cob)
+                    Prediction(amount: predictions[index], timestamp: predTime, type: type)
                 )
             }
         }
-        zt.indices.forEach { index in
-            let predTime = Date(
-                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
-                    .timeInterval
-            )
-            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
-                calculatedPredictions.append(
-                    Prediction(amount: zt[index], timestamp: predTime, type: .zt)
-                )
-            }
-        }
-        Predictions = calculatedPredictions
+        return calculatedPredictions
     }
 
-    private func getLastUam() -> Int {
-        let uam = suggestion?.predictions?.uam ?? []
-        return uam.last ?? 0
+    private func calculatePredictions() {
+        guard let suggestion = suggestion, let deliveredAt = suggestion.deliverAt else { return }
+        let uamPredictions = suggestion.predictions?.uam ?? []
+        let iobPredictions = suggestion.predictions?.iob ?? []
+        let cobPredictions = suggestion.predictions?.cob ?? []
+        let ztPredictions = suggestion.predictions?.zt ?? []
+
+        let uam = addPredictions(uamPredictions, type: .uam, deliveredAt: deliveredAt, endMarker: endMarker)
+        let iob = addPredictions(iobPredictions, type: .iob, deliveredAt: deliveredAt, endMarker: endMarker)
+        let cob = addPredictions(cobPredictions, type: .cob, deliveredAt: deliveredAt, endMarker: endMarker)
+        let zt = addPredictions(ztPredictions, type: .zt, deliveredAt: deliveredAt, endMarker: endMarker)
+
+        Predictions = uam + iob + cob + zt
     }
 
     private func calculateTempBasals() {
-        var basals = tempBasals
+        let basals = tempBasals
         var returnTempBasalRates: [PumpHistoryEvent] = []
         var finished: [Int: Bool] = [:]
         basals.indices.forEach { i in
@@ -890,8 +779,6 @@ extension MainChartView {
 
     private func calculateBasals() {
         let dayAgoTime = Date().addingTimeInterval(-1.days.timeInterval).timeIntervalSince1970
-        let firstTempTime = (tempBasals.first?.timestamp ?? Date()).timeIntervalSince1970
-
         let regularPoints = findRegularBasalPoints(
             timeBegin: dayAgoTime,
             timeEnd: endMarker.timeIntervalSince1970,
@@ -926,3 +813,17 @@ extension MainChartView {
         BasalProfiles = basals
     }
 }
+
+struct LegendItem: View {
+    var color: Color
+    var label: String
+
+    var body: some View {
+        Group {
+            Circle().fill(color).frame(width: 8, height: 8)
+            Text(label)
+                .font(.system(size: 10, weight: .bold))
+                .foregroundColor(color)
+        }
+    }
+}

+ 8 - 8
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -359,6 +359,8 @@ extension Home {
                 MainChartView(
                     glucose: $state.glucose,
                     manualGlucose: $state.manualGlucose,
+                    carbsForChart: $state.carbsForChart,
+                    fpusForChart: $state.fpusForChart,
                     units: $state.units,
                     eventualBG: $state.eventualBG,
                     suggestion: $state.suggestion,
@@ -371,7 +373,6 @@ extension Home {
                     autotunedBasalProfile: $state.autotunedBasalProfile,
                     basalProfile: $state.basalProfile,
                     tempTargets: $state.tempTargets,
-                    carbs: $state.carbs,
                     smooth: $state.smooth,
                     highGlucose: $state.highGlucose,
                     lowGlucose: $state.lowGlucose,
@@ -744,12 +745,12 @@ extension Home {
                         .frame(maxHeight: UIScreen.main.bounds.height * 0.45)
                         .frame(minHeight: UIScreen.main.bounds.height * 0.4)
 
-                    timeInterval.padding(.top, 20).padding(.bottom, 20)
+                    timeInterval.padding(.top, 20).padding(.bottom, 40)
 
                     if let progress = state.bolusProgress {
-                        bolusView(geo, progress).padding(.bottom, 30)
+                        bolusView(geo, progress).padding(.bottom, 10)
                     } else {
-                        profileView(geo).padding(.bottom, 30)
+                        profileView(geo).padding(.bottom, 10)
                     }
                 }
                 .background(color)
@@ -793,14 +794,13 @@ extension Home {
                 TabView {
                     let carbsRequiredBadge: String? = {
                         guard let carbsRequired = state.carbsRequired else { return nil }
-                        return carbsRequired > 0 ? numberFormatter.string(from: carbsRequired as NSNumber) : nil
+                        return carbsRequired > 0 ? "\(numberFormatter.string(from: carbsRequired as NSNumber) ?? "") " +
+                            NSLocalizedString("g", comment: "Short representation of grams") : nil
                     }()
 
-                    mainView()
+                    NavigationStack { mainView() }
                         .tabItem { Label("Home", systemImage: "house") }
                         .badge(carbsRequiredBadge)
-                        .toolbarBackground(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white, for: .tabBar)
-                        .toolbarBackground(.visible, for: .tabBar)
 
                     NavigationStack { DataTable.RootView(resolver: resolver) }
                         .tabItem { Label("History", systemImage: historySFSymbol) }

+ 8 - 1
FreeAPS/Sources/Views/DecimalTextField.swift

@@ -8,6 +8,7 @@ struct DecimalTextField: UIViewRepresentable {
     private var autofocus: Bool
     private var cleanInput: Bool
     private var useButtons: Bool
+    private var textColor: UIColor?
 
     init(
         _ placeholder: String,
@@ -15,7 +16,8 @@ struct DecimalTextField: UIViewRepresentable {
         formatter: NumberFormatter,
         autofocus: Bool = false,
         cleanInput: Bool = false,
-        useButtons: Bool = true
+        useButtons: Bool = true,
+        textColor: UIColor? = nil
     ) {
         self.placeholder = placeholder
         _value = value
@@ -23,6 +25,7 @@ struct DecimalTextField: UIViewRepresentable {
         self.autofocus = autofocus
         self.cleanInput = cleanInput
         self.useButtons = useButtons
+        self.textColor = textColor
     }
 
     func makeUIView(context: Context) -> UITextField {
@@ -33,6 +36,10 @@ struct DecimalTextField: UIViewRepresentable {
         textfield.text = cleanInput ? "" : formatter.string(for: value) ?? placeholder
         textfield.textAlignment = .right
 
+        if let textColor = textColor {
+            textfield.textColor = textColor
+        }
+
         lazy var toolBar: UIToolbar = {
             let tool: UIToolbar = .init(frame: .init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 35))
             tool.barStyle = .default