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

Major refactor to fix Double <-> Decimal parsing issues; NS import not string WIP

Deniz Cengiz 1 год назад
Родитель
Сommit
9fdbb5be87

+ 4 - 4
Trio.xcodeproj/project.pbxproj

@@ -553,7 +553,7 @@
 		DD3F1F832D9DC78800DCE7B3 /* UnitSelectionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */; };
 		DD3F1F852D9DD84000DCE7B3 /* DeliveryLimitsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */; };
 		DD3F1F872D9DDB1200DCE7B3 /* AnimationPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F862D9DDB1200DCE7B3 /* AnimationPlaceholder.swift */; };
-		DD3F1F892D9E078D00DCE7B3 /* TimeValueEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F882D9E078300DCE7B3 /* TimeValueEditorView.swift */; };
+		DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */; };
 		DD3F1F8B2D9E08B600DCE7B3 /* NightscoutLoginStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */; };
 		DD3F1F8D2D9E0E0600DCE7B3 /* NightscoutStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutStepView.swift */; };
 		DD3F1F902D9E153F00DCE7B3 /* NightscoutImportStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */; };
@@ -1336,7 +1336,7 @@
 		DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitSelectionStepView.swift; sourceTree = "<group>"; };
 		DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryLimitsStepView.swift; sourceTree = "<group>"; };
 		DD3F1F862D9DDB1200DCE7B3 /* AnimationPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationPlaceholder.swift; sourceTree = "<group>"; };
-		DD3F1F882D9E078300DCE7B3 /* TimeValueEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeValueEditorView.swift; sourceTree = "<group>"; };
+		DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingEditorView.swift; sourceTree = "<group>"; };
 		DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutLoginStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportStepView.swift; sourceTree = "<group>"; };
@@ -2735,7 +2735,7 @@
 		BD47FD152D88AAD80043966B /* View */ = {
 			isa = PBXGroup;
 			children = (
-				DD3F1F882D9E078300DCE7B3 /* TimeValueEditorView.swift */,
+				DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */,
 				DD3F1F862D9DDB1200DCE7B3 /* AnimationPlaceholder.swift */,
 				DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */,
 				BD47FD182D88AAF90043966B /* OnboardingView.swift */,
@@ -4330,7 +4330,7 @@
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
 				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
-				DD3F1F892D9E078D00DCE7B3 /* TimeValueEditorView.swift in Sources */,
+				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
 				DD1745132C54169400211FAC /* DevicesView.swift in Sources */,
 				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
 				1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */,

+ 6 - 13
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -5407,19 +5407,6 @@
         }
       }
     },
-    "%.1f" : {
-
-    },
-    "%.1f %@" : {
-      "localizations" : {
-        "en" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "%1$.1f %2$@"
-          }
-        }
-      }
-    },
     "%.2f" : {
 
     },
@@ -117663,6 +117650,9 @@
         }
       }
     },
+    "mg/dL/U" : {
+
+    },
     "Middleware" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -119823,6 +119813,9 @@
         }
       }
     },
