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

Merge branch 'dev' into recommended-bolus-to-nightscout

Jonas Björkert 6 месяцев назад
Родитель
Сommit
c7b8970f1f

+ 1 - 1
Config.xcconfig

@@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.6.0
-APP_DEV_VERSION = 0.6.0.9
+APP_DEV_VERSION = 0.6.0.10
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 25 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -1711,6 +1711,7 @@
       }
     },
     " %@/U" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -3754,6 +3755,7 @@
     },
     " U/day" : {
       "comment" : "Total AT / Scheduled basal insulin",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -8952,6 +8954,7 @@
     },
     "%@ U/hr" : {
       "comment" : "Number of units per hour",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -10034,6 +10037,7 @@
       }
     },
     "%lld h" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -26283,6 +26287,7 @@
       }
     },
     "Add an entry by tapping 'Add Rate +' in the top right-hand corner of the screen." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -26401,6 +26406,7 @@
       }
     },
     "Add an entry by tapping 'Add Ratio +' in the top right-hand corner of the screen." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -26519,6 +26525,7 @@
       }
     },
     "Add an entry by tapping 'Add Sensitivity +' in the top right-hand corner of the screen." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -26637,6 +26644,7 @@
       }
     },
     "Add an entry by tapping 'Add Target +' in the top right-hand corner of the screen." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -30122,6 +30130,7 @@
       }
     },
     "Add Rate" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -30240,6 +30249,7 @@
       }
     },
     "Add Ratio" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -30358,6 +30368,7 @@
       }
     },
     "Add Sensitivity" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -30602,6 +30613,7 @@
       }
     },
     "Add Target" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -60839,6 +60851,7 @@
       }
     },
     "Carb Ratios cover 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -135297,6 +135310,7 @@
       }
     },
     "Insulin Sensitivities cover 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -198762,6 +198776,7 @@
       }
     },
     "Schedule" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -204922,6 +204937,7 @@
       }
     },
     "Set Rate" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -205040,6 +205056,7 @@
       }
     },
     "Set Ratio" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -205158,6 +205175,7 @@
       }
     },
     "Set Target" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -220946,6 +220964,7 @@
       }
     },
     "Swipe left to delete a single entry. Tap on it, to edit its time or rate." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -221418,6 +221437,7 @@
       }
     },
     "Swipe to delete a single entry. Tap on it, to edit its time or rate." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -221535,6 +221555,9 @@
         }
       }
     },
+    "Swipe to delete a single entry. Tap on it, to edit its time or value." : {
+
+    },
     "System Default" : {
       "localizations" : {
         "bg" : {
@@ -223077,6 +223100,7 @@
       }
     },
     "Target " : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -223556,6 +223580,7 @@
       }
     },
     "Target Glucose covered for 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {

