Explorar o código

scroll to bottom when adding a new item

Marvin Polscheit hai 7 meses
pai
achega
88f20ec827

+ 12 - 0
Trio/Resources/InfoPlist.xcstrings

@@ -457,6 +457,18 @@
         }
       }
     },
+    "NSCalendarsFullAccessUsageDescription" : {
+      "comment" : "Privacy - Calendars Full Access Usage Description",
+      "extractionState" : "extracted_with_value",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay"
+          }
+        }
+      }
+    },
     "NSCalendarsUsageDescription" : {
       "comment" : "Privacy - Calendars Usage Description",
       "extractionState" : "extracted_with_value",

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

@@ -8836,6 +8836,9 @@
         }
       }
     },
+    "%lld h" : {
+
+    },
     "%lld hr" : {
       "localizations" : {
         "bg" : {

+ 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 45g 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 45g 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(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(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 10g 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 10g 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()
         }
     }
 

+ 91 - 82
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,102 +35,110 @@ 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)" +
+                                    "\(state.units.rawValue)"
+                                Text(
+                                    "• An ISF of \(isfValue) means 1 U lowers your glucose by \(isfValue)"
+                                )
+                                Text("• A lower number means you're more sensitive to insulin")
+                                Text("• A higher number means you're less sensitive to insulin")
+                            }
+                            .font(.caption)
+                            .foregroundColor(.secondary)
                             .padding(.horizontal)
-
-                        VStack(alignment: .leading, spacing: 4) {
-                            let isfValue = "\(state.units == .mgdL ? Decimal(50) : 50.asMmolL)" +
-                                "\(state.units.rawValue)"
-                            Text(
-                                "• An ISF of \(isfValue) means 1 U lowers your glucose by \(isfValue)"
-                            )
-                            Text("• A lower number means you're more sensitive to insulin")
-                            Text("• A higher number means you're less sensitive 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()
         }
     }
 

+ 106 - 89
Trio/Sources/Modules/Onboarding/View/TherapySettingEditorView.swift

@@ -6,112 +6,128 @@ 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 {
-        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
-
-                // Sort items, in case user has changed time of one item, then taps 'Add'
-                sortTherapyItems()
-            } label: {
+        ScrollViewReader { proxy in
+            ScrollView {
                 HStack {
-                    Image(systemName: "plus.circle.fill")
-                    Text("Add")
-                }.foregroundColor(.accentColor)
-            }
-            .disabled(items.count >= 48)
-        }
-        .listRowBackground(Color.chart.opacity(0.65))
-        .padding(.vertical, 10)
-
-        List {
-            ForEach($items) { $item in
-                VStack(spacing: 0) {
+                    Text("Entries").bold()
+                    Spacer()
                     Button {
-                        selectedItemID = selectedItemID == item.id ? nil : item.id
+                        // 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
+
+                        // 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(.accentColor)
+                    }
+                    .disabled(items.count >= 48)
+                }
+                .listRowBackground(Color.chart.opacity(0.65))
+                .padding(.vertical, 10)
 
-                            Spacer()
+                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)
+                                    }
 
-                            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)
+                                    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())
                             }
-                        }
-                        .contentShape(Rectangle())
-                    }
-                    .buttonStyle(.plain)
+                            .buttonStyle(.plain)
 
-                    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")
+                            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")
+                                }
+                            }
                         }
                     }
+                    .listRowBackground(Color.chart.opacity(0.65))
+                }
+                .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))
-        }
-        .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()
-        })
     }
 
     @ViewBuilder private func timeValuePickerRow(
@@ -285,6 +301,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
     )
 }