+    "mmol/L/U" : {
+
+    },
     "mmol/mol" : {
       "localizations" : {
         "bg" : {

+ 4 - 6
Trio/Sources/Modules/Onboarding/OnboardingStateModel+Nightscout.swift

@@ -196,7 +196,7 @@ extension Onboarding.StateModel {
         targetItems = targetsProfile.targets.map { entry in
             let timeIndex = targetTimeValues.firstIndex(where: { Int($0) == entry.offset * 60 }) ?? 0
             let lowIndex = targetRateValues.enumerated().min(by: {
-                abs(Double($0.element) - Double(entry.low)) < abs(Double($1.element) - Double(entry.low))
+                abs($0.element - entry.low) < abs($1.element - entry.low)
             })?.offset ?? 0
 
             return TargetsEditor.Item(lowIndex: lowIndex, highIndex: lowIndex, timeIndex: timeIndex)
@@ -207,9 +207,8 @@ extension Onboarding.StateModel {
         basalProfileItems = basals.map { entry in
             let timeIndex = basalProfileTimeValues.firstIndex(where: { Int($0) == entry.minutes * 60 }) ?? 0
             let rateIndex = basalProfileRateValues.enumerated().min(by: {
-                abs(Double($0.element) - Double(entry.rate)) < abs(Double($1.element) - Double(entry.rate))
+                abs($0.element - entry.rate) < abs($1.element - entry.rate)
             })?.offset ?? 0
-
             return BasalProfileEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
         }
         initialBasalProfileItems = basalProfileItems
@@ -218,9 +217,8 @@ extension Onboarding.StateModel {
         carbRatioItems = carbratiosProfile.schedule.map { entry in
             let timeIndex = carbRatioTimeValues.firstIndex(where: { Int($0) == entry.offset * 60 }) ?? 0
             let rateIndex = carbRatioRateValues.enumerated().min(by: {
-                abs(Double($0.element) - Double(entry.ratio)) < abs(Double($1.element) - Double(entry.ratio))
+                abs($0.element - entry.ratio) < abs($1.element - entry.ratio)
             })?.offset ?? 0
-
             return CarbRatioEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
         }
         initialCarbRatioItems = carbRatioItems
@@ -229,7 +227,7 @@ extension Onboarding.StateModel {
         isfItems = sensitivitiesProfile.sensitivities.map { entry in
             let timeIndex = isfTimeValues.firstIndex(where: { Int($0) == entry.offset * 60 }) ?? 0
             let rateIndex = isfRateValues.enumerated().min(by: {
-                abs(Double($0.element) - Double(entry.sensitivity)) < abs(Double($1.element) - Double(entry.sensitivity))
+                abs($0.element - entry.sensitivity) < abs($1.element - entry.sensitivity)
             })?.offset ?? 0
 
             return ISFEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)

+ 51 - 43
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -28,13 +28,25 @@ extension Onboarding {
         var nightscoutImportStatus: ImportStatus = .finished
 
         // Carb Ratio related
+        let carbRatioPickerSetting = PickerSetting(value: 3, step: 0.1, min: 3, max: 50, type: .gram)
         var carbRatioItems: [CarbRatioEditor.Item] = []
         var initialCarbRatioItems: [CarbRatioEditor.Item] = []
         let carbRatioTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
             .sorted { $0 < $1 }
-        let carbRatioRateValues = stride(from: 30.0, to: 501.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
+        var carbRatioRateValues: [Decimal] { settingsProvider.generatePickerValues(from: carbRatioPickerSetting, units: units) }
 
         // Basal Profile related
+        var basalRatePickerSetting: PickerSetting {
+            switch pumpModel {
+            case .dana,
+                 .minimed:
+                return PickerSetting(value: 0.1, step: 0.1, min: 0.1, max: 30, type: .insulinUnit)
+            case .omnipodDash,
+                 .omnipodEros:
+                return PickerSetting(value: 0.5, step: 0.05, min: 0.5, max: 30, type: .insulinUnit)
+            }
+        }
+
         var initialBasalProfileItems: [BasalProfileEditor.Item] = []
         var basalProfileItems: [BasalProfileEditor.Item] = []
         let basalProfileTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
@@ -43,37 +55,27 @@ extension Onboarding {
             switch pumpModel {
             case .dana,
                  .minimed:
-                return stride(from: 0.1, to: 30.0, by: 0.1).map { Decimal($0) }
+                return settingsProvider.generatePickerValues(from: basalRatePickerSetting, units: units)
             case .omnipodDash,
                  .omnipodEros:
-                return stride(from: 0.05, to: 30.0, by: 0.05).map { Decimal($0) }
+                return settingsProvider.generatePickerValues(from: basalRatePickerSetting, units: units)
             }
         }
 
         // ISF related
+        var sensitivityPickerSetting = PickerSetting(value: 100, step: 1, min: 9, max: 540, type: .glucose)
         var isfItems: [ISFEditor.Item] = []
         var initialISFItems: [ISFEditor.Item] = []
         let isfTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }.sorted { $0 < $1 }
-        var isfRateValues: [Decimal] {
-            var values = stride(from: 9, to: 540.01, by: 1.0).map { Decimal($0) }
-
-            if units == .mmolL {
-                values = values.filter { Int(truncating: $0 as NSNumber) % 2 == 0 }
-            }
-
-            return values
-        }
+        var isfRateValues: [Decimal] { settingsProvider.generatePickerValues(from: sensitivityPickerSetting, units: units) }
 
         // Target related
+        let letTargetPickerSetting = PickerSetting(value: 100, step: 1, min: 72, max: 180, type: .glucose)
         var targetItems: [TargetsEditor.Item] = []
         var initialTargetItems: [TargetsEditor.Item] = []
         let targetTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
             .sorted { $0 < $1 }
-
-        var targetRateValues: [Decimal] {
-            let glucoseSetting = PickerSetting(value: 0, step: 1, min: 72, max: 180, type: .glucose)
-            return settingsProvider.generatePickerValues(from: glucoseSetting, units: units)
-        }
+        var targetRateValues: [Decimal] { settingsProvider.generatePickerValues(from: letTargetPickerSetting, units: units) }
 
         // Basal Profile
         var basalRates: [BasalRateEntry] = [BasalRateEntry(startTime: 0, rate: 1.0)]
@@ -114,7 +116,15 @@ extension Onboarding {
         func saveOnboardingData() {
             applyToSettings()
             applyToPreferences()
-            applyToPumpSettings()
+            Task {
+                await applyToPumpSettings()
+
+                // Store therapy settings
+                await saveTargets()
+                await saveBasalProfile()
+                await saveCarbRatios()
+                await saveISFValues()
+            }
         }
 
         /// Applies the onboarding data to the app's settings.
@@ -124,12 +134,6 @@ extension Onboarding {
 
             settingsCopy.units = units
 
-            // Store therapy settings
-            saveTargets()
-            saveBasalProfile()
-            saveCarbRatios()
-            saveISFValues()
-
             // We'll directly set the settings property which will trigger the didSet observer
             settingsManager.settings = settingsCopy
         }
@@ -144,10 +148,11 @@ extension Onboarding {
             settingsManager.preferences = preferencesCopy
         }
 
-        func applyToPumpSettings() {
+        func applyToPumpSettings() async {
             let defaultDIA = settingsProvider.settings.insulinPeakTime.value
             let pumpSettings = PumpSettings(insulinActionCurve: defaultDIA, maxBolus: maxBolus, maxBasal: maxBasal)
-            fileStorage.save(pumpSettings, as: OpenAPS.Settings.settings)
+
+            await fileStorage.saveAsync(pumpSettings, as: OpenAPS.Settings.settings)
 
             // TODO: is this actually necessary at this point? Nothing is set up yet, nothing is subscribed to this observer...
             DispatchQueue.main.async {
@@ -163,7 +168,7 @@ extension Onboarding {
                 TherapySettingItem(
                     id: UUID(),
                     time: targetTimeValues[$0.timeIndex],
-                    value: Double(targetRateValues[$0.lowIndex])
+                    value: targetRateValues[$0.lowIndex]
                 )
             }.sorted { $0.time < $1.time }
         }
@@ -171,7 +176,7 @@ extension Onboarding {
         func updateTargets(from therapyItems: [TherapySettingItem]) {
             targetItems = therapyItems.map { item in
                 let timeIndex = targetTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
-                let closestTargetIndex = targetRateValues.firstIndex(of: Decimal(item.value)) ?? 0
+                let closestTargetIndex = targetRateValues.firstIndex(of: item.value) ?? 0
 
                 return TargetsEditor.Item(lowIndex: closestTargetIndex, highIndex: closestTargetIndex, timeIndex: timeIndex)
             }.sorted { $0.timeIndex < $1.timeIndex }
@@ -182,7 +187,7 @@ extension Onboarding {
                 TherapySettingItem(
                     id: UUID(),
                     time: basalProfileTimeValues[$0.timeIndex],
-                    value: Double(basalProfileRateValues[$0.rateIndex])
+                    value: basalProfileRateValues[$0.rateIndex]
                 )
             }.sorted { $0.time < $1.time }
         }
@@ -190,7 +195,7 @@ extension Onboarding {
         func updateBasalRates(from therapyItems: [TherapySettingItem]) {
             basalProfileItems = therapyItems.map { item in
                 let timeIndex = basalProfileTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
-                let closestRateIndex = basalProfileRateValues.firstIndex(of: Decimal(item.value)) ?? 0
+                let closestRateIndex = basalProfileRateValues.firstIndex(of: item.value) ?? 0
 
                 return BasalProfileEditor.Item(rateIndex: closestRateIndex, timeIndex: timeIndex)
             }.sorted { $0.timeIndex < $1.timeIndex }
@@ -201,7 +206,7 @@ extension Onboarding {
                 TherapySettingItem(
                     id: UUID(),
                     time: carbRatioTimeValues[$0.timeIndex],
-                    value: Double(carbRatioRateValues[$0.rateIndex])
+                    value: carbRatioRateValues[$0.rateIndex]
                 )
             }.sorted { $0.time < $1.time }
         }
@@ -209,7 +214,7 @@ extension Onboarding {
         func updateCarbRatios(from therapyItems: [TherapySettingItem]) {
             carbRatioItems = therapyItems.map { item in
                 let timeIndex = carbRatioTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
-                let closestRateIndex = carbRatioRateValues.firstIndex(of: Decimal(item.value)) ?? 0
+                let closestRateIndex = carbRatioRateValues.firstIndex(of: item.value) ?? 0
 
                 return CarbRatioEditor.Item(rateIndex: closestRateIndex, timeIndex: timeIndex)
             }.sorted { $0.timeIndex < $1.timeIndex }
@@ -220,7 +225,7 @@ extension Onboarding {
                 TherapySettingItem(
                     id: UUID(),
                     time: isfTimeValues[$0.timeIndex],
-                    value: Double(isfRateValues[$0.rateIndex])
+                    value: isfRateValues[$0.rateIndex]
                 )
             }.sorted { $0.time < $1.time }
         }
@@ -228,7 +233,7 @@ extension Onboarding {
         func updateSensitivies(from therapyItems: [TherapySettingItem]) {
             isfItems = therapyItems.map { item in
                 let timeIndex = isfTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
-                let closestRateIndex = isfRateValues.firstIndex(of: Decimal(item.value)) ?? 0
+                let closestRateIndex = isfRateValues.firstIndex(of: item.value) ?? 0
 
                 return ISFEditor.Item(rateIndex: closestRateIndex, timeIndex: timeIndex)
             }.sorted { $0.timeIndex < $1.timeIndex }
@@ -255,7 +260,7 @@ extension Onboarding.StateModel {
         return false
     }
 
-    func saveCarbRatios() {
+    func saveCarbRatios() async {
         guard carbRatiosHaveChanges else { return }
 
         let schedule = carbRatioItems.enumerated().map { _, item -> CarbRatioEntry in
@@ -269,7 +274,7 @@ extension Onboarding.StateModel {
         }
         let profile = CarbRatios(units: .grams, schedule: schedule)
 
-        fileStorage.save(profile, as: OpenAPS.Settings.carbRatios)
+        await fileStorage.saveAsync(profile, as: OpenAPS.Settings.carbRatios)
 
         initialCarbRatioItems = carbRatioItems.map { CarbRatioEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
     }
@@ -293,7 +298,7 @@ extension Onboarding.StateModel {
         initialTargetItems != targetItems
     }
 
-    func saveTargets() {
+    func saveTargets() async {
         guard targetsHaveChanged else { return }
 
         let targets = targetItems.map { item -> BGTargetEntry in
@@ -302,13 +307,13 @@ extension Onboarding.StateModel {
             formatter.dateFormat = "HH:mm:ss"
             let date = Date(timeIntervalSince1970: self.targetTimeValues[item.timeIndex])
             let minutes = Int(date.timeIntervalSince1970 / 60)
-            let low = self.isfRateValues[item.lowIndex]
+            let low = self.targetRateValues[item.lowIndex]
             let high = low
             return BGTargetEntry(low: low, high: high, start: formatter.string(from: date), offset: minutes)
         }
         let profile = BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: targets)
 
-        fileStorage.save(profile, as: OpenAPS.Settings.bgTargets)
+        await fileStorage.saveAsync(profile, as: OpenAPS.Settings.bgTargets)
 
         initialTargetItems = targetItems
             .map { TargetsEditor.Item(lowIndex: $0.lowIndex, highIndex: $0.highIndex, timeIndex: $0.timeIndex) }
@@ -333,7 +338,7 @@ extension Onboarding.StateModel {
         initialISFItems != isfItems
     }
 
-    func saveISFValues() {
+    func saveISFValues() async {
         guard isfValuesHaveChanges else { return }
 
         let sensitivities = isfItems.map { item -> InsulinSensitivityEntry in
@@ -351,7 +356,7 @@ extension Onboarding.StateModel {
             sensitivities: sensitivities
         )
 
-        fileStorage.save(profile, as: OpenAPS.Settings.insulinSensitivities)
+        await fileStorage.saveAsync(profile, as: OpenAPS.Settings.insulinSensitivities)
 
         initialISFItems = isfItems.map { ISFEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
     }
@@ -385,7 +390,7 @@ extension Onboarding.StateModel {
         return false
     }
 
-    func saveBasalProfile() {
+    func saveBasalProfile() async {
         let profile = basalProfileItems.map { item -> BasalProfileEntry in
             let formatter = DateFormatter()
             formatter.timeZone = TimeZone(secondsFromGMT: 0)
@@ -396,7 +401,10 @@ extension Onboarding.StateModel {
             return BasalProfileEntry(start: formatter.string(from: date), minutes: minutes, rate: rate)
         }
 
-        fileStorage.save(profile, as: OpenAPS.Settings.basalProfile)
+        await fileStorage.saveAsync(profile, as: OpenAPS.Settings.basalProfile)
+
+        initialBasalProfileItems = basalProfileItems
+            .map { BasalProfileEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
     }
 
     func validateBasal() {

+ 2 - 2
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/BasalProfileStepView.swift

@@ -47,9 +47,9 @@ struct BasalProfileStepView: View {
                     .cornerRadius(10)
                 }
 
-                TimeValueEditorView(
+                TherapySettingEditorView(
                     items: $therapyItems,
-                    unit: String(localized: "U/hr"),
+                    unit: .unitPerHour,
                     timeOptions: state.basalProfileTimeValues,
                     valueOptions: state.basalProfileRateValues
                 )

+ 2 - 2
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/CarbRatioStepView.swift

@@ -47,9 +47,9 @@ struct CarbRatioStepView: View {
                     .cornerRadius(10)
                 }
 
-                TimeValueEditorView(
+                TherapySettingEditorView(
                     items: $therapyItems,
-                    unit: String(localized: "g/U"),
+                    unit: .gramPerUnit,
                     timeOptions: state.carbRatioTimeValues,
                     valueOptions: state.carbRatioRateValues
                 )

+ 7 - 4
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/GlucoseTargetStepView.swift

@@ -56,9 +56,9 @@ struct GlucoseTargetStepView: View {
                 }
 
                 // Glucose target list
-                TimeValueEditorView(
+                TherapySettingEditorView(
                     items: $therapyItems,
-                    unit: state.units.rawValue,
+                    unit: state.units == .mgdL ? .mgdL : .mmolL,
                     timeOptions: state.targetTimeValues,
                     valueOptions: state.targetRateValues
                 )
@@ -78,9 +78,12 @@ struct GlucoseTargetStepView: View {
 
     // Add initial target
     private func addTarget() {
-        // Default to midnight (00:00) and 1.0 U/h rate
         let timeIndex = state.targetTimeValues.firstIndex { abs($0 - 0) < 1 } ?? 0
-        let targetIndex = state.targetRateValues.firstIndex { abs(Double($0) - 100) < 0.05 } ?? 100
+        let expectedDefault = Decimal(100)
+
+        let targetIndex = state.targetRateValues.enumerated()
+            .min(by: { abs($0.element - expectedDefault) < abs($1.element - expectedDefault) })?
+            .offset ?? 0
 
         let newItem = TargetsEditor.Item(lowIndex: targetIndex, highIndex: targetIndex, timeIndex: timeIndex)
         state.targetItems.append(newItem)

+ 7 - 5
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/InsulinSensitivityStepView.swift

@@ -47,9 +47,9 @@ struct InsulinSensitivityStepView: View {
                     .cornerRadius(10)
                 }
 
-                TimeValueEditorView(
+                TherapySettingEditorView(
                     items: $therapyItems,
-                    unit: String(localized: "\(state.units.rawValue)/U"),
+                    unit: state.units == .mgdL ? .mgdLPerUnit : .mmolLPerUnit,
                     timeOptions: state.isfTimeValues,
                     valueOptions: state.isfRateValues
                 )
@@ -133,10 +133,12 @@ struct InsulinSensitivityStepView: View {
 
     // Add initial ISF value
     private func addInitialISF() {
-        // Default to midnight (00:00) and 50 mg/dL (or 2.8 mmol/L)
         let timeIndex = state.isfTimeValues.firstIndex { abs($0 - 0) < 1 } ?? 0
-        let defaultISF = state.units == .mgdL ? 50.0 : 2.8
-        let rateIndex = state.isfRateValues.firstIndex { abs(Double($0) - defaultISF) < 0.5 } ?? 45
+        let expectedDefault = Decimal(50)
+
+        let rateIndex = state.isfRateValues.enumerated()
+            .min(by: { abs($0.element - expectedDefault) < abs($1.element - expectedDefault) })?
+            .offset ?? 0
 
         let newItem = ISFEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
         state.isfItems.append(newItem)

+ 65 - 32
Trio/Sources/Modules/Onboarding/View/TimeValueEditorView.swift

@@ -1,8 +1,8 @@
 import SwiftUI
 
-struct TimeValueEditorView: View {
+struct TherapySettingEditorView: View {
     @Binding var items: [TherapySettingItem]
-    var unit: String
+    var unit: TherapySettingUnit
     var timeOptions: [TimeInterval]
     var valueOptions: [Decimal]
 
@@ -36,9 +36,9 @@ struct TimeValueEditorView: View {
                     } label: {
                         HStack {
                             HStack {
-                                Text("\(item.value, specifier: "%.1f")")
+                                Text(displayText(for: unit, decimalValue: item.value))
                                     .foregroundStyle(selectedItemID == item.id ? Color.accentColor : Color.primary)
-                                Text(unit.description)
+                                Text(unit.displayName)
                                     .foregroundStyle(Color.secondary)
                             }
 
@@ -58,7 +58,7 @@ struct TimeValueEditorView: View {
                     .buttonStyle(.plain)
 
                     if selectedItemID == item.id {
-                        TimeValuePickerRow(
+                        timeValuePickerRow(
                             item: $item,
                             timeOptions: timeOptions,
                             valueOptions: valueOptions,
@@ -100,42 +100,28 @@ struct TimeValueEditorView: View {
         // 55 for header row, item counts x 45 for every entry row + 230 for a visible picker row
     }
 
-    private var timeFormatter: DateFormatter {
-        let formatter = DateFormatter()
-        formatter.dateFormat = "HH:mm"
-        return formatter
-    }
-}
-
-struct TherapySettingItem: Identifiable, Equatable {
-    var id = UUID()
-    var time: TimeInterval // seconds since start of day
-    var value: Double
-}
-
-struct TimeValuePickerRow: View {
-    @Binding var item: TherapySettingItem
-    var timeOptions: [TimeInterval]
-    var valueOptions: [Decimal]
-    var unit: String
-
-    var body: some View {
+    @ViewBuilder private func timeValuePickerRow(
+        item: Binding<TherapySettingItem>,
+        timeOptions: [TimeInterval],
+        valueOptions: [Decimal],
+        unit: TherapySettingUnit
+    ) -> some View {
         VStack(spacing: 8) {
             HStack {
                 Picker("Value", selection: Binding(
-                    get: { item.value },
-                    set: { item.value = $0 }
+                    get: { Double(item.wrappedValue.value) },
+                    set: { item.wrappedValue.value = Decimal($0) }
                 )) {
                     ForEach(valueOptions, id: \.self) { value in
-                        Text("\(Double(value), specifier: "%.1f") \(unit)").tag(Double(value))
+                        Text("\(displayText(for: unit, decimalValue: value)) \(unit.displayName)").tag(Double(value))
                     }
                 }
                 .frame(maxWidth: .infinity)
                 .clipped()
 
                 Picker("Time", selection: Binding(
-                    get: { timeOptions.contains(item.time) ? item.time : timeOptions.first ?? 0 },
-                    set: { item.time = $0 }
+                    get: { item.wrappedValue.time },
+                    set: { item.wrappedValue.time = $0 }
                 )) {
                     ForEach(timeOptions, id: \.self) { time in
                         Text(timeFormatter.string(from: Date(timeIntervalSince1970: time)))
@@ -156,6 +142,53 @@ struct TimeValuePickerRow: View {
         formatter.timeStyle = .short
         return formatter
     }
+
+    private func displayText(for unit: TherapySettingUnit, decimalValue: Decimal) -> String {
+        switch unit {
+        case .mmolL,
+             .mmolLPerUnit:
+            return decimalValue.formattedAsMmolL
+        case .gramPerUnit,
+             .mgdL,
+             .mgdLPerUnit,
+             .unitPerHour:
+            return decimalValue.description
+        }
+    }
+}
+
+struct TherapySettingItem: Identifiable, Equatable, Hashable {
+    var id = UUID()
+    var time: TimeInterval // seconds since start of day
+    var value: Decimal
+}
+
+enum TherapySettingUnit: String, CaseIterable {
+    case mmolLPerUnit
+    case mgdLPerUnit
+    case unitPerHour
+    case gramPerUnit
+    case mmolL
+    case mgdL
+
+    var id: String { rawValue }
+
+    var displayName: String {
+        switch self {
+        case .mmolLPerUnit:
+            return String(localized: "mmol/L/U")
+        case .mgdLPerUnit:
+            return String(localized: "mg/dL/U")
+        case .unitPerHour:
+            return String(localized: "U/hr")
+        case .gramPerUnit:
+            return String(localized: "g/U")
+        case .mmolL:
+            return "mmol/L"
+        case .mgdL:
+            return "mg/dL"
+        }
+    }
 }
 
 #Preview {
@@ -164,9 +197,9 @@ struct TimeValuePickerRow: View {
         TherapySettingItem(time: 1800, value: 1.2)
     ]
 
-    TimeValueEditorView(
+    TherapySettingEditorView(
         items: $previewItems,
-        unit: "U/h",
+        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) }
     )