+ 20 - 0
Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -9,6 +9,7 @@ extension BasalProfileEditor {
         var syncInProgress: Bool = false
         var initialItems: [Item] = []
         var items: [Item] = []
+        var therapyItems: [TherapySettingItem] = []
         var total: Decimal = 0.0
         var showAlert: Bool = false
         var chartData: [BasalProfile]? = []
@@ -26,6 +27,25 @@ extension BasalProfileEditor {
             initialItems != items
         }
 
+        // Convert items to TherapySettingItem format
+        func getTherapyItems() -> [TherapySettingItem] {
+            items.map { item in
+                TherapySettingItem(
+                    time: timeValues[item.timeIndex],
+                    value: rateValues[item.rateIndex]
+                )
+            }
+        }
+
+        // Update items from TherapySettingItem format
+        func updateFromTherapyItems(_ therapyItems: [TherapySettingItem]) {
+            items = therapyItems.map { therapyItem in
+                let timeIndex = timeValues.firstIndex(where: { abs($0 - therapyItem.time) < 1 }) ?? 0
+                let rateIndex = rateValues.firstIndex(of: therapyItem.value) ?? 0
+                return Item(rateIndex: rateIndex, timeIndex: timeIndex)
+            }
+        }
+
         override func subscribe() {
             rateValues = provider.supportedBasalRates ?? stride(from: 5.0, to: 1001.0, by: 5.0)
                 .map { ($0.decimal ?? .zero) / 100 }

+ 143 - 154
Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -6,41 +6,43 @@ extension BasalProfileEditor {
     struct RootView: BaseView {
         let resolver: Resolver
         @State var state = StateModel()
-        @State private var editMode = EditMode.inactive
-
-        let chartScale = Calendar.current
-            .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
-        let tzOffset = TimeZone.current.secondsFromGMT()
+        @State private var refreshUI = UUID()
+        @State private var now = Date()
+        @Namespace private var bottomID
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
-        private var dateFormatter: DateFormatter {
-            let formatter = DateFormatter()
-            formatter.timeZone = TimeZone(secondsFromGMT: 0)
-            formatter.timeStyle = .short
-            return formatter
-        }
-
         private var rateFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
-
             return formatter
         }
 
-        var now = Date()
-        var basalScheduleChart: some View {
+        // Chart for visualizing basal profile
+        private var basalProfileChart: some View {
             Chart {
-                ForEach(state.chartData!, id: \.self) { profile in
-                    let startDate = Calendar.current.startOfDay(for: now)
-                        .addingTimeInterval(profile.startDate.timeIntervalSinceReferenceDate + Double(tzOffset))
-                    let endDate = Calendar.current.startOfDay(for: now)
-                        .addingTimeInterval(profile.endDate!.timeIntervalSinceReferenceDate + Double(tzOffset))
+                ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
+                    let displayValue = state.rateValues[item.rateIndex]
+
+                    let startDate = Calendar.current
+                        .startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues[item.timeIndex])
+
+                    var offset: TimeInterval {
+                        if state.items.count > index + 1 {
+                            return state.timeValues[state.items[index + 1].timeIndex]
+                        } else {
+                            return state.timeValues.last! + 30 * 60
+                        }
+                    }
+
+                    let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
+
                     RectangleMark(
                         xStart: .value("start", startDate),
                         xEnd: .value("end", endDate),
-                        yStart: .value("rate-start", profile.amount),
+                        yStart: .value("rate-start", displayValue),
                         yEnd: .value("rate-end", 0)
                     ).foregroundStyle(
                         .linearGradient(
@@ -53,30 +55,30 @@ extension BasalProfileEditor {
                         )
                     ).alignsMarkStylesWithPlotArea()
 
-                    LineMark(x: .value("End Date", endDate), y: .value("Amount", profile.amount))
+                    LineMark(x: .value("End Date", startDate), y: .value("Rate", displayValue))
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.purple)
 
-                    LineMark(x: .value("Start Date", startDate), y: .value("Amount", profile.amount))
+                    LineMark(x: .value("Start Date", endDate), y: .value("Rate", displayValue))
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.purple)
                 }
             }
+            .id(refreshUI) // Force chart update
             .chartXAxis {
                 AxisMarks(values: .automatic(desiredCount: 6)) { _ in
                     AxisValueLabel(format: .dateTime.hour())
                     AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
                 }
             }
+            .chartXScale(
+                domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
+                    .addingTimeInterval(60 * 60 * 24)
+            )
             .chartYAxis {
-                AxisMarks(values: .automatic(desiredCount: 2)) { _ in
+                AxisMarks(values: .automatic(desiredCount: 4)) { _ in
                     AxisValueLabel()
                     AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
                 }
             }
-            .chartXScale(
-                domain: Calendar.current.startOfDay(for: now) ... Calendar
-                    .current.startOfDay(for: now)
-                    .addingTimeInterval(60 * 60 * 24)
-            )
         }
 
         var saveButton: some View {
@@ -116,147 +118,134 @@ extension BasalProfileEditor {
             }
         }
 
-        var body: some View {
-            Form {
-                if !state.canAdd {
-                    Section {
-                        VStack(alignment: .leading) {
-                            Text(
-                                "Basal profile covers 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space."
-                            ).bold()
-                        }
-                    }.listRowBackground(Color.tabBar)
-                }
+        var fullScheduleWarning: some View {
+            VStack {
+                Text(
+                    "Basal profile covers 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space."
+                ).bold()
+            }
+            .frame(maxWidth: .infinity, alignment: .leading)
+            .padding()
+            .background(Color.tabBar)
+            .clipShape(
+                .rect(
+                    topLeadingRadius: 10,
+                    bottomLeadingRadius: 10,
+                    bottomTrailingRadius: 10,
+                    topTrailingRadius: 10
+                )
+            )
+        }
 
-                Section(header: Text("Schedule")) {
-                    if !state.items.isEmpty {
-                        basalScheduleChart.padding(.vertical)
-                    }
+        var totalBasalRow: some View {
+            VStack(alignment: .leading, spacing: 0) {
+                HStack {
+                    Text("Total")
+                        .bold()
 
-                    list
-                }.listRowBackground(Color.chart)
+                    Spacer()
 
-                Section {
                     HStack {
-                        Text("Total")
-                            .bold()
-                            .foregroundColor(.primary)
-                        Spacer()
                         Text(rateFormatter.string(from: state.total as NSNumber) ?? "0")
-                            .foregroundColor(.primary) +
-                            Text(" U/day")
-                            .foregroundColor(.secondary)
-                    }
-                }.listRowBackground(Color.chart)
-
-                Section {} header: {
-                    VStack(alignment: .leading, spacing: 10) {
-                        HStack {
-                            Image(systemName: "note.text.badge.plus").foregroundStyle(.primary)
-                            Text("Add an entry by tapping 'Add Rate +' in the top right-hand corner of the screen.")
-                        }
-                        HStack {
-                            Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
-                            Text("Swipe to delete a single entry. Tap on it, to edit its time or rate.")
-                        }
+                        Text("U/day")
+                            .foregroundStyle(Color.secondary)
                     }
-                    .textCase(nil)
+                    .id(refreshUI)
                 }
             }
-            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
-            .alert(isPresented: $state.showAlert) {
-                Alert(
-                    title: Text("Unable to Save"),
-                    message: Text("Trio could not communicate with your pump. Changes to your basal profile were not saved."),
-                    dismissButton: .default(Text("Close"))
-                )
-            }
-            .onChange(of: state.items) {
-                state.calcTotal()
-                state.calculateChartData()
-            }
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .navigationTitle("Basal Rates")
-            .navigationBarTitleDisplayMode(.automatic)
-            .toolbar(content: {
-                ToolbarItem(placement: .topBarTrailing) {
-                    Button(action: { state.add() }) {
-                        HStack {
-                            Text("Add Rate")
-                            Image(systemName: "plus")
-                        }
-                    }.disabled(!state.canAdd)
-                }
-            })
-            .environment(\.editMode, $editMode)
-            .onAppear {
-                configureView()
-                state.validate()
-                state.calculateChartData()
-            }
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .cornerRadius(10)
+            .padding(.horizontal)
+            .id(bottomID)
         }
 
-        private func pickers(for index: Int) -> some View {
-            Form {
-                Section {
-                    Picker(selection: $state.items[index].rateIndex, label: Text("Rate")) {
-                        ForEach(0 ..< state.rateValues.count, id: \.self) { i in
-                            Text(
-                                (self.rateFormatter.string(from: state.rateValues[i] as NSNumber) ?? "") + " " +
-                                    String(localized: "U/hr")
-                            ).tag(i)
-                        }
-                    }
-                    .onChange(of: state.items[index].rateIndex, { state.calcTotal() })
-                }.listRowBackground(Color.chart)
+        var body: some View {
+            ScrollViewReader { proxy in
+                VStack(spacing: 0) {
+                    ScrollView {
+                        LazyVStack {
+                            VStack(alignment: .leading, spacing: 0) {
+                                if !state.canAdd {
+                                    fullScheduleWarning
+                                        .padding()
+                                }
 
-                Section {
-                    Picker(selection: $state.items[index].timeIndex, label: Text("Time")) {
-                        ForEach(state.availableTimeIndices(index), id: \.self) { i in
-                            Text(
-                                self.dateFormatter
-                                    .string(from: Date(
-                                        timeIntervalSince1970: state
-                                            .timeValues[i]
-                                    ))
-                            ).tag(i)
-                        }
-                    }
-                    .onChange(of: state.items[index].timeIndex, { state.calcTotal() })
-                }.listRowBackground(Color.chart)
-            }
-            .padding(.top)
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .navigationTitle("Set Rate")
-            .navigationBarTitleDisplayMode(.automatic)
-        }
+                                // Chart visualization
+                                if !state.items.isEmpty {
+                                    basalProfileChart
+                                        .frame(height: 180)
+                                        .padding()
+                                        .background(Color.chart.opacity(0.65))
+                                        .clipShape(
+                                            .rect(
+                                                topLeadingRadius: 10,
+                                                bottomLeadingRadius: 0,
+                                                bottomTrailingRadius: 0,
+                                                topTrailingRadius: 10
+                                            )
+                                        )
+                                        .padding(.horizontal)
+                                        .padding(.top)
+                                }
+
+                                // Basal profile list
+                                TherapySettingEditorView(
+                                    items: $state.therapyItems,
+                                    unit: .unitPerHour,
+                                    timeOptions: state.timeValues,
+                                    valueOptions: state.rateValues,
+                                    validateOnDelete: state.validate,
+                                    onItemAdded: {
+                                        withAnimation {
+                                            proxy.scrollTo(bottomID, anchor: .bottom)
+                                        }
+                                    }
+                                )
+                                .padding(.horizontal)
+
+                                if !state.items.isEmpty {
+                                    totalBasalRow
+                                }
 
-        private var list: some View {
-            List {
-                ForEach(state.items.indexed(), id: \.1.id) { index, item in
-                    NavigationLink(destination: pickers(for: index)) {
-                        HStack {
-                            Text("Rate").foregroundColor(.secondary)
-                            Text(
-                                "\(rateFormatter.string(from: state.rateValues[item.rateIndex] as NSNumber) ?? "0") U/hr"
-                            )
-                            Spacer()
-                            Text("starts at").foregroundColor(.secondary)
-                            Text(
-                                "\(dateFormatter.string(from: Date(timeIntervalSince1970: state.timeValues[item.timeIndex])))"
-                            )
+                                HStack {
+                                    Image(systemName: "hand.draw.fill")
+                                        .padding(.leading)
+
+                                    Text("Swipe to delete a single entry. Tap on it, to edit its time or value.")
+                                        .padding(.trailing)
+                                }
+                                .font(.subheadline)
+                                .fontWeight(.light)
+                                .foregroundStyle(.secondary)
+                                .padding()
+                            }
                         }
                     }
-                    .moveDisabled(true)
+
+                    saveButton
+                }
+                .background(appState.trioBackgroundColor(for: colorScheme))
+                .alert(isPresented: $state.showAlert) {
+                    Alert(
+                        title: Text("Unable to Save"),
+                        message: Text("Trio could not communicate with your pump. Changes to your basal profile were not saved."),
+                        dismissButton: .default(Text("Close"))
+                    )
+                }
+                .navigationTitle("Basal Rates")
+                .navigationBarTitleDisplayMode(.automatic)
+                .onAppear {
+                    configureView()
+                    state.validate()
+                    state.therapyItems = state.getTherapyItems()
+                }
+                .onChange(of: state.therapyItems) { _, newItems in
+                    state.updateFromTherapyItems(newItems)
+                    state.calcTotal()
+                    refreshUI = UUID()
                 }
-                .onDelete(perform: onDelete)
             }
         }
-
-        private func onDelete(offsets: IndexSet) {
-            state.items.remove(atOffsets: offsets)
-            state.validate()
-            state.calculateChartData()
-        }
     }
 }

+ 20 - 0
Trio/Sources/Modules/CarbRatioEditor/CarbRatioEditorStateModel.swift

@@ -5,6 +5,7 @@ extension CarbRatioEditor {
         @Injected() private var nightscout: NightscoutManager!
         @Published var items: [Item] = []
         @Published var initialItems: [Item] = []
+        @Published var therapyItems: [TherapySettingItem] = []
         @Published var shouldDisplaySaving: Bool = false
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
@@ -30,6 +31,25 @@ extension CarbRatioEditor {
             return false
         }
 
+        // Convert items to TherapySettingItem format
+        func getTherapyItems() -> [TherapySettingItem] {
+            items.map { item in
+                TherapySettingItem(
+                    time: timeValues[item.timeIndex],
+                    value: rateValues[item.rateIndex]
+                )
+            }
+        }
+
+        // Update items from TherapySettingItem format
+        func updateFromTherapyItems(_ therapyItems: [TherapySettingItem]) {
+            items = therapyItems.map { therapyItem in
+                let timeIndex = timeValues.firstIndex(where: { abs($0 - therapyItem.time) < 1 }) ?? 0
+                let rateIndex = rateValues.firstIndex(of: therapyItem.value) ?? 0
+                return Item(rateIndex: rateIndex, timeIndex: timeIndex)
+            }
+        }
+
         override func subscribe() {
             items = provider.profile.schedule.map { value in
                 let timeIndex = timeValues.firstIndex(of: Double(value.offset * 60)) ?? 0

+ 81 - 124
Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -6,21 +6,17 @@ extension CarbRatioEditor {
     struct RootView: BaseView {
         let resolver: Resolver
         @StateObject var state = StateModel()
-        @State private var editMode = EditMode.inactive
+        @State private var refreshUI = UUID()
+        @State private var now = Date()
+        @Namespace private var bottomID
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
-        private var dateFormatter: DateFormatter {
-            let formatter = DateFormatter()
-            formatter.timeZone = TimeZone(secondsFromGMT: 0)
-            formatter.timeStyle = .short
-            return formatter
-        }
-
-        private var rateFormatter: NumberFormatter {
+        private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 1
             return formatter
         }
 
@@ -69,130 +65,96 @@ extension CarbRatioEditor {
         }
 
         var body: some View {
-            Form {
-                if !state.canAdd {
-                    Section {
-                        VStack(alignment: .leading) {
-                            Text(
-                                "Carb Ratios cover 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space."
-                            ).bold()
-                        }
-                    }.listRowBackground(Color.tabBar)
-                }
+            ScrollViewReader { proxy in
+                VStack(spacing: 0) {
+                    ScrollView {
+                        LazyVStack {
+                            VStack(alignment: .leading, spacing: 0) {
+                                // Chart visualization
+                                if !state.items.isEmpty {
+                                    carbRatioChart
+                                        .frame(height: 180)
+                                        .padding()
+                                        .background(Color.chart.opacity(0.65))
+                                        .clipShape(
+                                            .rect(
+                                                topLeadingRadius: 10,
+                                                bottomLeadingRadius: 0,
+                                                bottomTrailingRadius: 0,
+                                                topTrailingRadius: 10
+                                            )
+                                        )
+                                        .padding(.horizontal)
+                                        .padding(.top)
+                                }
 
-                Section(header: Text("Schedule")) {
-                    list
-                }.listRowBackground(Color.chart)
+                                // Carb ratio list
+                                TherapySettingEditorView(
+                                    items: $state.therapyItems,
+                                    unit: .gramPerUnit,
+                                    timeOptions: state.timeValues,
+                                    valueOptions: state.rateValues,
+                                    validateOnDelete: state.validate,
+                                    onItemAdded: {
+                                        withAnimation {
+                                            proxy.scrollTo(bottomID, anchor: .bottom)
+                                        }
+                                    }
+                                )
+                                .padding(.horizontal)
 
-                Section {} header: {
-                    VStack(alignment: .leading, spacing: 10) {
-                        HStack {
-                            Image(systemName: "note.text.badge.plus").foregroundStyle(.primary)
-                            Text("Add an entry by tapping 'Add Ratio +' in the top right-hand corner of the screen.")
-                        }
-                        HStack {
-                            Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
-                            Text("Swipe to delete a single entry. Tap on it, to edit its time or rate.")
-                        }
-                    }
-                    .textCase(nil)
-                }
-            }
-            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .onAppear(perform: configureView)
-            .navigationTitle("Carb Ratios")
-            .navigationBarTitleDisplayMode(.automatic)
-            .toolbar(content: {
-                ToolbarItem(placement: .topBarTrailing) {
-                    Button(action: { state.add() }) {
-                        HStack {
-                            Text("Add Ratio")
-                            Image(systemName: "plus")
-                        }
-                    }.disabled(!state.canAdd)
-                }
-            })
-            .environment(\.editMode, $editMode)
-            .onAppear {
-                state.validate()
-            }
-        }
+                                HStack {
+                                    Image(systemName: "hand.draw.fill")
+                                        .padding(.leading)
 
-        private func pickers(for index: Int) -> some View {
-            Form {
-                Section {
-                    Picker(selection: $state.items[index].rateIndex, label: Text("Ratio")) {
-                        ForEach(0 ..< state.rateValues.count, id: \.self) { i in
-                            Text(
-                                (self.rateFormatter.string(from: state.rateValues[i] as NSNumber) ?? "") + " " +
-                                    String(localized: "g/U")
-                            ).tag(i)
-                        }
-                    }
-                }.listRowBackground(Color.chart)
-
-                Section {
-                    Picker(selection: $state.items[index].timeIndex, label: Text("Time")) {
-                        ForEach(0 ..< state.timeValues.count, id: \.self) { i in
-                            Text(
-                                self.dateFormatter
-                                    .string(from: Date(
-                                        timeIntervalSince1970: state
-                                            .timeValues[i]
-                                    ))
-                            ).tag(i)
+                                    Text("Swipe to delete a single entry. Tap on it, to edit its time or value.")
+                                        .padding(.trailing)
+                                }
+                                .font(.subheadline)
+                                .fontWeight(.light)
+                                .foregroundStyle(.secondary)
+                                .padding()
+                            }
                         }
                     }
 
-                }.listRowBackground(Color.chart)
-            }
-            .padding(.top)
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .navigationTitle("Set Ratio")
-            .navigationBarTitleDisplayMode(.automatic)
-        }
-
-        private var list: some View {
-            List {
-                chart.padding(.vertical)
-                ForEach(state.items.indexed(), id: \.1.id) { index, item in
-                    NavigationLink(destination: pickers(for: index)) {
-                        HStack {
-                            Text("Ratio").foregroundColor(.secondary)
-                            Text(
-                                (rateFormatter.string(from: state.rateValues[item.rateIndex] as NSNumber) ?? "0") + " " +
-                                    String(localized: "g/U")
-                            )
-                            Spacer()
-                            Text("starts at").foregroundColor(.secondary)
-                            Text(
-                                "\(dateFormatter.string(from: Date(timeIntervalSince1970: state.timeValues[item.timeIndex])))"
-                            )
-                        }
-                    }
-                    .moveDisabled(true)
+                    saveButton
+                }
+                .background(appState.trioBackgroundColor(for: colorScheme))
+                .onAppear(perform: configureView)
+                .navigationTitle("Carb Ratios")
+                .navigationBarTitleDisplayMode(.automatic)
+                .onAppear {
+                    state.validate()
+                    state.therapyItems = state.getTherapyItems()
+                }
+                .onChange(of: state.therapyItems) { _, newItems in
+                    state.updateFromTherapyItems(newItems)
+                    refreshUI = UUID()
                 }
-                .onDelete(perform: onDelete)
             }
         }
 
-        var now = Date()
-        var chart: some View {
+        // Chart for visualizing carb ratios
+        private var carbRatioChart: some View {
             Chart {
-                ForEach(state.items.indexed(), id: \.1.id) { index, item in
+                ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
                     let displayValue = state.rateValues[item.rateIndex]
 
                     let startDate = Calendar.current
                         .startOfDay(for: now)
                         .addingTimeInterval(state.timeValues[item.timeIndex])
-                    let endDate = state.items
-                        .count > index + 1 ?
-                        Calendar.current.startOfDay(for: now)
-                        .addingTimeInterval(state.timeValues[state.items[index + 1].timeIndex])
-                        :
-                        Calendar.current.startOfDay(for: now)
-                        .addingTimeInterval(state.timeValues.last! + 30 * 60)
+
+                    var offset: TimeInterval {
+                        if state.items.count > index + 1 {
+                            return state.timeValues[state.items[index + 1].timeIndex]
+                        } else {
+                            return state.timeValues.last! + 30 * 60
+                        }
+                    }
+
+                    let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
+
                     RectangleMark(
                         xStart: .value("start", startDate),
                         xEnd: .value("end", endDate),
@@ -216,6 +178,7 @@ extension CarbRatioEditor {
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
                 }
             }
+            .id(refreshUI) // Force chart update
             .chartXAxis {
                 AxisMarks(values: .automatic(desiredCount: 6)) { _ in
                     AxisValueLabel(format: .dateTime.hour())
@@ -223,8 +186,7 @@ extension CarbRatioEditor {
                 }
             }
             .chartXScale(
-                domain: Calendar.current.startOfDay(for: now) ... Calendar
-                    .current.startOfDay(for: now)
+                domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
                     .addingTimeInterval(60 * 60 * 24)
             )
             .chartYAxis {
@@ -234,10 +196,5 @@ extension CarbRatioEditor {
                 }
             }
         }
-
-        private func onDelete(offsets: IndexSet) {
-            state.items.remove(atOffsets: offsets)
-            state.validate()
-        }
     }
 }

+ 20 - 0
Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -19,6 +19,7 @@ extension ISFEditor {
 
         var items: [Item] = []
         var initialItems: [Item] = []
+        var therapyItems: [TherapySettingItem] = []
         var shouldDisplaySaving: Bool = false
 
         let context = CoreDataStack.shared.newTaskContext()
@@ -42,6 +43,25 @@ extension ISFEditor {
 
         private(set) var units: GlucoseUnits = .mgdL
 
+        // Convert items to TherapySettingItem format
+        func getTherapyItems() -> [TherapySettingItem] {
+            items.map { item in
+                TherapySettingItem(
+                    time: timeValues[item.timeIndex],
+                    value: rateValues[item.rateIndex]
+                )
+            }
+        }
+
+        // Update items from TherapySettingItem format
+        func updateFromTherapyItems(_ therapyItems: [TherapySettingItem]) {
+            items = therapyItems.map { therapyItem in
+                let timeIndex = timeValues.firstIndex(where: { abs($0 - therapyItem.time) < 1 }) ?? 0
+                let rateIndex = rateValues.firstIndex(of: therapyItem.value) ?? 0
+                return Item(rateIndex: rateIndex, timeIndex: timeIndex)
+            }
+        }
+
         override func subscribe() {
             units = settingsManager.settings.units
 

+ 86 - 132
Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -6,15 +6,17 @@ extension ISFEditor {
     struct RootView: BaseView {
         let resolver: Resolver
         @State var state = StateModel()
-        @State private var editMode = EditMode.inactive
+        @State private var refreshUI = UUID()
+        @State private var now = Date()
+        @Namespace private var bottomID
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
-        private var dateFormatter: DateFormatter {
-            let formatter = DateFormatter()
-            formatter.timeZone = TimeZone(secondsFromGMT: 0)
-            formatter.timeStyle = .short
+        private var numberFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = state.units == .mmolL ? 1 : 0
             return formatter
         }
 
@@ -67,143 +69,100 @@ extension ISFEditor {
         }
 
         var body: some View {
-            Form {
-                if !state.canAdd {
-                    Section {
-                        VStack(alignment: .leading) {
-                            Text(
-                                "Insulin Sensitivities cover 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space."
-                            ).bold()
-                        }
-                    }.listRowBackground(Color.tabBar)
-                }
+            ScrollViewReader { proxy in
+                VStack(spacing: 0) {
+                    ScrollView {
+                        LazyVStack {
+                            VStack(alignment: .leading, spacing: 0) {
+                                // Chart visualization
+                                if !state.items.isEmpty {
+                                    isfChart
+                                        .frame(height: 180)
+                                        .padding()
+                                        .background(Color.chart.opacity(0.65))
+                                        .clipShape(
+                                            .rect(
+                                                topLeadingRadius: 10,
+                                                bottomLeadingRadius: 0,
+                                                bottomTrailingRadius: 0,
+                                                topTrailingRadius: 10
+                                            )
+                                        )
+                                        .padding(.horizontal)
+                                        .padding(.top)
+                                }
 
-                Section(header: Text("Schedule")) {
-                    list
-                }.listRowBackground(Color.chart)
+                                // ISF list
+                                TherapySettingEditorView(
+                                    items: $state.therapyItems,
+                                    unit: state.units == .mgdL ? .mgdLPerUnit : .mmolLPerUnit,
+                                    timeOptions: state.timeValues,
+                                    valueOptions: state.rateValues,
+                                    validateOnDelete: state.validate,
+                                    onItemAdded: {
+                                        withAnimation {
+                                            proxy.scrollTo(bottomID, anchor: .bottom)
+                                        }
+                                    }
+                                )
+                                .padding(.horizontal)
 
-                Section {} header: {
-                    VStack(alignment: .leading, spacing: 10) {
-                        HStack {
-                            Image(systemName: "note.text.badge.plus").foregroundStyle(.primary)
-                            Text("Add an entry by tapping 'Add Sensitivity +' in the top right-hand corner of the screen.")
-                        }
-                        HStack {
-                            Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
-                            Text("Swipe left to delete a single entry. Tap on it, to edit its time or rate.")
-                        }
-                    }
-                    .textCase(nil)
-                }
-            }
-            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .onAppear(perform: configureView)
-            .navigationTitle("Insulin Sensitivities")
-            .navigationBarTitleDisplayMode(.automatic)
-            .toolbar(content: {
-                ToolbarItem(placement: .topBarTrailing) {
-                    Button(action: { state.add() }) {
-                        HStack {
-                            Text("Add Sensitivity")
-                            Image(systemName: "plus")
-                        }
-                    }.disabled(!state.canAdd)
-                }
-            })
-            .environment(\.editMode, $editMode)
-            .onAppear {
-                state.validate()
-            }
-        }
+                                HStack {
+                                    Image(systemName: "hand.draw.fill")
+                                        .padding(.leading)
 
-        private func pickers(for index: Int) -> some View {
-            Form {
-                Section {
-                    Picker(selection: $state.items[index].rateIndex, label: Text("Rate")) {
-                        ForEach(0 ..< state.rateValues.count, id: \.self) { i in
-                            Text(
-                                state.units == .mgdL ? state.rateValues[i].description : state.rateValues[i]
-                                    .formattedAsMmolL + String(localized: " \(state.units.rawValue)/U")
-                            ).tag(i)
-                        }
-                    }
-                }.listRowBackground(Color.chart)
-
-                Section {
-                    Picker(selection: $state.items[index].timeIndex, label: Text("Time")) {
-                        ForEach(0 ..< state.timeValues.count, id: \.self) { i in
-                            Text(
-                                self.dateFormatter
-                                    .string(from: Date(
-                                        timeIntervalSince1970: state
-                                            .timeValues[i]
-                                    ))
-                            ).tag(i)
+                                    Text("Swipe to delete a single entry. Tap on it, to edit its time or value.")
+                                        .padding(.trailing)
+                                }
+                                .font(.subheadline)
+                                .fontWeight(.light)
+                                .foregroundStyle(.secondary)
+                                .padding()
+                            }
                         }
                     }
-                }.listRowBackground(Color.chart)
-            }
-            .padding(.top)
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .navigationTitle("Set Rate")
-            .navigationBarTitleDisplayMode(.automatic)
-        }
-
-        private var list: some View {
-            List {
-                chart.padding(.vertical)
-                ForEach(state.items.indexed(), id: \.1.id) { index, item in
-                    let displayValue = state.units == .mgdL ? state.rateValues[item.rateIndex].description : state
-                        .rateValues[item.rateIndex].formattedAsMmolL
 
-                    NavigationLink(destination: pickers(for: index)) {
-                        HStack {
-                            Text("Rate").foregroundColor(.secondary)
-
-                            Text(
-                                displayValue + String(localized: " \(state.units.rawValue)/U")
-                            )
-                            Spacer()
-                            Text("starts at").foregroundColor(.secondary)
-                            Text(
-                                "\(dateFormatter.string(from: Date(timeIntervalSince1970: state.timeValues[item.timeIndex])))"
-                            )
-                        }
-                    }
-                    .moveDisabled(true)
+                    saveButton
+                }
+                .background(appState.trioBackgroundColor(for: colorScheme))
+                .onAppear(perform: configureView)
+                .navigationTitle("Insulin Sensitivities")
+                .navigationBarTitleDisplayMode(.automatic)
+                .onAppear {
+                    state.validate()
+                    state.therapyItems = state.getTherapyItems()
+                }
+                .onChange(of: state.therapyItems) { _, newItems in
+                    state.updateFromTherapyItems(newItems)
+                    refreshUI = UUID()
                 }
-                .onDelete(perform: onDelete)
             }
         }
 
-        var now = Date()
-
-        var chart: some View {
+        // Chart for visualizing ISF profile
+        private var isfChart: some View {
             Chart {
-                ForEach(state.items.indexed(), id: \.1.id) { index, item in
-                    let displayValue = state.units == .mgdL ? state.rateValues[item.rateIndex].description : state
-                        .rateValues[item.rateIndex].formattedAsMmolL
-
-                    // Convert from string so we know we use the same math as the rest of Trio.
-                    // However, swift doesn't understand languages that use comma as decimal delminator
-                    let displayValueFloat = Double(displayValue.replacingOccurrences(of: ",", with: "."))
+                ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
+                    let displayValue = state.rateValues[item.rateIndex]
 
                     let startDate = Calendar.current
                         .startOfDay(for: now)
                         .addingTimeInterval(state.timeValues[item.timeIndex])
 
-                    let endDate = state.items
-                        .count > index + 1 ?
-                        Calendar.current.startOfDay(for: now)
-                        .addingTimeInterval(state.timeValues[state.items[index + 1].timeIndex])
-                        :
-                        Calendar.current.startOfDay(for: now)
-                        .addingTimeInterval(state.timeValues.last! + 30 * 60)
+                    var offset: TimeInterval {
+                        if state.items.count > index + 1 {
+                            return state.timeValues[state.items[index + 1].timeIndex]
+                        } else {
+                            return state.timeValues.last! + 30 * 60
+                        }
+                    }
+
+                    let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
+
                     RectangleMark(
                         xStart: .value("start", startDate),
                         xEnd: .value("end", endDate),
-                        yStart: .value("rate-start", displayValueFloat ?? 0),
+                        yStart: .value("rate-start", displayValue),
                         yEnd: .value("rate-end", 0)
                     ).foregroundStyle(
                         .linearGradient(
@@ -216,13 +175,14 @@ extension ISFEditor {
                         )
                     ).alignsMarkStylesWithPlotArea()
 
-                    LineMark(x: .value("End Date", startDate), y: .value("ISF", displayValueFloat ?? 0))
+                    LineMark(x: .value("End Date", startDate), y: .value("ISF", displayValue))
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.cyan)
 
-                    LineMark(x: .value("Start Date", endDate), y: .value("ISF", displayValueFloat ?? 0))
+                    LineMark(x: .value("Start Date", endDate), y: .value("ISF", displayValue))
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.cyan)
                 }
             }
+            .id(refreshUI) // Force chart update
             .chartXAxis {
                 AxisMarks(values: .automatic(desiredCount: 6)) { _ in
                     AxisValueLabel(format: .dateTime.hour())
@@ -230,8 +190,7 @@ extension ISFEditor {
                 }
             }
             .chartXScale(
-                domain: Calendar.current.startOfDay(for: now) ... Calendar
-                    .current.startOfDay(for: now)
+                domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
                     .addingTimeInterval(60 * 60 * 24)
             )
             .chartYAxis {
@@ -241,10 +200,5 @@ extension ISFEditor {
                 }
             }
         }
-
-        private func onDelete(offsets: IndexSet) {
-            state.items.remove(atOffsets: offsets)
-            state.validate()
-        }
     }
 }

+ 60 - 51
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/BasalProfileStepView.swift

@@ -14,6 +14,7 @@ struct BasalProfileStepView: View {
     @State private var refreshUI = UUID() // to update chart when slider value changes
     @State private var therapyItems: [TherapySettingItem] = []
     @State private var now = Date()
+    @Namespace private var bottomID
 
     private var rateFormatter: NumberFormatter {
         let formatter = NumberFormatter()
@@ -29,69 +30,77 @@ struct BasalProfileStepView: View {
     }
 
     var body: some View {
-        LazyVStack {
-            VStack(alignment: .leading, spacing: 0) {
-                // Chart visualization
-                if !state.basalProfileItems.isEmpty {
-                    VStack(alignment: .leading) {
-                        basalProfileChart
-                            .frame(height: 180)
-                            .padding(.horizontal)
-                    }
-                    .padding(.vertical)
-                    .background(Color.chart.opacity(0.65))
-                    .clipShape(
-                        .rect(
-                            topLeadingRadius: 10,
-                            bottomLeadingRadius: 0,
-                            bottomTrailingRadius: 0,
-                            topTrailingRadius: 10
+        ScrollViewReader { proxy in
+            LazyVStack {
+                VStack(alignment: .leading, spacing: 0) {
+                    // Chart visualization
+                    if !state.basalProfileItems.isEmpty {
+                        VStack(alignment: .leading) {
+                            basalProfileChart
+                                .frame(height: 180)
+                                .padding(.horizontal)
+                        }
+                        .padding(.vertical)
+                        .background(Color.chart.opacity(0.65))
+                        .clipShape(
+                            .rect(
+                                topLeadingRadius: 10,
+                                bottomLeadingRadius: 0,
+                                bottomTrailingRadius: 0,
+                                topTrailingRadius: 10
+                            )
                         )
-                    )
-                }
+                    }
 
-                TherapySettingEditorView(
-                    items: $therapyItems,
-                    unit: .unitPerHour,
-                    timeOptions: state.basalProfileTimeValues,
-                    valueOptions: state.basalProfileRateValues,
-                    validateOnDelete: state.validateBasal
-                )
+                    TherapySettingEditorView(
+                        items: $therapyItems,
+                        unit: .unitPerHour,
+                        timeOptions: state.basalProfileTimeValues,
+                        valueOptions: state.basalProfileRateValues,
+                        validateOnDelete: state.validateBasal,
+                        onItemAdded: {
+                            withAnimation {
+                                proxy.scrollTo(bottomID, anchor: .bottom)
+                            }
+                        }
+                    )
 
-                Spacer(minLength: 20)
+                    Spacer(minLength: 20)
 
-                // Total daily basal calculation
-                if !state.basalProfileItems.isEmpty {
-                    VStack(alignment: .leading, spacing: 0) {
-                        HStack {
-                            Text("Total")
-                                .bold()
+                    // Total daily basal calculation
+                    if !state.basalProfileItems.isEmpty {
+                        VStack(alignment: .leading, spacing: 0) {
+                            HStack {
+                                Text("Total")
+                                    .bold()
 
-                            Spacer()
+                                Spacer()
 
-                            HStack {
-                                Text(rateFormatter.string(from: calculateTotalDailyBasal() as NSNumber) ?? "0")
-                                Text("U/day")
-                                    .foregroundStyle(Color.secondary)
+                                HStack {
+                                    Text(rateFormatter.string(from: calculateTotalDailyBasal() as NSNumber) ?? "0")
+                                    Text("U/day")
+                                        .foregroundStyle(Color.secondary)
+                                }
+                                .id(refreshUI) // Erzwingt die Aktualisierung des Totals
                             }
-                            .id(refreshUI) // Erzwingt die Aktualisierung des Totals
                         }
+                        .padding()
+                        .background(Color.chart.opacity(0.65))
+                        .cornerRadius(10)
+                        .id(bottomID)
                     }
-                    .padding()
-                    .background(Color.chart.opacity(0.65))
-                    .cornerRadius(10)
                 }
             }
-        }
-        .onAppear {
-            if state.basalProfileItems.isEmpty {
-                state.addInitialBasalRate()
+            .onAppear {
+                if state.basalProfileItems.isEmpty {
+                    state.addInitialBasalRate()
+                }
+                state.validateBasal()
+                therapyItems = state.getBasalTherapyItems()
+            }.onChange(of: therapyItems) { _, newItems in
+                state.updateBasal(from: newItems)
+                refreshUI = UUID()
             }
-            state.validateBasal()
-            therapyItems = state.getBasalTherapyItems()
-        }.onChange(of: therapyItems) { _, newItems in
-            state.updateBasal(from: newItems)
-            refreshUI = UUID()
         }
     }
 

+ 83 - 74
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/CarbRatioStepView.swift

@@ -14,6 +14,7 @@ struct CarbRatioStepView: View {
     @State private var refreshUI = UUID() // to update chart when slider value changes
     @State private var therapyItems: [TherapySettingItem] = []
     @State private var now = Date()
+    @Namespace private var bottomID
 
     private var formatter: NumberFormatter {
         let formatter = NumberFormatter()
@@ -30,97 +31,105 @@ struct CarbRatioStepView: View {
     }
 
     var body: some View {
-        LazyVStack {
-            VStack(alignment: .leading, spacing: 0) {
-                // Chart visualization
-                if !state.carbRatioItems.isEmpty {
-                    VStack(alignment: .leading) {
-                        carbRatioChart
-                            .frame(height: 180)
-                            .padding(.horizontal)
-                    }
-                    .padding(.vertical)
-                    .background(Color.chart.opacity(0.65))
-                    .clipShape(
-                        .rect(
-                            topLeadingRadius: 10,
-                            bottomLeadingRadius: 0,
-                            bottomTrailingRadius: 0,
-                            topTrailingRadius: 10
+        ScrollViewReader { proxy in
+            LazyVStack {
+                VStack(alignment: .leading, spacing: 0) {
+                    // Chart visualization
+                    if !state.carbRatioItems.isEmpty {
+                        VStack(alignment: .leading) {
+                            carbRatioChart
+                                .frame(height: 180)
+                                .padding(.horizontal)
+                        }
+                        .padding(.vertical)
+                        .background(Color.chart.opacity(0.65))
+                        .clipShape(
+                            .rect(
+                                topLeadingRadius: 10,
+                                bottomLeadingRadius: 0,
+                                bottomTrailingRadius: 0,
+                                topTrailingRadius: 10
+                            )
                         )
+                    }
+
+                    TherapySettingEditorView(
+                        items: $therapyItems,
+                        unit: .gramPerUnit,
+                        timeOptions: state.carbRatioTimeValues,
+                        valueOptions: state.carbRatioRateValues,
+                        validateOnDelete: state.validateCarbRatios,
+                        onItemAdded: {
+                            withAnimation {
+                                proxy.scrollTo(bottomID, anchor: .bottom)
+                            }
+                        }
                     )
-                }
 
-                TherapySettingEditorView(
-                    items: $therapyItems,
-                    unit: .gramPerUnit,
-                    timeOptions: state.carbRatioTimeValues,
-                    valueOptions: state.carbRatioRateValues,
-                    validateOnDelete: state.validateCarbRatios
-                )
-
-                // Example calculation based on first carb ratio
-                if !state.carbRatioItems.isEmpty {
-                    Spacer(minLength: 20)
-
-                    VStack(alignment: .leading, spacing: 8) {
-                        Text("Example Calculation")
-                            .font(.headline)
-                            .padding(.horizontal)
+                    // Example calculation based on first carb ratio
+                    if !state.carbRatioItems.isEmpty {
+                        Spacer(minLength: 20)
 
                         VStack(alignment: .leading, spacing: 8) {
-                            Text("For 45 g of carbs, you would need:")
-                                .font(.subheadline)
+                            Text("Example Calculation")
+                                .font(.headline)
                                 .padding(.horizontal)
 
-                            let insulinNeeded = 45 /
-                                Double(
-                                    truncating: state
-                                        .carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber
+                            VStack(alignment: .leading, spacing: 8) {
+                                Text("For 45 g of carbs, you would need:")
+                                    .font(.subheadline)
+                                    .padding(.horizontal)
+
+                                let insulinNeeded = 45 /
+                                    Double(
+                                        truncating: state
+                                            .carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber
+                                    )
+                                Text(
+                                    "45 \(String(localized: "g", comment: "Gram abbreviation")) / \(formatter.string(from: state.carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber) ?? "--") \(String(localized: "g/U")) = \(String(format: "%.1f", insulinNeeded))" +
+                                        " " + String(localized: "U", comment: "Insulin unit abbreviation")
                                 )
-                            Text(
-                                "45 \(String(localized: "g", comment: "Gram abbreviation")) / \(formatter.string(from: state.carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber) ?? "--") \(String(localized: "g/U")) = \(String(format: "%.1f", insulinNeeded))" +
-                                    " " + String(localized: "U", comment: "Insulin unit abbreviation")
-                            )
-                            .font(.system(.body, design: .monospaced))
-                            .foregroundColor(.orange)
-                            .padding()
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .background(Color.chart.opacity(0.65))
-                            .cornerRadius(10)
+                                .font(.system(.body, design: .monospaced))
+                                .foregroundColor(.orange)
+                                .padding()
+                                .frame(maxWidth: .infinity, alignment: .center)
+                                .background(Color.chart.opacity(0.65))
+                                .cornerRadius(10)
+                            }
                         }
-                    }
 
-                    Spacer(minLength: 20)
+                        Spacer(minLength: 20)
 
-                    // Information about the carb ratio
-                    VStack(alignment: .leading, spacing: 8) {
-                        Text("What This Means")
-                            .font(.headline)
-                            .padding(.horizontal)
+                        // Information about the carb ratio
+                        VStack(alignment: .leading, spacing: 8) {
+                            Text("What This Means")
+                                .font(.headline)
+                                .padding(.horizontal)
 
-                        VStack(alignment: .leading, spacing: 4) {
-                            Text("• A ratio of 10 g/U means 1 unit of insulin covers 10 g of carbs")
-                            Text("• A lower number means you need more insulin for the same amount of carbs")
-                            Text("• A higher number means you need less insulin for the same amount of carbs")
-                            Text("• Different times of day may require different ratios")
+                            VStack(alignment: .leading, spacing: 4) {
+                                Text("• A ratio of 10 g/U means 1 unit of insulin covers 10 g of carbs")
+                                Text("• A lower number means you need more insulin for the same amount of carbs")
+                                Text("• A higher number means you need less insulin for the same amount of carbs")
+                                Text("• Different times of day may require different ratios")
+                            }
+                            .font(.caption)
+                            .foregroundColor(.secondary)
+                            .padding(.horizontal)
                         }
-                        .font(.caption)
-                        .foregroundColor(.secondary)
-                        .padding(.horizontal)
+                        .id(bottomID)
                     }
                 }
             }
-        }
-        .onAppear {
-            if state.carbRatioItems.isEmpty {
-                state.addInitialCarbRatio()
+            .onAppear {
+                if state.carbRatioItems.isEmpty {
+                    state.addInitialCarbRatio()
+                }
+                state.validateCarbRatios()
+                therapyItems = state.getCarbRatioTherapyItems()
+            }.onChange(of: therapyItems) { _, newItems in
+                state.updateCarbRatio(from: newItems)
+                refreshUI = UUID()
             }
-            state.validateCarbRatios()
-            therapyItems = state.getCarbRatioTherapyItems()
-        }.onChange(of: therapyItems) { _, newItems in
-            state.updateCarbRatio(from: newItems)
-            refreshUI = UUID()
         }
     }
 

+ 44 - 36
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/GlucoseTargetStepView.swift

@@ -15,6 +15,7 @@ struct GlucoseTargetStepView: View {
     @State private var refreshUI = UUID() // to update chart when slider value changes
     @State private var therapyItems: [TherapySettingItem] = []
     @State private var now = Date()
+    @Namespace private var bottomID
 
     // Formatter for glucose values
     private var numberFormatter: NumberFormatter {
@@ -32,46 +33,53 @@ struct GlucoseTargetStepView: View {
     }
 
     var body: some View {
-        LazyVStack {
-            VStack(alignment: .leading, spacing: 0) {
-                // Chart visualization
-                if !state.targetItems.isEmpty {
-                    VStack(alignment: .leading) {
-                        glucoseTargetChart
-                            .frame(height: 180)
-                            .padding(.horizontal)
-                    }
-                    .padding(.vertical)
-                    .background(Color.chart.opacity(0.65))
-                    .clipShape(
-                        .rect(
-                            topLeadingRadius: 10,
-                            bottomLeadingRadius: 0,
-                            bottomTrailingRadius: 0,
-                            topTrailingRadius: 10
+        ScrollViewReader { proxy in
+            LazyVStack {
+                VStack(alignment: .leading, spacing: 0) {
+                    // Chart visualization
+                    if !state.targetItems.isEmpty {
+                        VStack(alignment: .leading) {
+                            glucoseTargetChart
+                                .frame(height: 180)
+                                .padding(.horizontal)
+                        }
+                        .padding(.vertical)
+                        .background(Color.chart.opacity(0.65))
+                        .clipShape(
+                            .rect(
+                                topLeadingRadius: 10,
+                                bottomLeadingRadius: 0,
+                                bottomTrailingRadius: 0,
+                                topTrailingRadius: 10
+                            )
                         )
-                    )
-                }
+                    }
 
-                // Glucose target list
-                TherapySettingEditorView(
-                    items: $therapyItems,
-                    unit: state.units == .mgdL ? .mgdL : .mmolL,
-                    timeOptions: state.targetTimeValues,
-                    valueOptions: state.targetRateValues,
-                    validateOnDelete: state.validateTarget
-                )
+                    // Glucose target list
+                    TherapySettingEditorView(
+                        items: $therapyItems,
+                        unit: state.units == .mgdL ? .mgdL : .mmolL,
+                        timeOptions: state.targetTimeValues,
+                        valueOptions: state.targetRateValues,
+                        validateOnDelete: state.validateTarget,
+                        onItemAdded: {
+                            withAnimation {
+                                proxy.scrollTo(bottomID, anchor: .bottom)
+                            }
+                        }
+                    ).id(bottomID)
+                }
             }
-        }
-        .onAppear {
-            if state.targetItems.isEmpty {
-                state.addInitialTarget()
+            .onAppear {
+                if state.targetItems.isEmpty {
+                    state.addInitialTarget()
+                }
+                state.validateTarget()
+                therapyItems = state.getTargetTherapyItems()
+            }.onChange(of: therapyItems) { _, newItems in
+                state.updateTargets(from: newItems)
+                refreshUI = UUID()
             }
-            state.validateTarget()
-            therapyItems = state.getTargetTherapyItems()
-        }.onChange(of: therapyItems) { _, newItems in
-            state.updateTargets(from: newItems)
-            refreshUI = UUID()
         }
     }
 

+ 90 - 81
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/InsulinSensitivityStepView.swift

@@ -14,6 +14,7 @@ struct InsulinSensitivityStepView: View {
     @State private var refreshUI = UUID() // to update chart when slider value changes
     @State private var therapyItems: [TherapySettingItem] = []
     @State private var now = Date()
+    @Namespace private var bottomID
 
     // For chart scaling
     private let chartScale = Calendar.current
@@ -34,101 +35,109 @@ struct InsulinSensitivityStepView: View {
     }
 
     var body: some View {
-        LazyVStack {
-            VStack(alignment: .leading, spacing: 0) {
-                // Chart visualization
-                if !state.isfItems.isEmpty {
-                    VStack(alignment: .leading) {
-                        isfChart
-                            .frame(height: 180)
-                            .padding(.horizontal)
-                    }
-                    .padding(.vertical)
-                    .background(Color.chart.opacity(0.65))
-                    .clipShape(
-                        .rect(
-                            topLeadingRadius: 10,
-                            bottomLeadingRadius: 0,
-                            bottomTrailingRadius: 0,
-                            topTrailingRadius: 10
+        ScrollViewReader { proxy in
+            LazyVStack {
+                VStack(alignment: .leading, spacing: 0) {
+                    // Chart visualization
+                    if !state.isfItems.isEmpty {
+                        VStack(alignment: .leading) {
+                            isfChart
+                                .frame(height: 180)
+                                .padding(.horizontal)
+                        }
+                        .padding(.vertical)
+                        .background(Color.chart.opacity(0.65))
+                        .clipShape(
+                            .rect(
+                                topLeadingRadius: 10,
+                                bottomLeadingRadius: 0,
+                                bottomTrailingRadius: 0,
+                                topTrailingRadius: 10
+                            )
                         )
+                    }
+
+                    TherapySettingEditorView(
+                        items: $therapyItems,
+                        unit: state.units == .mgdL ? .mgdLPerUnit : .mmolLPerUnit,
+                        timeOptions: state.isfTimeValues,
+                        valueOptions: state.isfRateValues,
+                        validateOnDelete: state.validateISF,
+                        onItemAdded: {
+                            withAnimation {
+                                proxy.scrollTo(bottomID, anchor: .bottom)
+                            }
+                        }
                     )
-                }
 
-                TherapySettingEditorView(
-                    items: $therapyItems,
-                    unit: state.units == .mgdL ? .mgdLPerUnit : .mmolLPerUnit,
-                    timeOptions: state.isfTimeValues,
-                    valueOptions: state.isfRateValues,
-                    validateOnDelete: state.validateISF
-                )
-
-                // Example calculation based on first ISF
-                if !state.isfItems.isEmpty {
-                    Spacer(minLength: 20)
-
-                    VStack(alignment: .leading, spacing: 8) {
-                        Text("Example Calculation")
-                            .font(.headline)
-                            .padding(.horizontal)
+                    // Example calculation based on first ISF
+                    if !state.isfItems.isEmpty {
+                        Spacer(minLength: 20)
 
                         VStack(alignment: .leading, spacing: 8) {
-                            // Current glucose is 40 mg/dL or 2.2 mmol/L above target
-                            let aboveTarget = state.units == .mgdL ? Decimal(40) : 40.asMmolL
-                            let firstIsfRate: Decimal = state.isfRateValues[state.isfItems.first?.rateIndex ?? 0]
-                            let isfValue = state.units == .mgdL ? firstIsfRate : firstIsfRate.asMmolL
-                            let insulinNeeded = aboveTarget / isfValue
-
-                            Text(
-                                "If you are \(numberFormatter.string(from: aboveTarget as NSNumber) ?? "--") \(state.units.rawValue) above target:"
-                            )
-                            .font(.subheadline)
-                            .padding(.horizontal)
-
-                            Text(
-                                "\(aboveTarget.description) \(state.units.rawValue) / \(isfValue.description) \(state.units.rawValue)/\(String(localized: "U", comment: "Insulin unit abbreviation")) = \(String(format: "%.1f", Double(insulinNeeded))) \(String(localized: "U", comment: "Insulin unit abbreviation"))"
-                            )
-                            .font(.system(.body, design: .monospaced))
-                            .foregroundColor(.cyan)
-                            .padding()
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .background(Color.chart.opacity(0.65))
-                            .cornerRadius(10)
+                            Text("Example Calculation")
+                                .font(.headline)
+                                .padding(.horizontal)
+
+                            VStack(alignment: .leading, spacing: 8) {
+                                // Current glucose is 40 mg/dL or 2.2 mmol/L above target
+                                let aboveTarget = state.units == .mgdL ? Decimal(40) : 40.asMmolL
+                                let firstIsfRate: Decimal = state.isfRateValues[state.isfItems.first?.rateIndex ?? 0]
+                                let isfValue = state.units == .mgdL ? firstIsfRate : firstIsfRate.asMmolL
+                                let insulinNeeded = aboveTarget / isfValue
+
+                                Text(
+                                    "If you are \(numberFormatter.string(from: aboveTarget as NSNumber) ?? "--") \(state.units.rawValue) above target:"
+                                )
+                                .font(.subheadline)
+                                .padding(.horizontal)
+
+                                Text(
+                                    "\(aboveTarget.description) \(state.units.rawValue) / \(isfValue.description) \(state.units.rawValue)/\(String(localized: "U", comment: "Insulin unit abbreviation")) = \(String(format: "%.1f", Double(insulinNeeded))) \(String(localized: "U", comment: "Insulin unit abbreviation"))"
+                                )
+                                .font(.system(.body, design: .monospaced))
+                                .foregroundColor(.cyan)
+                                .padding()
+                                .frame(maxWidth: .infinity, alignment: .center)
+                                .background(Color.chart.opacity(0.65))
+                                .cornerRadius(10)
+                            }
                         }
-                    }
 
-                    Spacer(minLength: 20)
+                        Spacer(minLength: 20)
 
-                    // Information about ISF
-                    VStack(alignment: .leading, spacing: 8) {
-                        Text("What This Means")
-                            .font(.headline)
+                        // Information about ISF
+                        VStack(alignment: .leading, spacing: 8) {
+                            Text("What This Means")
+                                .font(.headline)
+                                .padding(.horizontal)
+
+                            VStack(alignment: .leading, spacing: 4) {
+                                let isfValue = "\(state.units == .mgdL ? Decimal(50) : 50.asMmolL)"
+                                Text(
+                                    "• An ISF of \(isfValue) \(state.units.rawValue)/U means 1 U lowers your glucose by \(isfValue) \(state.units.rawValue)"
+                                )
+                                Text("• A lower number means you're less sensitive (more resistant) to insulin")
+                                Text("• A higher number means you're more sensitive (less resistant) to insulin")
+                            }
+                            .font(.caption)
+                            .foregroundColor(.secondary)
                             .padding(.horizontal)
-
-                        VStack(alignment: .leading, spacing: 4) {
-                            let isfValue = "\(state.units == .mgdL ? Decimal(50) : 50.asMmolL)"
-                            Text(
-                                "• An ISF of \(isfValue) \(state.units.rawValue)/U means 1 U lowers your glucose by \(isfValue) \(state.units.rawValue)"
-                            )
-                            Text("• A lower number means you're less sensitive (more resistant) to insulin")
-                            Text("• A higher number means you're more sensitive (less resistant) to insulin")
                         }
-                        .font(.caption)
-                        .foregroundColor(.secondary)
-                        .padding(.horizontal)
+                        .id(bottomID)
                     }
                 }
             }
-        }
-        .onAppear {
-            if state.isfItems.isEmpty {
-                state.addInitialISF()
+            .onAppear {
+                if state.isfItems.isEmpty {
+                    state.addInitialISF()
+                }
+                state.validateISF()
+                therapyItems = state.getISFTherapyItems()
+            }.onChange(of: therapyItems) { _, newItems in
+                state.updateISF(from: newItems)
+                refreshUI = UUID()
             }
-            state.validateISF()
-            therapyItems = state.getISFTherapyItems()
-        }.onChange(of: therapyItems) { _, newItems in
-            state.updateISF(from: newItems)
-            refreshUI = UUID()
         }
     }
 

+ 148 - 102
Trio/Sources/Modules/Onboarding/View/TherapySettingEditorView.swift

@@ -6,126 +6,146 @@ struct TherapySettingEditorView: View {
     var timeOptions: [TimeInterval]
     var valueOptions: [Decimal]
     var validateOnDelete: (() -> Void)?
+    var onItemAdded: (() -> Void)?
 
     @State private var selectedItemID: UUID?
+    @Namespace var bottomID
 
     var body: some View {
-        List {
-            HStack {
-                Text("Entries").bold()
-                Spacer()
-                Button {
-                    // Prepare and add new entry
-                    let lastTime = items.last?.time ?? 0
-                    let newTime = min(lastTime + 1800, 23 * 3600 + 1800)
-                    let newValue = items.last?.value ?? 1.0
-                    items.append(TherapySettingItem(time: newTime, value: newValue))
-
-                    // Reset selected item to close picker
-                    selectedItemID = nil
+        ScrollViewReader { proxy in
+            ScrollView {
+                HStack {
+                    Text("Entries").bold()
+                        .padding([.top, .bottom], 10)
+                        .padding(.leading, 20)
+                    Spacer()
+                    Button {
+                        // Prepare and add new entry
+                        let lastTime = items.last?.time ?? 0
+                        let newTime = min(lastTime + 1800, 23 * 3600 + 1800)
+                        let newValue = items.last?.value ?? 1.0
+                        items.append(TherapySettingItem(time: newTime, value: newValue))
 
-                    // Sort items, in case user has changed time of one item, then taps 'Add'
-                    sortTherapyItems()
-                } label: {
-                    HStack {
-                        Image(systemName: "plus.circle.fill")
-                        Text("Add")
-                    }.foregroundColor(.accentColor)
-                }
-                .disabled(items.count >= 48)
-            }
-            .listRowBackground(Color.chart.opacity(0.65))
-            .padding(.vertical, 5)
+                        // Reset selected item to close picker
+                        selectedItemID = nil
 
-            ForEach($items) { $item in
-                VStack(spacing: 0) {
-                    Button {
-                        selectedItemID = selectedItemID == item.id ? nil : item.id
+                        // Sort items, in case user has changed time of one item, then taps 'Add'
                         sortTherapyItems()
+
+                        // scroll to bottom when adding a new item
+                        withAnimation {
+                            proxy.scrollTo(bottomID)
+                        }
+
+                        // Notify parent view to scroll
+                        onItemAdded?()
                     } label: {
                         HStack {
-                            HStack {
-                                Text(displayText(for: unit, decimalValue: item.value))
-                                    .foregroundStyle(
-                                        selectedItemID == item.id ? Color.accentColor : Color
-                                            .primary
-                                    )
-                                Text(unit.displayName)
-                                    .foregroundStyle(Color.secondary)
-                            }
+                            Image(systemName: "plus.circle.fill")
+                            Text("Add")
+                        }.foregroundColor(cannotAddMoreEntries ? .secondary : .accentColor)
+                            .padding([.top, .bottom], 10)
+                            .padding(.trailing, 20)
+                    }
+                    .disabled(cannotAddMoreEntries)
+                }
+                .background(Color.chart.opacity(0.65))
+                .padding(.bottom, -10)
+
+                List {
+                    ForEach($items) { $item in
+                        VStack(spacing: 0) {
+                            Button {
+                                selectedItemID = selectedItemID == item.id ? nil : item.id
+                                sortTherapyItems()
+                            } label: {
+                                HStack {
+                                    HStack {
+                                        Text(displayText(for: unit, decimalValue: item.value))
+                                            .foregroundStyle(
+                                                selectedItemID == item.id ? Color.accentColor : Color
+                                                    .primary
+                                            )
+                                        Text(unit.displayName)
+                                            .foregroundStyle(Color.secondary)
+                                    }
 
-                            Spacer()
+                                    Spacer()
+
+                                    HStack {
+                                        Text("starts at").foregroundStyle(Color.secondary)
+                                        let timeIndex = timeOptions.firstIndex { abs($0 - item.time) < 1 } ?? 0
+                                        let time = timeOptions[timeIndex]
+                                        let date = Date(timeIntervalSince1970: time)
+                                        let timeString = timeFormatter.string(from: date)
+                                        Text(timeString)
+                                            .foregroundStyle(selectedItemID == item.id ? Color.accentColor : Color.primary)
+                                    }
+                                }
+                                .contentShape(Rectangle())
+                            }
+                            .buttonStyle(.plain)
 
-                            HStack {
-                                Text("starts at").foregroundStyle(Color.secondary)
-                                let timeIndex = timeOptions.firstIndex { abs($0 - item.time) < 1 } ?? 0
-                                let time = timeOptions[timeIndex]
-                                let date = Date(timeIntervalSince1970: time)
-                                let timeString = timeFormatter.string(from: date)
-                                Text(timeString).foregroundStyle(selectedItemID == item.id ? Color.accentColor : Color.primary)
+                            if selectedItemID == item.id {
+                                timeValuePickerRow(
+                                    item: $item,
+                                    timeOptions: timeOptions,
+                                    valueOptions: valueOptions,
+                                    unit: unit
+                                )
+                                .transition(.slide)
+                            }
+                        }
+                        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                            if let index = items.firstIndex(where: { $0.id == item.id }), items.count > 1 {
+                                Button(role: .destructive) {
+                                    items.remove(at: index)
+                                    selectedItemID = nil
+                                    validateTherapySettingItems()
+                                } label: {
+                                    Label("Delete", systemImage: "trash")
+                                }
+                                .tint(.red)
                             }
                         }
-                        .contentShape(Rectangle())
                     }
-                    .buttonStyle(.plain)
-
-                    if selectedItemID == item.id {
-                        timeValuePickerRow(
-                            item: $item,
-                            timeOptions: timeOptions,
-                            valueOptions: valueOptions,
-                            unit: unit
+                    .listRowBackground(Color.chart.opacity(0.65))
+
+                    Rectangle().fill(Color.chart.opacity(0.65)).frame(height: 10)
+                        .clipShape(
+                            .rect(
+                                topLeadingRadius: 0,
+                                bottomLeadingRadius: 10,
+                                bottomTrailingRadius: 10,
+                                topTrailingRadius: 0
+                            )
                         )
-                        .transition(.slide)
-                    }
+                        .listRowBackground(Color.clear)
+                        .listRowInsets(EdgeInsets(top: -22, leading: 0, bottom: 0, trailing: 0))
+                        .listRowSeparator(.hidden)
                 }
-                .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                    if let index = items.firstIndex(where: { $0.id == item.id }), items.count > 1 {
-                        Button(role: .destructive) {
-                            items.remove(at: index)
-                            selectedItemID = nil
-                            validateTherapySettingItems()
-                        } label: {
-                            Label("Delete", systemImage: "trash")
-                        }
-                    }
+                .id(bottomID)
+                .listStyle(.plain)
+                .scrollContentBackground(.hidden)
+                // 55 for header row, item counts x 45 for every entry row + 230 for a visible picker row
+                .frame(height: 55 + CGFloat(items.count) * 45 + (items.contains(where: { $0.id == selectedItemID }) ? 230 : 0))
+                .onAppear {
+                    // ensure picker is closed when view appears
+                    selectedItemID = nil
+                    // sorts items
+                    validateTherapySettingItems()
                 }
+                .onDisappear {
+                    // ensure picker is closed when view appears
+                    selectedItemID = nil
+                    // sorts items
+                    validateTherapySettingItems()
+                }
+                .onChange(of: items, { _, _ in
+                    validateTherapySettingItems()
+                })
             }
-            .listRowBackground(Color.chart.opacity(0.65))
-
-            Rectangle().fill(Color.chart.opacity(0.65)).frame(height: 10)
-                .clipShape(
-                    .rect(
-                        topLeadingRadius: 0,
-                        bottomLeadingRadius: 10,
-                        bottomTrailingRadius: 10,
-                        topTrailingRadius: 0
-                    )
-                )
-                .listRowBackground(Color.clear)
-                .listRowInsets(EdgeInsets(top: -22, leading: 0, bottom: 0, trailing: 0))
-                .listRowSeparator(.hidden)
-        }
-        .listStyle(.plain)
-        .scrollDisabled(true)
-        .scrollContentBackground(.hidden)
-        // 55 for header row, item counts x 45 for every entry row + 230 for a visible picker row
-        .frame(height: 55 + CGFloat(items.count) * 45 + (items.contains(where: { $0.id == selectedItemID }) ? 230 : 0))
-        .onAppear {
-            // ensure picker is closed when view appears
-            selectedItemID = nil
-            // sorts items
-            validateTherapySettingItems()
         }
-        .onDisappear {
-            // ensure picker is closed when view appears
-            selectedItemID = nil
-            // sorts items
-            validateTherapySettingItems()
-        }
-        .onChange(of: items, { _, _ in
-            validateTherapySettingItems()
-        })
     }
 
     @ViewBuilder private func timeValuePickerRow(
@@ -185,6 +205,21 @@ struct TherapySettingEditorView: View {
         .padding(.vertical, 8)
     }
 
+    /// Check if we can add more entries
+    /// Disabled when: 48 entries OR last entry is at 23:30 (84600 seconds)
+    private var cannotAddMoreEntries: Bool {
+        if items.count >= 48 {
+            return true
+        }
+
+        // Check if last entry is at 23:30 (23.5 hours * 3600 seconds = 84600)
+        if let lastTime = items.last?.time, lastTime >= 84600 {
+            return true
+        }
+
+        return false
+    }
+
     private func sortTherapyItems() {
         Task { @MainActor in
             withAnimation {
@@ -194,6 +229,11 @@ struct TherapySettingEditorView: View {
     }
 
     private func validateTherapySettingItems() {
+        // Store the time value of the currently selected item (if any)
+        let selectedTime = selectedItemID.flatMap { id in
+            items.first(where: { $0.id == id })?.time
+        }
+
         // validates therapy items (i.e. parsed therapy settings into wrapper class)
         var newItems = Array(Set(items)).sorted { $0.time < $1.time }
         if !newItems.isEmpty {
@@ -207,6 +247,11 @@ struct TherapySettingEditorView: View {
         // force ALL items to have new UUIDs (to enforce binding update)
         items = newItems.map { TherapySettingItem(copying: $0, newID: true) }
 
+        // Restore selection by finding the item with the same time value
+        if let selectedTime = selectedTime {
+            selectedItemID = items.first(where: { $0.time == selectedTime })?.id
+        }
+
         // validates underlying "raw" therapy setting (i.e. item of type basal, target, isf, carb ratio)
         validateOnDelete?()
     }
@@ -299,6 +344,7 @@ enum TherapySettingUnit: String, CaseIterable {
         items: $previewItems,
         unit: .unitPerHour,
         timeOptions: stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 },
-        valueOptions: stride(from: 0.0, through: 10.0, by: 0.05).map { Decimal(round(100 * $0) / 100) }
+        valueOptions: stride(from: 0.0, through: 10.0, by: 0.05).map { Decimal(round(100 * $0) / 100) },
+        onItemAdded: nil
     )
 }

+ 1 - 1
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift

@@ -329,7 +329,7 @@ private struct BolusSelectionPopover: View {
     private func xOffset() -> CGFloat {
         // If the selected date is outside the visible domain, hide the popover
         guard selectedDate >= domain.start && selectedDate <= domain.end else { return 0 }
-        
+
         let domainDuration = domain.end.timeIntervalSince(domain.start)
         guard domainDuration > 0, chartWidth > 0 else { return 0 }
 

+ 20 - 0
Trio/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift

@@ -7,6 +7,7 @@ extension TargetsEditor {
 
         @Published var items: [Item] = []
         @Published var initialItems: [Item] = []
+        @Published var therapyItems: [TherapySettingItem] = []
         @Published var shouldDisplaySaving: Bool = false
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
@@ -28,6 +29,25 @@ extension TargetsEditor {
 
         private(set) var units: GlucoseUnits = .mgdL
 
+        // Convert items to TherapySettingItem format
+        func getTherapyItems() -> [TherapySettingItem] {
+            items.map { item in
+                TherapySettingItem(
+                    time: timeValues[item.timeIndex],
+                    value: rateValues[item.lowIndex]
+                )
+            }
+        }
+
+        // Update items from TherapySettingItem format
+        func updateFromTherapyItems(_ therapyItems: [TherapySettingItem]) {
+            items = therapyItems.map { therapyItem in
+                let timeIndex = timeValues.firstIndex(where: { abs($0 - therapyItem.time) < 1 }) ?? 0
+                let lowIndex = rateValues.firstIndex(of: therapyItem.value) ?? 0
+                return Item(lowIndex: lowIndex, highIndex: lowIndex, timeIndex: timeIndex)
+            }
+        }
+
         override func subscribe() {
             units = settingsManager.settings.units
 

+ 95 - 133
Trio/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -6,15 +6,17 @@ extension TargetsEditor {
     struct RootView: BaseView {
         let resolver: Resolver
         @StateObject var state = StateModel()
-        @State private var editMode = EditMode.inactive
+        @State private var refreshUI = UUID()
+        @State private var now = Date()
+        @Namespace private var bottomID
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
-        private var dateFormatter: DateFormatter {
-            let formatter = DateFormatter()
-            formatter.timeZone = TimeZone(secondsFromGMT: 0)
-            formatter.timeStyle = .short
+        private var numberFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = state.units == .mmolL ? 1 : 0
             return formatter
         }
 
@@ -61,144 +63,106 @@ extension TargetsEditor {
         }
 
         var body: some View {
-            Form {
-                Section(header: Text("Schedule")) {
-                    list
-                }.listRowBackground(Color.chart)
-
-                Section {} header: {
-                    VStack(alignment: .leading, spacing: 10) {
-                        HStack {
-                            Image(systemName: "note.text.badge.plus").foregroundStyle(.primary)
-                            Text("Add an entry by tapping 'Add Target +' in the top right-hand corner of the screen.")
-                        }
-                        HStack {
-                            Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
-                            Text("Swipe to delete a single entry. Tap on it, to edit its time or rate.")
-                        }
-                    }
-                    .textCase(nil)
-                }
-            }
-            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .onAppear(perform: configureView)
-            .navigationTitle("Glucose Targets")
-            .navigationBarTitleDisplayMode(.automatic)
-            .toolbar(content: {
-                ToolbarItem(placement: .topBarTrailing) {
-                    Button(action: { state.add() }) {
-                        HStack {
-                            Text("Add Target")
-                            Image(systemName: "plus")
-                        }
-                    }.disabled(!state.canAdd)
-                }
-            })
-            .environment(\.editMode, $editMode)
-            .onAppear {
-                state.validate()
-            }
-        }
-
-        private func pickers(for index: Int) -> some View {
-            Form {
-                if !state.canAdd {
-                    Section {
-                        VStack(alignment: .leading) {
-                            Text(
-                                "Target Glucose covered for 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space."
-                            ).bold()
-                        }
-                    }.listRowBackground(Color.tabBar)
-                }
+            ScrollViewReader { proxy in
+                VStack(spacing: 0) {
+                    ScrollView {
+                        LazyVStack {
+                            VStack(alignment: .leading, spacing: 0) {
+                                // Chart visualization
+                                if !state.items.isEmpty {
+                                    glucoseTargetChart
+                                        .frame(height: 180)
+                                        .padding()
+                                        .background(Color.chart.opacity(0.65))
+                                        .clipShape(
+                                            .rect(
+                                                topLeadingRadius: 10,
+                                                bottomLeadingRadius: 0,
+                                                bottomTrailingRadius: 0,
+                                                topTrailingRadius: 10
+                                            )
+                                        )
+                                        .padding(.horizontal)
+                                        .padding(.top)
+                                }
 
-                Section {
-                    Picker(
-                        selection: $state.items[index].lowIndex,
-                        label: Text("Target ")
-                    ) {
-                        ForEach(0 ..< state.rateValues.count, id: \.self) { i in
-                            Text(state.units == .mgdL ? state.rateValues[i].description : state.rateValues[i].formattedAsMmolL)
-                                .tag(i)
-                        }
-                    }
-                }.listRowBackground(Color.chart)
-
-                Section {
-                    Picker(selection: $state.items[index].timeIndex, label: Text("Time")) {
-                        ForEach(0 ..< state.timeValues.count, id: \.self) { i in
-                            Text(
-                                self.dateFormatter
-                                    .string(from: Date(
-                                        timeIntervalSince1970: state
-                                            .timeValues[i]
-                                    ))
-                            ).tag(i)
+                                // Glucose target list
+                                TherapySettingEditorView(
+                                    items: $state.therapyItems,
+                                    unit: state.units == .mgdL ? .mgdL : .mmolL,
+                                    timeOptions: state.timeValues,
+                                    valueOptions: state.rateValues,
+                                    validateOnDelete: state.validate,
+                                    onItemAdded: {
+                                        withAnimation {
+                                            proxy.scrollTo(bottomID, anchor: .bottom)
+                                        }
+                                    }
+                                )
+                                .padding(.horizontal)
+                                .id(bottomID)
+
+                                HStack {
+                                    Image(systemName: "hand.draw.fill")
+                                        .padding(.leading)
+
+                                    Text("Swipe to delete a single entry. Tap on it, to edit its time or value.")
+                                        .padding(.trailing)
+                                }
+                                .font(.subheadline)
+                                .fontWeight(.light)
+                                .foregroundStyle(.secondary)
+                                .padding()
+                            }
                         }
                     }
-                }.listRowBackground(Color.chart)
-            }
-            .padding(.top)
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .navigationTitle("Set Target")
-            .navigationBarTitleDisplayMode(.automatic)
-        }
 
-        private var list: some View {
-            List {
-                chart.padding(.vertical)
-                ForEach(state.items.indexed(), id: \.1.id) { index, item in
-                    NavigationLink(destination: pickers(for: index)) {
-                        HStack {
-                            Text(
-                                state.units == .mgdL ? state.rateValues[item.lowIndex].description : state
-                                    .rateValues[item.lowIndex].formattedAsMmolL
-                            )
-                            Text("\(state.units.rawValue)").foregroundColor(.secondary)
-                            Spacer()
-                            Text("starts at").foregroundColor(.secondary)
-                            Text(
-                                "\(dateFormatter.string(from: Date(timeIntervalSince1970: state.timeValues[item.timeIndex])))"
-                            )
-                        }
-                    }
-                    .moveDisabled(true)
+                    saveButton
+                }
+                .background(appState.trioBackgroundColor(for: colorScheme))
+                .onAppear(perform: configureView)
+                .navigationTitle("Glucose Targets")
+                .navigationBarTitleDisplayMode(.automatic)
+                .onAppear {
+                    state.validate()
+                    state.therapyItems = state.getTherapyItems()
+                }
+                .onChange(of: state.therapyItems) { _, newItems in
+                    state.updateFromTherapyItems(newItems)
+                    refreshUI = UUID()
                 }
-                .onDelete(perform: onDelete)
             }
         }
 
-        var now = Date()
-        var chart: some View {
+        // Chart for visualizing glucose targets
+        private var glucoseTargetChart: some View {
             Chart {
-                ForEach(state.items.indexed(), id: \.1.id) { index, item in
-                    let displayValue = state.units == .mgdL ? state.rateValues[item.lowIndex].description : state
-                        .rateValues[item.lowIndex].formattedAsMmolL
-
-                    // Convert from string so we know we use the same math as the rest of Trio.
-                    // However, swift doesn't understand languages that use comma as decimal delminator
-                    let displayValueFloat = Double(displayValue.replacingOccurrences(of: ",", with: "."))
+                ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
+                    let rawValue = state.rateValues[item.lowIndex]
+                    let displayValue = state.units == .mgdL ? rawValue : rawValue.asMmolL
 
                     let startDate = Calendar.current
                         .startOfDay(for: now)
                         .addingTimeInterval(state.timeValues[item.timeIndex])
 
-                    let endDate = state.items
-                        .count > index + 1 ?
-                        Calendar.current.startOfDay(for: now)
-                        .addingTimeInterval(state.timeValues[state.items[index + 1].timeIndex])
-                        :
-                        Calendar.current.startOfDay(for: now)
-                        .addingTimeInterval(state.timeValues.last! + 30 * 60)
+                    var offset: TimeInterval {
+                        if state.items.count > index + 1 {
+                            return state.timeValues[state.items[index + 1].timeIndex]
+                        } else {
+                            return state.timeValues.last! + 30 * 60
+                        }
+                    }
 
-                    LineMark(x: .value("End Date", startDate), y: .value("Target", displayValueFloat ?? 0.0))
-                        .lineStyle(.init(lineWidth: 2.5)).foregroundStyle(Color.green.gradient)
+                    let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
 
-                    LineMark(x: .value("Start Date", endDate), y: .value("Target", displayValueFloat ?? 0.0))
-                        .lineStyle(.init(lineWidth: 2.5)).foregroundStyle(Color.green.gradient)
+                    LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
+                        .lineStyle(.init(lineWidth: 2.5)).foregroundStyle(Color.green)
+
+                    LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
+                        .lineStyle(.init(lineWidth: 2.5)).foregroundStyle(Color.green)
                 }
             }
+            .id(refreshUI) // Force chart update
             .chartXAxis {
                 AxisMarks(values: .automatic(desiredCount: 6)) { _ in
                     AxisValueLabel(format: .dateTime.hour())
@@ -206,8 +170,7 @@ extension TargetsEditor {
                 }
             }
             .chartXScale(
-                domain: Calendar.current.startOfDay(for: now) ... Calendar
-                    .current.startOfDay(for: now)
+                domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
                     .addingTimeInterval(60 * 60 * 24)
             )
             .chartYAxis {
@@ -215,12 +178,11 @@ extension TargetsEditor {
                     AxisValueLabel()
                     AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
                 }
-            }.chartYScale(domain: (state.units == .mgdL ? 72 : 4.0) ... (state.units == .mgdL ? 180 : 10))
-        }
-
-        private func onDelete(offsets: IndexSet) {
-            state.items.remove(atOffsets: offsets)
-            state.validate()
+            }
+            .chartYScale(
+                domain: (state.units == .mgdL ? Decimal(72) : Decimal(72).asMmolL) ...
+                    (state.units == .mgdL ? Decimal(180) : Decimal(180).asMmolL)
+            )
         }
     }
 }