소스 검색

carb Ratios

polscm32 1 년 전
부모
커밋
178270f738
2개의 변경된 파일221개의 추가작업 그리고 152개의 파일을 삭제
  1. 3 9
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  2. 218 143
      Trio/Sources/Modules/Main/View/OnboardingSteps/CarbRatioStepView.swift

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

@@ -6729,9 +6729,6 @@
         }
       }
     },
-    "%@g" : {
-
-    },
     "%1$@ for %2$@" : {
       "comment" : "Summary string for temporary basal rate configuration page",
       "extractionState" : "manual",
@@ -13977,9 +13974,6 @@
         }
       }
     },
-    "=" : {
-
-    },
     ">  11  " : {
       "extractionState" : "stale",
       "localizations" : {
@@ -20531,9 +20525,6 @@
         }
       }
     },
-    "Add Carb Ratio" : {
-
-    },
     "Add carbs" : {
       "localizations" : {
         "bg" : {
@@ -44057,6 +44048,9 @@
         }
       }
     },
+    "Carb Ratio Profile" : {
+
+    },
     "Carb Ratio:" : {
       "localizations" : {
         "bg" : {

+ 218 - 143
Trio/Sources/Modules/Main/View/OnboardingSteps/CarbRatioStepView.swift

@@ -4,6 +4,7 @@
 //
 //  Created by Marvin Polscheit on 19.03.25.
 //
+import Charts
 import SwiftUI
 
 /// Carb ratio step view for setting insulin-to-carb ratio.
@@ -12,6 +13,10 @@ struct CarbRatioStepView: View {
     @State private var showTimeSelector = false
     @State private var selectedRatioIndex: Int?
 
+    // For chart scaling
+    private let chartScale = Calendar.current
+        .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
+
     private var formatter: NumberFormatter {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
@@ -27,171 +32,179 @@ struct CarbRatioStepView: View {
     }
 
     var body: some View {
-        VStack(alignment: .leading, spacing: 20) {
-            Text("Your carb ratio tells how many grams of carbohydrates one unit of insulin will cover.")
-                .font(.subheadline)
-                .foregroundColor(.secondary)
-
-            // Carb ratios list
-            VStack(alignment: .leading, spacing: 10) {
-                Text("Carb Ratios")
-                    .font(.headline)
-
-                if onboardingData.items.isEmpty {
-                    // Add default entry if no items exist
-                    Button("Add Initial Carb Ratio") {
-                        onboardingData.addCarbRatio()
-                    }
-                    .foregroundColor(.orange)
-                    .padding(.vertical, 8)
-                } else {
-                    ForEach(Array(onboardingData.items.enumerated()), id: \.element.id) { index, item in
-                        HStack {
-                            // Time display
-                            Text(
-                                dateFormatter
-                                    .string(from: Date(timeIntervalSince1970: onboardingData.timeValues[item.timeIndex]))
-                            )
-                            .frame(width: 80, alignment: .leading)
-
-                            // Ratio slider
-                            Slider(
-                                value: Binding(
-                                    get: { Double(truncating: onboardingData.rateValues[item.rateIndex] as NSNumber) },
-                                    set: { newValue in
-                                        // Find closest match in rateValues array
-                                        let newIndex = onboardingData.rateValues
-                                            .firstIndex { abs(Double($0) - newValue) < 0.05 } ?? item.rateIndex
-                                        onboardingData.items[index].rateIndex = newIndex
-                                    }
-                                ),
-                                in: Double(truncating: onboardingData.rateValues.first! as NSNumber) ...
-                                    Double(truncating: onboardingData.rateValues.last! as NSNumber),
-                                step: 0.5
-                            )
-                            .accentColor(.orange)
-
-                            // Display the current value
-                            Text("\(formatter.string(from: onboardingData.rateValues[item.rateIndex] as NSNumber) ?? "--") g/U")
-                                .frame(width: 70, alignment: .trailing)
-
-                            // Delete button (not for the first entry at 00:00)
-                            if index > 0 {
-                                Button(action: {
-                                    onboardingData.items.remove(at: index)
-                                }) {
-                                    Image(systemName: "trash")
-                                        .foregroundColor(.red)
-                                }
-                            }
-                        }
-                        .padding(.vertical, 8)
-                        .background(Color.orange.opacity(0.05))
-                        .cornerRadius(8)
+        ScrollView {
+            VStack(alignment: .leading, spacing: 20) {
+                Text("Your carb ratio tells how many grams of carbohydrates one unit of insulin will cover.")
+                    .font(.subheadline)
+                    .foregroundColor(.secondary)
+                    .padding(.horizontal)
+
+                // Chart visualization
+                if !onboardingData.items.isEmpty {
+                    VStack(alignment: .leading) {
+                        Text("Carb Ratio Profile")
+                            .font(.headline)
+                            .padding(.horizontal)
+
+                        carbRatioChart
+                            .frame(height: 180)
+                            .padding(.horizontal)
                     }
+                    .padding(.vertical, 5)
+                    .background(Color.orange.opacity(0.05))
+                    .cornerRadius(10)
                 }
-            }
 
-            // Add new carb ratio button
-            if !onboardingData.items.isEmpty && onboardingData.items.count < 24 {
-                Button(action: {
-                    showTimeSelector = true
-                }) {
+                // Carb ratios list
+                VStack(alignment: .leading, spacing: 10) {
                     HStack {
-                        Image(systemName: "plus.circle.fill")
-                        Text("Add Carb Ratio")
+                        Text("Carb Ratios")
+                            .font(.headline)
+
+                        Spacer()
+
+                        // Add new carb ratio button
+                        if onboardingData.items.count < 24 {
+                            Button(action: {
+                                showTimeSelector = true
+                            }) {
+                                HStack {
+                                    Image(systemName: "plus.circle.fill")
+                                    Text("Add Ratio")
+                                }
+                                .foregroundColor(.orange)
+                            }
+                            .disabled(!canAddRatio)
+                        }
                     }
-                    .foregroundColor(.orange)
-                    .padding(.vertical, 8)
-                }
-            }
+                    .padding(.horizontal)
 
-            Divider()
-
-            // Example calculation based on first carb ratio
-            if !onboardingData.items.isEmpty {
-                VStack(alignment: .leading, spacing: 8) {
-                    Text("Example Calculation")
-                        .font(.headline)
-                        .padding(.top)
-
-                    VStack(alignment: .leading, spacing: 4) {
-                        Text("For 45g of carbs, you would need:")
-                            .font(.subheadline)
-
-                        let insulinNeeded = 45 /
-                            Double(truncating: onboardingData.rateValues[onboardingData.items.first!.rateIndex] as NSNumber)
-                        Text(
-                            "45g ÷ \(formatter.string(from: onboardingData.rateValues[onboardingData.items.first!.rateIndex] as NSNumber) ?? "--") = \(String(format: "%.1f", insulinNeeded)) units of insulin"
-                        )
-                        .font(.system(.body, design: .monospaced))
+                    if onboardingData.items.isEmpty {
+                        // Add default entry if no items exist
+                        Button("Add Initial Carb Ratio") {
+                            onboardingData.addCarbRatio()
+                        }
                         .foregroundColor(.orange)
-                        .padding(.vertical, 8)
-                        .padding(.horizontal, 12)
+                        .padding()
+                        .frame(maxWidth: .infinity)
                         .background(Color.orange.opacity(0.1))
                         .cornerRadius(8)
-                    }
-                    .padding(.vertical, 4)
-                }
+                        .padding(.horizontal)
+                    } else {
+                        // List of carb ratios
+                        VStack(spacing: 2) {
+                            ForEach(Array(onboardingData.items.enumerated()), id: \.element.id) { index, item in
+                                HStack {
+                                    // Time display
+                                    Text(
+                                        dateFormatter
+                                            .string(from: Date(timeIntervalSince1970: onboardingData.timeValues[item.timeIndex]))
+                                    )
+                                    .frame(width: 80, alignment: .leading)
+                                    .padding(.leading)
+
+                                    // Ratio slider
+                                    Slider(
+                                        value: Binding(
+                                            get: { Double(truncating: onboardingData.rateValues[item.rateIndex] as NSNumber) },
+                                            set: { newValue in
+                                                // Find closest match in rateValues array
+                                                let newIndex = onboardingData.rateValues
+                                                    .firstIndex { abs(Double($0) - newValue) < 0.05 } ?? item.rateIndex
+                                                onboardingData.items[index].rateIndex = newIndex
+                                            }
+                                        ),
+                                        in: Double(truncating: onboardingData.rateValues.first! as NSNumber) ...
+                                            Double(truncating: onboardingData.rateValues.last! as NSNumber),
+                                        step: 0.5
+                                    )
+                                    .accentColor(.orange)
+                                    .padding(.horizontal, 5)
+
+                                    // Display the current value
+                                    Text(
+                                        "\(formatter.string(from: onboardingData.rateValues[item.rateIndex] as NSNumber) ?? "--") g/U"
+                                    )
+                                    .frame(width: 60, alignment: .trailing)
 
-                // Information about the carb ratio
-                VStack(alignment: .leading, spacing: 8) {
-                    Text("What This Means")
-                        .font(.headline)
-                        .padding(.top, 8)
-
-                    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")
+                                    // Delete button (not for the first entry at 00:00)
+                                    if index > 0 {
+                                        Button(action: {
+                                            onboardingData.items.remove(at: index)
+                                        }) {
+                                            Image(systemName: "trash")
+                                                .foregroundColor(.red)
+                                                .padding(.horizontal, 5)
+                                        }
+                                    } else {
+                                        // Spacer to maintain alignment
+                                        Spacer()
+                                            .frame(width: 30)
+                                    }
+                                }
+                                .padding(.vertical, 12)
+                                .background(index % 2 == 0 ? Color.orange.opacity(0.05) : Color.clear)
+                                .cornerRadius(8)
+                            }
+                        }
+                        .background(Color.orange.opacity(0.05))
+                        .cornerRadius(10)
+                        .padding(.horizontal)
                     }
-                    .font(.caption)
-                    .foregroundColor(.secondary)
                 }
-            }
 
-            // Visualization of carb ratio if we have ratios defined
-            if !onboardingData.items.isEmpty {
-                VStack(alignment: .leading, spacing: 8) {
-                    Text("Visual Reference")
-                        .font(.headline)
-                        .padding(.top)
-
-                    HStack(spacing: 20) {
-                        VStack {
-                            Image(systemName: "fork.knife")
-                                .font(.system(size: 40))
-                                .foregroundColor(.orange)
+                // Example calculation based on first carb ratio
+                if !onboardingData.items.isEmpty {
+                    Divider()
+                        .padding(.horizontal)
+
+                    VStack(alignment: .leading, spacing: 8) {
+                        Text("Example Calculation")
+                            .font(.headline)
+                            .padding(.horizontal)
+
+                        VStack(alignment: .leading, spacing: 4) {
+                            Text("For 45g of carbs, you would need:")
+                                .font(.subheadline)
+                                .padding(.horizontal)
+
+                            let insulinNeeded = 45 /
+                                Double(truncating: onboardingData.rateValues[onboardingData.items.first!.rateIndex] as NSNumber)
                             Text(
-                                "\(formatter.string(from: onboardingData.rateValues[onboardingData.items.first!.rateIndex] as NSNumber) ?? "--")g"
+                                "45g ÷ \(formatter.string(from: onboardingData.rateValues[onboardingData.items.first!.rateIndex] as NSNumber) ?? "--") = \(String(format: "%.1f", insulinNeeded)) units of insulin"
                             )
-                            .font(.headline)
-                            Text("Carbs")
-                                .font(.caption)
+                            .font(.system(.body, design: .monospaced))
+                            .foregroundColor(.orange)
+                            .padding(.vertical, 8)
+                            .padding(.horizontal, 12)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                            .background(Color.orange.opacity(0.1))
+                            .cornerRadius(8)
+                            .padding(.horizontal)
                         }
+                        .padding(.vertical, 4)
+                    }
+
+                    // Information about the carb ratio
+                    VStack(alignment: .leading, spacing: 8) {
+                        Text("What This Means")
+                            .font(.headline)
+                            .padding(.horizontal)
 
-                        Text("=")
-                            .font(.title)
-
-                        VStack {
-                            Image(systemName: "drop.fill")
-                                .font(.system(size: 40))
-                                .foregroundColor(.blue)
-                            Text("1U")
-                                .font(.headline)
-                            Text("Insulin")
-                                .font(.caption)
+                        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)
                     }
-                    .frame(maxWidth: .infinity)
-                    .padding()
-                    .background(Color.orange.opacity(0.1))
-                    .cornerRadius(12)
                 }
             }
+            .padding(.vertical)
         }
-        .padding()
         .actionSheet(isPresented: $showTimeSelector) {
             var buttons: [ActionSheet.Button] = []
 
@@ -224,4 +237,66 @@ struct CarbRatioStepView: View {
             )
         }
     }
+
+    // Computed property to check if we can add more carb ratios
+    private var canAddRatio: Bool {
+        guard let lastItem = onboardingData.items.last else { return true }
+        return lastItem.timeIndex < onboardingData.timeValues.count - 1
+    }
+
+    // Chart for visualizing carb ratios
+    private var carbRatioChart: some View {
+        Chart {
+            ForEach(Array(onboardingData.items.enumerated()), id: \.element.id) { index, item in
+                let displayValue = onboardingData.rateValues[item.rateIndex]
+
+                let tzOffset = TimeZone.current.secondsFromGMT() * -1
+                let startDate = Date(timeIntervalSinceReferenceDate: onboardingData.timeValues[item.timeIndex])
+                    .addingTimeInterval(TimeInterval(tzOffset))
+                let endDate = onboardingData.items.count > index + 1 ?
+                    Date(timeIntervalSinceReferenceDate: onboardingData.timeValues[onboardingData.items[index + 1].timeIndex])
+                    .addingTimeInterval(TimeInterval(tzOffset)) :
+                    Date(timeIntervalSinceReferenceDate: onboardingData.timeValues.last!).addingTimeInterval(30 * 60)
+                    .addingTimeInterval(TimeInterval(tzOffset))
+
+                RectangleMark(
+                    xStart: .value("start", startDate),
+                    xEnd: .value("end", endDate),
+                    yStart: .value("rate-start", displayValue),
+                    yEnd: .value("rate-end", 0)
+                ).foregroundStyle(
+                    .linearGradient(
+                        colors: [
+                            Color.orange.opacity(0.6),
+                            Color.orange.opacity(0.1)
+                        ],
+                        startPoint: .bottom,
+                        endPoint: .top
+                    )
+                ).alignsMarkStylesWithPlotArea()
+
+                LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
+
+                LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
+            }
+        }
+        .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: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
+                .addingTimeInterval(60 * 60 * 24)
+        )
+        .chartYAxis {
+            AxisMarks(values: .automatic(desiredCount: 4)) { _ in
+                AxisValueLabel()
+                AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+            }
+        }
+    }
 }