polscm32 1 tahun lalu
induk
melakukan
d10b439ad1

+ 15 - 7
Trio.xcodeproj/project.pbxproj

@@ -316,7 +316,7 @@
 		BD47FD132D88AA700043966B /* OnboardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FD122D88AA6B0043966B /* OnboardingManager.swift */; };
 		BD47FD172D88AAF50043966B /* OnboardingStepViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FD162D88AAEF0043966B /* OnboardingStepViews.swift */; };
 		BD47FD192D88AAFE0043966B /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FD182D88AAF90043966B /* OnboardingView.swift */; };
-		BD47FD1B2D88AB4F0043966B /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FD1A2D88AB4A0043966B /* Model.swift */; };
+		BD47FD1B2D88AB4F0043966B /* OnboardingStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FD1A2D88AB4A0043966B /* OnboardingStateModel.swift */; };
 		BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FDD62D8B64CC0043966B /* CarbRatioStepView.swift */; };
 		BD47FDD92D8B657D0043966B /* InsulinSensitivityStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FDD82D8B65730043966B /* InsulinSensitivityStepView.swift */; };
 		BD47FDDB2D8B659B0043966B /* BasalProfileStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FDDA2D8B65960043966B /* BasalProfileStepView.swift */; };
@@ -344,6 +344,8 @@
 		BD8207C42D2B42E60023339D /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D182B14D91600E76752 /* WidgetKit.framework */; };
 		BD8207C52D2B42E60023339D /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */; };
 		BD8207CE2D2B42E70023339D /* Trio Watch Complication Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		BD8E6B212D9036CA00ABF8FA /* OnboardingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8E6B202D9036CA00ABF8FA /* OnboardingProvider.swift */; };
+		BD8E6B232D9036F700ABF8FA /* OnboardingDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8E6B222D9036F700ABF8FA /* OnboardingDataFlow.swift */; };
 		BD8FC0542D66186000B95AED /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0532D66186000B95AED /* TestError.swift */; };
 		BD8FC0572D66188700B95AED /* PumpHistoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0562D66188700B95AED /* PumpHistoryStorageTests.swift */; };
 		BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0582D66189700B95AED /* TestAssembly.swift */; };
@@ -1048,7 +1050,7 @@
 		BD47FD122D88AA6B0043966B /* OnboardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManager.swift; sourceTree = "<group>"; };
 		BD47FD162D88AAEF0043966B /* OnboardingStepViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStepViews.swift; sourceTree = "<group>"; };
 		BD47FD182D88AAF90043966B /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
-		BD47FD1A2D88AB4A0043966B /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = "<group>"; };
+		BD47FD1A2D88AB4A0043966B /* OnboardingStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStateModel.swift; sourceTree = "<group>"; };
 		BD47FDD62D8B64CC0043966B /* CarbRatioStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbRatioStepView.swift; sourceTree = "<group>"; };
 		BD47FDD82D8B65730043966B /* InsulinSensitivityStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinSensitivityStepView.swift; sourceTree = "<group>"; };
 		BD47FDDA2D8B65960043966B /* BasalProfileStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalProfileStepView.swift; sourceTree = "<group>"; };
@@ -1072,6 +1074,8 @@
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
 		BD7DB88D2D2C4A0A003D3155 /* BolusCalculationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculationManager.swift; sourceTree = "<group>"; };
 		BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Trio Watch Complication Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+		BD8E6B202D9036CA00ABF8FA /* OnboardingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProvider.swift; sourceTree = "<group>"; };
+		BD8E6B222D9036F700ABF8FA /* OnboardingDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingDataFlow.swift; sourceTree = "<group>"; };
 		BD8FC0532D66186000B95AED /* TestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestError.swift; sourceTree = "<group>"; };
 		BD8FC0562D66188700B95AED /* PumpHistoryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC0582D66189700B95AED /* TestAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAssembly.swift; sourceTree = "<group>"; };
@@ -1760,9 +1764,6 @@
 		3811DE1F25C9D48300A708ED /* View */ = {
 			isa = PBXGroup;
 			children = (
-				BD47FDD52D8B64AE0043966B /* OnboardingSteps */,
-				BD47FD182D88AAF90043966B /* OnboardingView.swift */,
-				BD47FD162D88AAEF0043966B /* OnboardingStepViews.swift */,
 				3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */,
 				3811DE2025C9D48300A708ED /* MainRootView.swift */,
 			);
@@ -2592,7 +2593,9 @@
 		BD47FD142D88AACC0043966B /* Onboarding */ = {
 			isa = PBXGroup;
 			children = (
-				BD47FD1A2D88AB4A0043966B /* Model.swift */,
+				BD8E6B222D9036F700ABF8FA /* OnboardingDataFlow.swift */,
+				BD8E6B202D9036CA00ABF8FA /* OnboardingProvider.swift */,
+				BD47FD1A2D88AB4A0043966B /* OnboardingStateModel.swift */,
 				BD47FD152D88AAD80043966B /* View */,
 			);
 			path = Onboarding;
@@ -2601,6 +2604,8 @@
 		BD47FD152D88AAD80043966B /* View */ = {
 			isa = PBXGroup;
 			children = (
+				BD47FD182D88AAF90043966B /* OnboardingView.swift */,
+				BD47FDD52D8B64AE0043966B /* OnboardingSteps */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2608,6 +2613,7 @@
 		BD47FDD52D8B64AE0043966B /* OnboardingSteps */ = {
 			isa = PBXGroup;
 			children = (
+				BD47FD162D88AAEF0043966B /* OnboardingStepViews.swift */,
 				BD47FDDC2D8B65AD0043966B /* GlucoseTargetStepView.swift */,
 				BD47FDDA2D8B65960043966B /* BasalProfileStepView.swift */,
 				BD47FDD82D8B65730043966B /* InsulinSensitivityStepView.swift */,
@@ -3834,6 +3840,7 @@
 				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
 				DD1745322C55AE6000211FAC /* TargetBehavoirStateModel.swift in Sources */,
 				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
+				BD8E6B232D9036F700ABF8FA /* OnboardingDataFlow.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
@@ -3919,7 +3926,7 @@
 				582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
 				DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
-				BD47FD1B2D88AB4F0043966B /* Model.swift in Sources */,
+				BD47FD1B2D88AB4F0043966B /* OnboardingStateModel.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
 				DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */,
@@ -4043,6 +4050,7 @@
 				19D466AA29AA3099004D5F33 /* MealSettingsRootView.swift in Sources */,
 				CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */,
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
+				BD8E6B212D9036CA00ABF8FA /* OnboardingProvider.swift in Sources */,
 				DD1745502C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift in Sources */,
 				581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */,
 				BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */,

+ 1 - 1
Trio/Sources/Application/TrioApp.swift

@@ -187,7 +187,7 @@ extension Notification.Name {
                     }
             } else if onboardingManager.shouldShowOnboarding {
                 // Show onboarding if needed
-                OnboardingView(manager: onboardingManager)
+                Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager)
                     .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
                     .transition(.opacity)
             } else {

+ 5 - 0
Trio/Sources/Modules/Onboarding/OnboardingDataFlow.swift

@@ -0,0 +1,5 @@
+enum Onboarding {
+    enum Config {}
+}
+
+protocol OnboardingProvider: Provider {}

+ 5 - 0
Trio/Sources/Modules/Onboarding/OnboardingProvider.swift

@@ -0,0 +1,5 @@
+import Combine
+
+extension Onboarding {
+    final class Provider: BaseProvider, MainProvider {}
+}

+ 84 - 80
Trio/Sources/Modules/Onboarding/Model.swift

@@ -104,106 +104,110 @@ enum OnboardingStep: Int, CaseIterable, Identifiable {
 }
 
 /// Model that holds the data collected during onboarding.
-@Observable class OnboardingData: Injectable {
-    @ObservationIgnored @Injected() var settingsManager: SettingsManager!
-    @ObservationIgnored @Injected() var storage: FileStorage!
-    @ObservationIgnored @Injected() var deviceManager: DeviceDataManager!
-
-    // Carb Ratio related
-    var carbRatioItems: [CarbRatioEditor.Item] = []
-    var initialCarbRatioItems: [CarbRatioEditor.Item] = []
-    let carbRatioTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
-    let carbRatioRateValues = stride(from: 30.0, to: 501.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
-
-    // Basal Profile related
-    var initialBasalProfileItems: [BasalProfileEditor.Item] = []
-    var basalProfileItems: [BasalProfileEditor.Item] = []
-    let basalProfileTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
-    var basalProfileRateValues: [Decimal] = stride(from: 0.05, to: 3.05, by: 0.05).map { Decimal($0) }
-
-    // ISF related
-    var isfItems: [ISFEditor.Item] = []
-    var initialISFItems: [ISFEditor.Item] = []
-    let isfTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
-    var rateValues: [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 }
-        }
+extension Onboarding {
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var settingsManager: SettingsManager!
+        @ObservationIgnored @Injected() var storage: FileStorage!
+        @ObservationIgnored @Injected() var deviceManager: DeviceDataManager!
+
+        // Carb Ratio related
+        var carbRatioItems: [CarbRatioEditor.Item] = []
+        var initialCarbRatioItems: [CarbRatioEditor.Item] = []
+        let carbRatioTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+        let carbRatioRateValues = stride(from: 30.0, to: 501.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
+
+        // Basal Profile related
+        var initialBasalProfileItems: [BasalProfileEditor.Item] = []
+        var basalProfileItems: [BasalProfileEditor.Item] = []
+        let basalProfileTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+        var basalProfileRateValues: [Decimal] = stride(from: 0.05, to: 3.05, by: 0.05).map { Decimal($0) }
+
+        // ISF related
+        var isfItems: [ISFEditor.Item] = []
+        var initialISFItems: [ISFEditor.Item] = []
+        let isfTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+        var rateValues: [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
-    }
+            return values
+        }
 
-    // Target related
-    var targetItems: [TargetsEditor.Item] = []
-    var initialTargetItems: [TargetsEditor.Item] = []
-    let targetTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+        // Target related
+        var targetItems: [TargetsEditor.Item] = []
+        var initialTargetItems: [TargetsEditor.Item] = []
+        let targetTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
-    var targetRateValues: [Decimal] {
-        let settingsProvider = PickerSettingsProvider.shared
-        let glucoseSetting = PickerSetting(value: 0, step: 1, min: 72, max: 180, type: .glucose)
-        return settingsProvider.generatePickerValues(from: glucoseSetting, units: units)
-    }
+        var targetRateValues: [Decimal] {
+            let settingsProvider = PickerSettingsProvider.shared
+            let glucoseSetting = PickerSetting(value: 0, step: 1, min: 72, max: 180, type: .glucose)
+            return settingsProvider.generatePickerValues(from: glucoseSetting, units: units)
+        }
 
-    // Glucose Target
-    var targetLow: Decimal = 70
-    var targetHigh: Decimal = 180
+        // Glucose Target
+        var targetLow: Decimal = 70
+        var targetHigh: Decimal = 180
 
-    // Basal Profile
-    var basalRates: [BasalRateEntry] = [BasalRateEntry(startTime: 0, rate: 1.0)]
+        // Basal Profile
+        var basalRates: [BasalRateEntry] = [BasalRateEntry(startTime: 0, rate: 1.0)]
 
-    // Carb Ratio
-    var carbRatio: Decimal = 10
+        // Carb Ratio
+        var carbRatio: Decimal = 10
 
-    // Insulin Sensitivity Factor
-    var isf: Decimal = 40
+        // Insulin Sensitivity Factor
+        var isf: Decimal = 40
 
-    // Blood Glucose Units
-    var units: GlucoseUnits = .mgdL
+        // Blood Glucose Units
+        var units: GlucoseUnits = .mgdL
 
-    struct BasalRateEntry: Identifiable {
-        var id = UUID()
-        var startTime: Int // Minutes from midnight
-        var rate: Decimal
+        struct BasalRateEntry: Identifiable {
+            var id = UUID()
+            var startTime: Int // Minutes from midnight
+            var rate: Decimal
 
-        var timeFormatted: String {
-            let hours = startTime / 60
-            let minutes = startTime % 60
-            return String(format: "%02d:%02d", hours, minutes)
+            var timeFormatted: String {
+                let hours = startTime / 60
+                let minutes = startTime % 60
+                return String(format: "%02d:%02d", hours, minutes)
+            }
         }
-    }
 
-    /// Applies the onboarding data to the app's settings.
-    func applyToSettings() {
-        // Make a copy of the current settings that we can mutate
-        var settingsCopy = settingsManager.settings
+        override func subscribe() {}
 
-        // Apply glucose target - We'll use lowGlucose and highGlucose properties
-        settingsCopy.lowGlucose = targetLow
-        settingsCopy.highGlucose = targetHigh
+        /// Applies the onboarding data to the app's settings.
+        func applyToSettings() {
+            // Make a copy of the current settings that we can mutate
+            var settingsCopy = settingsManager.settings
 
-        // Apply glucose units
-        settingsCopy.units = units
+            // Apply glucose target - We'll use lowGlucose and highGlucose properties
+            settingsCopy.lowGlucose = targetLow
+            settingsCopy.highGlucose = targetHigh
 
-        // Apply basal profile
-        // TODO: - should we use the return value or modify the function to not return anything?
-        _ = saveBasalProfile()
+            // Apply glucose units
+            settingsCopy.units = units
 
-        // Apply carb ratio
-        saveCarbRatios()
+            // Apply basal profile
+            // TODO: - should we use the return value or modify the function to not return anything?
+            _ = saveBasalProfile()
 
-        // Apply ISF values
+            // Apply carb ratio
+            saveCarbRatios()
 
-        // Instead of using updateSettings which doesn't exist,
-        // we'll directly set the settings property which will trigger the didSet observer
-        settingsManager.settings = settingsCopy
+            // Apply ISF values
+
+            // Instead of using updateSettings which doesn't exist,
+            // we'll directly set the settings property which will trigger the didSet observer
+            settingsManager.settings = settingsCopy
+        }
     }
 }
 
 // MARK: - Setup Carb Ratios
 
-extension OnboardingData {
+extension Onboarding.StateModel {
     var carbRatiosHaveChanges: Bool {
         if initialCarbRatioItems.count != carbRatioItems.count {
             return true
@@ -266,7 +270,7 @@ extension OnboardingData {
 
 // MARK: - Setup glucose targets
 
-extension OnboardingData {
+extension Onboarding.StateModel {
     var targetsHaveChanged: Bool {
         initialTargetItems != targetItems
     }
@@ -328,7 +332,7 @@ extension OnboardingData {
 
 // MARK: - Setup ISF values
 
-extension OnboardingData {
+extension Onboarding.StateModel {
     var isfValuesHaveChanges: Bool {
         initialISFItems != isfItems
     }
@@ -390,7 +394,7 @@ extension OnboardingData {
 
 // MARK: - Setup Basal Profile
 
-extension OnboardingData {
+extension Onboarding.StateModel {
     var hasBasalProfileChanges: Bool {
         if initialBasalProfileItems.count != basalProfileItems.count {
             return true

+ 43 - 43
Trio/Sources/Modules/Main/View/OnboardingSteps/BasalProfileStepView.swift

@@ -10,7 +10,7 @@ import UIKit
 
 /// Basal profile step view for setting basal insulin rates.
 struct BasalProfileStepView: View {
-    @State var onboardingData: OnboardingData
+    @Bindable var state: Onboarding.StateModel
     @State private var showTimeSelector = false
     @State private var selectedBasalIndex: Int?
     @State private var showAlert = false
@@ -44,7 +44,7 @@ struct BasalProfileStepView: View {
                     .padding(.horizontal)
 
                 // Chart visualization
-                if !onboardingData.basalProfileItems.isEmpty {
+                if !state.basalProfileItems.isEmpty {
                     VStack(alignment: .leading) {
                         Text("Basal Profile")
                             .font(.headline)
@@ -68,7 +68,7 @@ struct BasalProfileStepView: View {
                         Spacer()
 
                         // Add new basal rate button
-                        if onboardingData.basalProfileItems.count < 24 {
+                        if state.basalProfileItems.count < 24 {
                             Button(action: {
                                 showTimeSelector = true
                             }) {
@@ -85,13 +85,13 @@ struct BasalProfileStepView: View {
 
                     // List of basal rates
                     VStack(spacing: 2) {
-                        ForEach(Array(onboardingData.basalProfileItems.enumerated()), id: \.element.id) { index, item in
+                        ForEach(Array(state.basalProfileItems.enumerated()), id: \.element.id) { index, item in
                             HStack {
                                 // Time display
                                 Text(
                                     dateFormatter
                                         .string(from: Date(
-                                            timeIntervalSince1970: onboardingData
+                                            timeIntervalSince1970: state
                                                 .basalProfileTimeValues[item.timeIndex]
                                         ))
                                 )
@@ -102,41 +102,41 @@ struct BasalProfileStepView: View {
                                 Slider(
                                     value: Binding(
                                         get: {
-                                            guard !onboardingData.basalProfileRateValues.isEmpty,
-                                                  item.rateIndex < onboardingData.basalProfileRateValues.count
+                                            guard !state.basalProfileRateValues.isEmpty,
+                                                  item.rateIndex < state.basalProfileRateValues.count
                                             else {
                                                 return 0.0
                                             }
                                             return Double(
-                                                truncating: onboardingData
+                                                truncating: state
                                                     .basalProfileRateValues[item.rateIndex] as NSNumber
                                             )
                                         },
                                         set: { newValue in
-                                            guard !onboardingData.basalProfileRateValues.isEmpty else { return }
+                                            guard !state.basalProfileRateValues.isEmpty else { return }
 
                                             // Find closest match in rateValues array
-                                            let newIndex = onboardingData.basalProfileRateValues
+                                            let newIndex = state.basalProfileRateValues
                                                 .firstIndex { abs(Double($0) - newValue) < 0.005 } ?? item.rateIndex
 
                                             // Ensure index is valid before updating
-                                            if newIndex < onboardingData.basalProfileRateValues.count,
-                                               index < onboardingData.basalProfileItems.count
+                                            if newIndex < state.basalProfileRateValues.count,
+                                               index < state.basalProfileItems.count
                                             {
-                                                onboardingData.basalProfileItems[index].rateIndex = newIndex
+                                                state.basalProfileItems[index].rateIndex = newIndex
                                                 // Force refresh when slider changes
                                                 refreshUI = UUID()
                                             }
                                         }
                                     ),
-                                    in: onboardingData.basalProfileRateValues.isEmpty ? 0 ... 1 :
-                                        Double(truncating: onboardingData.basalProfileRateValues.first! as NSNumber) ...
-                                        Double(truncating: onboardingData.basalProfileRateValues.last! as NSNumber),
+                                    in: state.basalProfileRateValues.isEmpty ? 0 ... 1 :
+                                        Double(truncating: state.basalProfileRateValues.first! as NSNumber) ...
+                                        Double(truncating: state.basalProfileRateValues.last! as NSNumber),
                                     step: 0.05
                                 )
                                 .accentColor(.purple)
                                 .padding(.horizontal, 5)
-                                .onChange(of: onboardingData.basalProfileItems[index].rateIndex) { _, _ in
+                                .onChange(of: state.basalProfileItems[index].rateIndex) { _, _ in
                                     // Trigger immediate UI update when slider value changes
                                     let impact = UIImpactFeedbackGenerator(style: .light)
                                     impact.impactOccurred()
@@ -144,7 +144,7 @@ struct BasalProfileStepView: View {
 
                                 // Display the current value
                                 Text(
-                                    "\(onboardingData.basalProfileRateValues.isEmpty || item.rateIndex >= onboardingData.basalProfileRateValues.count ? "--" : formatter.string(from: onboardingData.basalProfileRateValues[item.rateIndex] as NSNumber) ?? "--") U/h"
+                                    "\(state.basalProfileRateValues.isEmpty || item.rateIndex >= state.basalProfileRateValues.count ? "--" : formatter.string(from: state.basalProfileRateValues[item.rateIndex] as NSNumber) ?? "--") U/h"
                                 )
                                 .frame(width: 80, alignment: .trailing)
                                 .lineLimit(1)
@@ -153,7 +153,7 @@ struct BasalProfileStepView: View {
                                 // Delete button (not for the first entry at 00:00)
                                 if index > 0 {
                                     Button(action: {
-                                        onboardingData.basalProfileItems.remove(at: index)
+                                        state.basalProfileItems.remove(at: index)
                                     }) {
                                         Image(systemName: "trash")
                                             .foregroundColor(.red)
@@ -174,14 +174,14 @@ struct BasalProfileStepView: View {
                     .cornerRadius(10)
                     .padding(.horizontal)
                     .onAppear {
-                        if onboardingData.basalProfileItems.isEmpty {
+                        if state.basalProfileItems.isEmpty {
                             addBasalRate()
                         }
                     }
                 }
 
                 // Total daily basal calculation
-                if !onboardingData.basalProfileItems.isEmpty {
+                if !state.basalProfileItems.isEmpty {
                     VStack(alignment: .leading, spacing: 8) {
                         HStack {
                             Text("Total Daily Basal")
@@ -225,19 +225,19 @@ struct BasalProfileStepView: View {
             for hour in 0 ..< 24 {
                 let hourInMinutes = hour * 60
                 // Calculate timeIndex for this hour
-                let timeIndex = onboardingData.basalProfileTimeValues
+                let timeIndex = state.basalProfileTimeValues
                     .firstIndex { abs($0 - Double(hourInMinutes * 60)) < 10 } ?? 0
 
                 // Check if this hour is already in the profile
-                if !onboardingData.basalProfileItems.contains(where: { $0.timeIndex == timeIndex }) {
+                if !state.basalProfileItems.contains(where: { $0.timeIndex == timeIndex }) {
                     buttons.append(.default(Text("\(String(format: "%02d:00", hour))")) {
                         // Get the current rate from the last item
-                        let rateIndex = onboardingData.basalProfileItems.last?.rateIndex ?? 20 // 1.0 U/h as default
+                        let rateIndex = state.basalProfileItems.last?.rateIndex ?? 20 // 1.0 U/h as default
                         // Create new item with the specified time
                         let newItem = BasalProfileEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
                         // Add the new item and sort the list
-                        onboardingData.basalProfileItems.append(newItem)
-                        onboardingData.basalProfileItems.sort(by: { $0.timeIndex < $1.timeIndex })
+                        state.basalProfileItems.append(newItem)
+                        state.basalProfileItems.sort(by: { $0.timeIndex < $1.timeIndex })
                     })
                 }
             }
@@ -262,22 +262,22 @@ struct BasalProfileStepView: View {
     // Add initial basal rate
     private func addBasalRate() {
         // Default to midnight (00:00) and 1.0 U/h rate
-        let timeIndex = onboardingData.basalProfileTimeValues.firstIndex { abs($0 - 0) < 1 } ?? 0
-        let rateIndex = onboardingData.basalProfileRateValues.firstIndex { abs(Double($0) - 1.0) < 0.05 } ?? 20
+        let timeIndex = state.basalProfileTimeValues.firstIndex { abs($0 - 0) < 1 } ?? 0
+        let rateIndex = state.basalProfileRateValues.firstIndex { abs(Double($0) - 1.0) < 0.05 } ?? 20
 
         let newItem = BasalProfileEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
-        onboardingData.basalProfileItems.append(newItem)
+        state.basalProfileItems.append(newItem)
     }
 
     // Computed property to check if we can add more basal rates
     private var canAddBasalRate: Bool {
-        guard let lastItem = onboardingData.basalProfileItems.last else { return true }
-        return lastItem.timeIndex < onboardingData.basalProfileTimeValues.count - 1
+        guard let lastItem = state.basalProfileItems.last else { return true }
+        return lastItem.timeIndex < state.basalProfileTimeValues.count - 1
     }
 
     // Calculate the total daily basal insulin
     private func calculateTotalDailyBasal() -> Double {
-        let items = onboardingData.basalProfileItems
+        let items = state.basalProfileItems
 
         // If there are no items, return 0
         if items.isEmpty {
@@ -289,14 +289,14 @@ struct BasalProfileStepView: View {
         // Safely create profile items with proper error checking
         let profileItems = items.compactMap { item -> (timeIndex: Int, rate: Decimal)? in
             // Safety check - make sure indices are within bounds
-            guard item.timeIndex >= 0 && item.timeIndex < onboardingData.basalProfileTimeValues.count,
-                  item.rateIndex >= 0 && item.rateIndex < onboardingData.basalProfileRateValues.count
+            guard item.timeIndex >= 0 && item.timeIndex < state.basalProfileTimeValues.count,
+                  item.rateIndex >= 0 && item.rateIndex < state.basalProfileRateValues.count
             else {
                 return nil
             }
 
-            let timeValue = onboardingData.basalProfileTimeValues[item.timeIndex]
-            let rate = onboardingData.basalProfileRateValues[item.rateIndex]
+            let timeValue = state.basalProfileTimeValues[item.timeIndex]
+            let rate = state.basalProfileRateValues[item.rateIndex]
             return (Int(timeValue / 60), rate)
         }.sorted(by: { $0.timeIndex < $1.timeIndex })
 
@@ -332,19 +332,19 @@ struct BasalProfileStepView: View {
     // Chart for visualizing basal profile
     private var basalProfileChart: some View {
         Chart {
-            ForEach(Array(onboardingData.basalProfileItems.enumerated()), id: \.element.id) { index, item in
-                let displayValue = onboardingData.basalProfileRateValues[item.rateIndex]
+            ForEach(Array(state.basalProfileItems.enumerated()), id: \.element.id) { index, item in
+                let displayValue = state.basalProfileRateValues[item.rateIndex]
 
                 let tzOffset = TimeZone.current.secondsFromGMT() * -1
-                let startDate = Date(timeIntervalSinceReferenceDate: onboardingData.basalProfileTimeValues[item.timeIndex])
+                let startDate = Date(timeIntervalSinceReferenceDate: state.basalProfileTimeValues[item.timeIndex])
                     .addingTimeInterval(TimeInterval(tzOffset))
-                let endDate = onboardingData.basalProfileItems.count > index + 1 ?
+                let endDate = state.basalProfileItems.count > index + 1 ?
                     Date(
-                        timeIntervalSinceReferenceDate: onboardingData
-                            .basalProfileTimeValues[onboardingData.basalProfileItems[index + 1].timeIndex]
+                        timeIntervalSinceReferenceDate: state
+                            .basalProfileTimeValues[state.basalProfileItems[index + 1].timeIndex]
                     )
                     .addingTimeInterval(TimeInterval(tzOffset)) :
-                    Date(timeIntervalSinceReferenceDate: onboardingData.basalProfileTimeValues.last!).addingTimeInterval(30 * 60)
+                    Date(timeIntervalSinceReferenceDate: state.basalProfileTimeValues.last!).addingTimeInterval(30 * 60)
                     .addingTimeInterval(TimeInterval(tzOffset))
 
                 RectangleMark(

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

@@ -10,7 +10,7 @@ import UIKit
 
 /// Carb ratio step view for setting insulin-to-carb ratio.
 struct CarbRatioStepView: View {
-    @State var onboardingData: OnboardingData
+    @Bindable var state: Onboarding.StateModel
     @State private var showTimeSelector = false
     @State private var selectedRatioIndex: Int?
     @State private var refreshUI = UUID() // to update chart when slider value changes
@@ -42,7 +42,7 @@ struct CarbRatioStepView: View {
                     .padding(.horizontal)
 
                 // Chart visualization
-                if !onboardingData.carbRatioItems.isEmpty {
+                if !state.carbRatioItems.isEmpty {
                     VStack(alignment: .leading) {
                         Text("Carb Ratio Profile")
                             .font(.headline)
@@ -66,7 +66,7 @@ struct CarbRatioStepView: View {
                         Spacer()
 
                         // Add new carb ratio button
-                        if onboardingData.carbRatioItems.count < 24 {
+                        if state.carbRatioItems.count < 24 {
                             Button(action: {
                                 showTimeSelector = true
                             }) {
@@ -83,13 +83,13 @@ struct CarbRatioStepView: View {
 
                     // List of carb ratios
                     VStack(spacing: 2) {
-                        ForEach(Array(onboardingData.carbRatioItems.enumerated()), id: \.element.id) { index, item in
+                        ForEach(Array(state.carbRatioItems.enumerated()), id: \.element.id) { index, item in
                             HStack {
                                 // Time display
                                 Text(
                                     dateFormatter
                                         .string(from: Date(
-                                            timeIntervalSince1970: onboardingData
+                                            timeIntervalSince1970: state
                                                 .carbRatioTimeValues[item.timeIndex]
                                         ))
                                 )
@@ -101,32 +101,32 @@ struct CarbRatioStepView: View {
                                     value: Binding(
                                         get: {
                                             Double(
-                                                truncating: onboardingData
+                                                truncating: state
                                                     .carbRatioRateValues[item.rateIndex] as NSNumber
                                             ) },
                                         set: { newValue in
                                             // Find closest match in rateValues array
-                                            let newIndex = onboardingData.carbRatioRateValues
+                                            let newIndex = state.carbRatioRateValues
                                                 .firstIndex { abs(Double($0) - newValue) < 0.05 } ?? item.rateIndex
-                                            onboardingData.carbRatioItems[index].rateIndex = newIndex
+                                            state.carbRatioItems[index].rateIndex = newIndex
                                             // Force refresh when slider changes
                                             refreshUI = UUID()
                                         }
                                     ),
-                                    in: Double(truncating: onboardingData.carbRatioRateValues.first! as NSNumber) ...
-                                        Double(truncating: onboardingData.carbRatioRateValues.last! as NSNumber),
+                                    in: Double(truncating: state.carbRatioRateValues.first! as NSNumber) ...
+                                        Double(truncating: state.carbRatioRateValues.last! as NSNumber),
                                     step: 0.5
                                 )
                                 .accentColor(.orange)
                                 .padding(.horizontal, 5)
-                                .onChange(of: onboardingData.carbRatioItems[index].rateIndex) { _, _ in
+                                .onChange(of: state.carbRatioItems[index].rateIndex) { _, _ in
                                     let impact = UIImpactFeedbackGenerator(style: .light)
                                     impact.impactOccurred()
                                 }
 
                                 // Display the current value
                                 Text(
-                                    "\(formatter.string(from: onboardingData.carbRatioRateValues[item.rateIndex] as NSNumber) ?? "--") g/U"
+                                    "\(formatter.string(from: state.carbRatioRateValues[item.rateIndex] as NSNumber) ?? "--") g/U"
                                 )
                                 .frame(width: 80, alignment: .trailing)
                                 .lineLimit(1)
@@ -135,7 +135,7 @@ struct CarbRatioStepView: View {
                                 // Delete button (not for the first entry at 00:00)
                                 if index > 0 {
                                     Button(action: {
-                                        onboardingData.carbRatioItems.remove(at: index)
+                                        state.carbRatioItems.remove(at: index)
                                     }) {
                                         Image(systemName: "trash")
                                             .foregroundColor(.red)
@@ -156,14 +156,14 @@ struct CarbRatioStepView: View {
                     .cornerRadius(10)
                     .padding(.horizontal)
                     .onAppear {
-                        if onboardingData.carbRatioItems.isEmpty {
-                            onboardingData.addCarbRatio()
+                        if state.carbRatioItems.isEmpty {
+                            state.addCarbRatio()
                         }
                     }
                 }
 
                 // Example calculation based on first carb ratio
-                if !onboardingData.carbRatioItems.isEmpty {
+                if !state.carbRatioItems.isEmpty {
                     Divider()
                         .padding(.horizontal)
 
@@ -179,11 +179,11 @@ struct CarbRatioStepView: View {
 
                             let insulinNeeded = 45 /
                                 Double(
-                                    truncating: onboardingData
-                                        .carbRatioRateValues[onboardingData.carbRatioItems.first!.rateIndex] as NSNumber
+                                    truncating: state
+                                        .carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber
                                 )
                             Text(
-                                "45g ÷ \(formatter.string(from: onboardingData.carbRatioRateValues[onboardingData.carbRatioItems.first!.rateIndex] as NSNumber) ?? "--") = \(String(format: "%.1f", insulinNeeded)) units of insulin"
+                                "45g ÷ \(formatter.string(from: state.carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber) ?? "--") = \(String(format: "%.1f", insulinNeeded)) units of insulin"
                             )
                             .font(.system(.body, design: .monospaced))
                             .foregroundColor(.orange)
@@ -224,18 +224,18 @@ struct CarbRatioStepView: View {
             for hour in 0 ..< 24 {
                 let hourInMinutes = hour * 60
                 // Calculate timeIndex for this hour
-                let timeIndex = onboardingData.carbRatioTimeValues.firstIndex { abs($0 - Double(hourInMinutes * 60)) < 10 } ?? 0
+                let timeIndex = state.carbRatioTimeValues.firstIndex { abs($0 - Double(hourInMinutes * 60)) < 10 } ?? 0
 
                 // Check if this hour is already in the profile
-                if !onboardingData.carbRatioItems.contains(where: { $0.timeIndex == timeIndex }) {
+                if !state.carbRatioItems.contains(where: { $0.timeIndex == timeIndex }) {
                     buttons.append(.default(Text("\(String(format: "%02d:00", hour))")) {
                         // Get the current ratio from the last item
-                        let rateIndex = onboardingData.carbRatioItems.last?.rateIndex ?? 0
+                        let rateIndex = state.carbRatioItems.last?.rateIndex ?? 0
                         // Create new item with the specified time
                         let newItem = CarbRatioEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
                         // Add the new item and sort the list
-                        onboardingData.carbRatioItems.append(newItem)
-                        onboardingData.carbRatioItems.sort(by: { $0.timeIndex < $1.timeIndex })
+                        state.carbRatioItems.append(newItem)
+                        state.carbRatioItems.sort(by: { $0.timeIndex < $1.timeIndex })
                     })
                 }
             }
@@ -252,26 +252,26 @@ struct CarbRatioStepView: View {
 
     // Computed property to check if we can add more carb ratios
     private var canAddRatio: Bool {
-        guard let lastItem = onboardingData.carbRatioItems.last else { return true }
-        return lastItem.timeIndex < onboardingData.carbRatioTimeValues.count - 1
+        guard let lastItem = state.carbRatioItems.last else { return true }
+        return lastItem.timeIndex < state.carbRatioTimeValues.count - 1
     }
 
     // Chart for visualizing carb ratios
     private var carbRatioChart: some View {
         Chart {
-            ForEach(Array(onboardingData.carbRatioItems.enumerated()), id: \.element.id) { index, item in
-                let displayValue = onboardingData.carbRatioRateValues[item.rateIndex]
+            ForEach(Array(state.carbRatioItems.enumerated()), id: \.element.id) { index, item in
+                let displayValue = state.carbRatioRateValues[item.rateIndex]
 
                 let tzOffset = TimeZone.current.secondsFromGMT() * -1
-                let startDate = Date(timeIntervalSinceReferenceDate: onboardingData.carbRatioTimeValues[item.timeIndex])
+                let startDate = Date(timeIntervalSinceReferenceDate: state.carbRatioTimeValues[item.timeIndex])
                     .addingTimeInterval(TimeInterval(tzOffset))
-                let endDate = onboardingData.carbRatioItems.count > index + 1 ?
+                let endDate = state.carbRatioItems.count > index + 1 ?
                     Date(
-                        timeIntervalSinceReferenceDate: onboardingData
-                            .carbRatioTimeValues[onboardingData.carbRatioItems[index + 1].timeIndex]
+                        timeIntervalSinceReferenceDate: state
+                            .carbRatioTimeValues[state.carbRatioItems[index + 1].timeIndex]
                     )
                     .addingTimeInterval(TimeInterval(tzOffset)) :
-                    Date(timeIntervalSinceReferenceDate: onboardingData.carbRatioTimeValues.last!).addingTimeInterval(30 * 60)
+                    Date(timeIntervalSinceReferenceDate: state.carbRatioTimeValues.last!).addingTimeInterval(30 * 60)
                     .addingTimeInterval(TimeInterval(tzOffset))
 
                 RectangleMark(

+ 60 - 60
Trio/Sources/Modules/Main/View/OnboardingSteps/GlucoseTargetStepView.swift

@@ -10,7 +10,7 @@ import UIKit
 
 /// Glucose target step view for setting target glucose range.
 struct GlucoseTargetStepView: View {
-    @State var onboardingData: OnboardingData
+    @Bindable var state: Onboarding.StateModel
     @State private var showUnitPicker = false
     @State private var showTimeSelector = false
     @State private var selectedTargetIndex: Int?
@@ -22,7 +22,7 @@ struct GlucoseTargetStepView: View {
     private var numberFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = onboardingData.units == .mmolL ? 1 : 0
+        formatter.maximumFractionDigits = state.units == .mmolL ? 1 : 0
         return formatter
     }
 
@@ -51,7 +51,7 @@ struct GlucoseTargetStepView: View {
                         showUnitPicker.toggle()
                     }) {
                         HStack {
-                            Text(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")
+                            Text(state.units == .mgdL ? "mg/dL" : "mmol/L")
                             Image(systemName: "chevron.down")
                         }
                         .padding(.horizontal, 12)
@@ -62,27 +62,27 @@ struct GlucoseTargetStepView: View {
                     .actionSheet(isPresented: $showUnitPicker) {
                         let mgdlAction = ActionSheet.Button.default(Text("mg/dL")) {
                             // Store current unit
-                            let oldUnit = onboardingData.units
+                            let oldUnit = state.units
                             // Change to new unit
-                            onboardingData.units = .mgdL
+                            state.units = .mgdL
                             // Adjust values for unit change, only if unit actually changed
                             if oldUnit != .mgdL {
-                                onboardingData.targetLow = max(70, onboardingData.targetLow * 18)
-                                onboardingData.targetHigh = max(120, onboardingData.targetHigh * 18)
-                                onboardingData.isf = max(30, onboardingData.isf * 18)
+                                state.targetLow = max(70, state.targetLow * 18)
+                                state.targetHigh = max(120, state.targetHigh * 18)
+                                state.isf = max(30, state.isf * 18)
                             }
                         }
 
                         let mmolAction = ActionSheet.Button.default(Text("mmol/L")) {
                             // Store current unit
-                            let oldUnit = onboardingData.units
+                            let oldUnit = state.units
                             // Change to new unit
-                            onboardingData.units = .mmolL
+                            state.units = .mmolL
                             // Adjust values for unit change, only if unit actually changed
                             if oldUnit != .mmolL {
-                                onboardingData.targetLow = max(3.9, onboardingData.targetLow / 18)
-                                onboardingData.targetHigh = max(6.7, onboardingData.targetHigh / 18)
-                                onboardingData.isf = max(1.7, onboardingData.isf / 18)
+                                state.targetLow = max(3.9, state.targetLow / 18)
+                                state.targetHigh = max(6.7, state.targetHigh / 18)
+                                state.isf = max(1.7, state.isf / 18)
                             }
                         }
 
@@ -114,16 +114,16 @@ struct GlucoseTargetStepView: View {
                         HStack {
                             Slider(
                                 value: Binding(
-                                    get: { Double(truncating: onboardingData.targetLow as NSNumber) },
-                                    set: { onboardingData.targetLow = Decimal($0) }
+                                    get: { Double(truncating: state.targetLow as NSNumber) },
+                                    set: { state.targetLow = Decimal($0) }
                                 ),
-                                in: onboardingData.units == .mgdL ? 70 ... 120 : 3.9 ... 6.7,
-                                step: onboardingData.units == .mgdL ? 1 : 0.1
+                                in: state.units == .mgdL ? 70 ... 120 : 3.9 ... 6.7,
+                                step: state.units == .mgdL ? 1 : 0.1
                             )
                             .accentColor(.green)
 
                             Text(
-                                "\(numberFormatter.string(from: onboardingData.targetLow as NSNumber) ?? "--") \(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")"
+                                "\(numberFormatter.string(from: state.targetLow as NSNumber) ?? "--") \(state.units == .mgdL ? "mg/dL" : "mmol/L")"
                             )
                             .frame(width: 80, alignment: .trailing)
                         }
@@ -138,18 +138,18 @@ struct GlucoseTargetStepView: View {
                         HStack {
                             Slider(
                                 value: Binding(
-                                    get: { Double(truncating: onboardingData.targetHigh as NSNumber) },
-                                    set: { onboardingData.targetHigh = Decimal($0) }
+                                    get: { Double(truncating: state.targetHigh as NSNumber) },
+                                    set: { state.targetHigh = Decimal($0) }
                                 ),
-                                in: onboardingData.units == .mgdL ?
-                                    Double(truncating: onboardingData.targetLow as NSNumber) + 10 ... 200 :
-                                    Double(truncating: onboardingData.targetLow as NSNumber) + 0.6 ... 11.1,
-                                step: onboardingData.units == .mgdL ? 1 : 0.1
+                                in: state.units == .mgdL ?
+                                    Double(truncating: state.targetLow as NSNumber) + 10 ... 200 :
+                                    Double(truncating: state.targetLow as NSNumber) + 0.6 ... 11.1,
+                                step: state.units == .mgdL ? 1 : 0.1
                             )
                             .accentColor(.green)
 
                             Text(
-                                "\(numberFormatter.string(from: onboardingData.targetHigh as NSNumber) ?? "--") \(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")"
+                                "\(numberFormatter.string(from: state.targetHigh as NSNumber) ?? "--") \(state.units == .mgdL ? "mg/dL" : "mmol/L")"
                             )
                             .frame(width: 80, alignment: .trailing)
                         }
@@ -160,7 +160,7 @@ struct GlucoseTargetStepView: View {
                 Divider()
 
                 // Chart visualization
-                if !onboardingData.targetItems.isEmpty {
+                if !state.targetItems.isEmpty {
                     VStack(alignment: .leading) {
                         Text("Glucose Targets")
                             .font(.headline)
@@ -184,7 +184,7 @@ struct GlucoseTargetStepView: View {
                         Spacer()
 
                         // Add new target button
-                        if onboardingData.targetItems.count < 24 {
+                        if state.targetItems.count < 24 {
                             Button(action: {
                                 showTimeSelector = true
                             }) {
@@ -201,14 +201,14 @@ struct GlucoseTargetStepView: View {
 
                     // List of targets
                     VStack(spacing: 2) {
-                        ForEach(onboardingData.targetItems.indices, id: \.self) { index in
-                            let item = onboardingData.targetItems[index]
+                        ForEach(state.targetItems.indices, id: \.self) { index in
+                            let item = state.targetItems[index]
                             HStack {
                                 // Time display
                                 Text(
                                     dateFormatter
                                         .string(from: Date(
-                                            timeIntervalSince1970: onboardingData
+                                            timeIntervalSince1970: state
                                                 .targetTimeValues[item.timeIndex]
                                         ))
                                 )
@@ -220,38 +220,38 @@ struct GlucoseTargetStepView: View {
                                     value: Binding(
                                         get: {
                                             Double(
-                                                truncating: onboardingData
+                                                truncating: state
                                                     .targetRateValues[item.lowIndex] as NSNumber
                                             ) },
                                         set: { newValue in
                                             // Find closest match in rateValues array
-                                            let newIndex = onboardingData.targetRateValues
+                                            let newIndex = state.targetRateValues
                                                 .firstIndex { abs(Double($0) - newValue) < 0.05 } ?? item.lowIndex
-                                            onboardingData.targetItems[index].lowIndex = newIndex
+                                            state.targetItems[index].lowIndex = newIndex
 
                                             // Ensure high target is at least as high as low target
-                                            if onboardingData.targetItems[index].highIndex < newIndex {
-                                                onboardingData.targetItems[index].highIndex = newIndex
+                                            if state.targetItems[index].highIndex < newIndex {
+                                                state.targetItems[index].highIndex = newIndex
                                             }
 
                                             // Force refresh when slider changes
                                             refreshUI = UUID()
                                         }
                                     ),
-                                    in: Double(truncating: onboardingData.targetRateValues.first! as NSNumber) ...
-                                        Double(truncating: onboardingData.targetRateValues.last! as NSNumber),
-                                    step: onboardingData.units == .mgdL ? 1 : 0.1
+                                    in: Double(truncating: state.targetRateValues.first! as NSNumber) ...
+                                        Double(truncating: state.targetRateValues.last! as NSNumber),
+                                    step: state.units == .mgdL ? 1 : 0.1
                                 )
                                 .accentColor(.blue)
                                 .padding(.horizontal, 5)
-                                .onChange(of: onboardingData.targetItems[index].lowIndex) { _, _ in
+                                .onChange(of: state.targetItems[index].lowIndex) { _, _ in
                                     let impact = UIImpactFeedbackGenerator(style: .light)
                                     impact.impactOccurred()
                                 }
 
                                 // Display the current value
                                 Text(
-                                    "\(numberFormatter.string(from: onboardingData.targetRateValues[item.lowIndex] as NSNumber) ?? "--") \(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")"
+                                    "\(numberFormatter.string(from: state.targetRateValues[item.lowIndex] as NSNumber) ?? "--") \(state.units == .mgdL ? "mg/dL" : "mmol/L")"
                                 )
                                 .frame(width: 80, alignment: .trailing)
                                 .lineLimit(1)
@@ -260,7 +260,7 @@ struct GlucoseTargetStepView: View {
                                 // Delete button (not for the first entry at 00:00)
                                 if index > 0 {
                                     Button(action: {
-                                        onboardingData.targetItems.remove(at: index)
+                                        state.targetItems.remove(at: index)
                                     }) {
                                         Image(systemName: "trash")
                                             .foregroundColor(.red)
@@ -281,8 +281,8 @@ struct GlucoseTargetStepView: View {
                     .cornerRadius(10)
                     .padding(.horizontal)
                     .onAppear {
-                        if onboardingData.targetItems.isEmpty {
-                            onboardingData.addTarget()
+                        if state.targetItems.isEmpty {
+                            state.addTarget()
                         }
                     }
                 }
@@ -327,14 +327,14 @@ struct GlucoseTargetStepView: View {
 
                     // Range values
                     HStack(spacing: 0) {
-                        Text("\(numberFormatter.string(from: onboardingData.targetLow as NSNumber) ?? "--")")
+                        Text("\(numberFormatter.string(from: state.targetLow as NSNumber) ?? "--")")
                             .font(.caption)
                             .frame(width: 50, alignment: .center)
 
                         Spacer()
                             .frame(width: 100)
 
-                        Text("\(numberFormatter.string(from: onboardingData.targetHigh as NSNumber) ?? "--")")
+                        Text("\(numberFormatter.string(from: state.targetHigh as NSNumber) ?? "--")")
                             .font(.caption)
                             .frame(width: 50, alignment: .center)
                     }
@@ -354,21 +354,21 @@ struct GlucoseTargetStepView: View {
             for hour in 0 ..< 24 {
                 let hourInMinutes = hour * 60
                 // Calculate timeIndex for this hour
-                let timeIndex = onboardingData.targetTimeValues.firstIndex { abs($0 - Double(hourInMinutes * 60)) < 10 } ?? 0
+                let timeIndex = state.targetTimeValues.firstIndex { abs($0 - Double(hourInMinutes * 60)) < 10 } ?? 0
 
                 // Check if this hour is already in the profile
-                if !onboardingData.targetItems.contains(where: { $0.timeIndex == timeIndex }) {
+                if !state.targetItems.contains(where: { $0.timeIndex == timeIndex }) {
                     buttons.append(.default(Text("\(String(format: "%02d:00", hour))")) {
                         // Get the current low and high values from the last item
-                        let lowIndex = onboardingData.targetItems.last?.lowIndex ?? 0
-                        let highIndex = onboardingData.targetItems.last?.highIndex ?? lowIndex
+                        let lowIndex = state.targetItems.last?.lowIndex ?? 0
+                        let highIndex = state.targetItems.last?.highIndex ?? lowIndex
 
                         // Create new item with the specified time
                         let newItem = TargetsEditor.Item(lowIndex: lowIndex, highIndex: highIndex, timeIndex: timeIndex)
 
                         // Add the new item and sort the list by timeIndex
-                        onboardingData.targetItems.append(newItem)
-                        onboardingData.targetItems.sort(by: { $0.timeIndex < $1.timeIndex })
+                        state.targetItems.append(newItem)
+                        state.targetItems.sort(by: { $0.timeIndex < $1.timeIndex })
                     })
                 }
             }
@@ -385,26 +385,26 @@ struct GlucoseTargetStepView: View {
 
     // Computed property to check if we can add more targets
     private var canAddTarget: Bool {
-        guard let lastItem = onboardingData.targetItems.last else { return true }
-        return lastItem.timeIndex < onboardingData.targetTimeValues.count - 1
+        guard let lastItem = state.targetItems.last else { return true }
+        return lastItem.timeIndex < state.targetTimeValues.count - 1
     }
 
     // Chart for visualizing glucose targets
     private var glucoseTargetChart: some View {
         Chart {
-            ForEach(Array(onboardingData.targetItems.enumerated()), id: \.element.id) { index, item in
-                let displayValue = onboardingData.targetRateValues[item.lowIndex]
+            ForEach(Array(state.targetItems.enumerated()), id: \.element.id) { index, item in
+                let displayValue = state.targetRateValues[item.lowIndex]
 
                 let tzOffset = TimeZone.current.secondsFromGMT() * -1
-                let startDate = Date(timeIntervalSinceReferenceDate: onboardingData.targetTimeValues[item.timeIndex])
+                let startDate = Date(timeIntervalSinceReferenceDate: state.targetTimeValues[item.timeIndex])
                     .addingTimeInterval(TimeInterval(tzOffset))
-                let endDate = onboardingData.targetItems.count > index + 1 ?
+                let endDate = state.targetItems.count > index + 1 ?
                     Date(
-                        timeIntervalSinceReferenceDate: onboardingData
-                            .targetTimeValues[onboardingData.targetItems[index + 1].timeIndex]
+                        timeIntervalSinceReferenceDate: state
+                            .targetTimeValues[state.targetItems[index + 1].timeIndex]
                     )
                     .addingTimeInterval(TimeInterval(tzOffset)) :
-                    Date(timeIntervalSinceReferenceDate: onboardingData.targetTimeValues.last!).addingTimeInterval(30 * 60)
+                    Date(timeIntervalSinceReferenceDate: state.targetTimeValues.last!).addingTimeInterval(30 * 60)
                     .addingTimeInterval(TimeInterval(tzOffset))
 
                 RectangleMark(

+ 49 - 49
Trio/Sources/Modules/Main/View/OnboardingSteps/InsulinSensitivityStepView.swift

@@ -10,7 +10,7 @@ import UIKit
 
 /// Insulin sensitivity step view for setting insulin sensitivity factor.
 struct InsulinSensitivityStepView: View {
-    @State var onboardingData: OnboardingData
+    @Bindable var state: Onboarding.StateModel
     @State private var showTimeSelector = false
     @State private var selectedISFIndex: Int?
     @State private var showAlert = false
@@ -24,7 +24,7 @@ struct InsulinSensitivityStepView: View {
     private var numberFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = onboardingData.units == .mmolL ? 1 : 0
+        formatter.maximumFractionDigits = state.units == .mmolL ? 1 : 0
         return formatter
     }
 
@@ -46,7 +46,7 @@ struct InsulinSensitivityStepView: View {
                 .padding(.horizontal)
 
                 // Chart visualization
-                if !onboardingData.isfItems.isEmpty {
+                if !state.isfItems.isEmpty {
                     VStack(alignment: .leading) {
                         Text("ISF Profile")
                             .font(.headline)
@@ -70,7 +70,7 @@ struct InsulinSensitivityStepView: View {
                         Spacer()
 
                         // Add new ISF button
-                        if onboardingData.isfItems.count < 24 {
+                        if state.isfItems.count < 24 {
                             Button(action: {
                                 showTimeSelector = true
                             }) {
@@ -87,13 +87,13 @@ struct InsulinSensitivityStepView: View {
 
                     // List of ISF values
                     VStack(spacing: 2) {
-                        ForEach(Array(onboardingData.isfItems.enumerated()), id: \.element.id) { index, item in
+                        ForEach(Array(state.isfItems.enumerated()), id: \.element.id) { index, item in
                             HStack {
                                 // Time display
                                 Text(
                                     dateFormatter
                                         .string(from: Date(
-                                            timeIntervalSince1970: onboardingData
+                                            timeIntervalSince1970: state
                                                 .isfTimeValues[item.timeIndex]
                                         ))
                                 )
@@ -104,41 +104,41 @@ struct InsulinSensitivityStepView: View {
                                 Slider(
                                     value: Binding(
                                         get: {
-                                            guard !onboardingData.rateValues.isEmpty,
-                                                  item.rateIndex < onboardingData.rateValues.count
+                                            guard !state.rateValues.isEmpty,
+                                                  item.rateIndex < state.rateValues.count
                                             else {
                                                 return 0.0
                                             }
                                             return Double(
-                                                truncating: onboardingData
+                                                truncating: state
                                                     .rateValues[item.rateIndex] as NSNumber
                                             )
                                         },
                                         set: { newValue in
-                                            guard !onboardingData.rateValues.isEmpty else { return }
+                                            guard !state.rateValues.isEmpty else { return }
 
                                             // Find closest match in rateValues array
-                                            let newIndex = onboardingData.rateValues
+                                            let newIndex = state.rateValues
                                                 .firstIndex { abs(Double($0) - newValue) < 0.5 } ?? item.rateIndex
 
                                             // Ensure index is valid before updating
-                                            if newIndex < onboardingData.rateValues.count,
-                                               index < onboardingData.isfItems.count
+                                            if newIndex < state.rateValues.count,
+                                               index < state.isfItems.count
                                             {
-                                                onboardingData.isfItems[index].rateIndex = newIndex
+                                                state.isfItems[index].rateIndex = newIndex
                                                 // Force refresh when slider changes
                                                 refreshUI = UUID()
                                             }
                                         }
                                     ),
-                                    in: onboardingData.rateValues.isEmpty ? 0 ... 1 :
-                                        Double(truncating: onboardingData.rateValues.first! as NSNumber) ...
-                                        Double(truncating: onboardingData.rateValues.last! as NSNumber),
-                                    step: onboardingData.units == .mgdL ? 1 : 0.1
+                                    in: state.rateValues.isEmpty ? 0 ... 1 :
+                                        Double(truncating: state.rateValues.first! as NSNumber) ...
+                                        Double(truncating: state.rateValues.last! as NSNumber),
+                                    step: state.units == .mgdL ? 1 : 0.1
                                 )
                                 .accentColor(.red)
                                 .padding(.horizontal, 5)
-                                .onChange(of: onboardingData.isfItems[index].rateIndex) { _, _ in
+                                .onChange(of: state.isfItems[index].rateIndex) { _, _ in
                                     // Trigger immediate UI update when slider value changes
                                     let impact = UIImpactFeedbackGenerator(style: .light)
                                     impact.impactOccurred()
@@ -146,7 +146,7 @@ struct InsulinSensitivityStepView: View {
 
                                 // Display the current value
                                 Text(
-                                    "\(onboardingData.rateValues.isEmpty || item.rateIndex >= onboardingData.rateValues.count ? "--" : numberFormatter.string(from: onboardingData.rateValues[item.rateIndex] as NSNumber) ?? "--") \(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")"
+                                    "\(state.rateValues.isEmpty || item.rateIndex >= state.rateValues.count ? "--" : numberFormatter.string(from: state.rateValues[item.rateIndex] as NSNumber) ?? "--") \(state.units == .mgdL ? "mg/dL" : "mmol/L")"
                                 )
                                 .frame(width: 90, alignment: .trailing)
                                 .lineLimit(1)
@@ -155,7 +155,7 @@ struct InsulinSensitivityStepView: View {
                                 // Delete button (not for the first entry at 00:00)
                                 if index > 0 {
                                     Button(action: {
-                                        onboardingData.isfItems.remove(at: index)
+                                        state.isfItems.remove(at: index)
                                     }) {
                                         Image(systemName: "trash")
                                             .foregroundColor(.red)
@@ -176,14 +176,14 @@ struct InsulinSensitivityStepView: View {
                     .cornerRadius(10)
                     .padding(.horizontal)
                     .onAppear {
-                        if onboardingData.isfItems.isEmpty {
-                            onboardingData.addISFValue()
+                        if state.isfItems.isEmpty {
+                            state.addISFValue()
                         }
                     }
                 }
 
                 // Example calculation based on first ISF
-                if !onboardingData.isfItems.isEmpty {
+                if !state.isfItems.isEmpty {
                     Divider()
                         .padding(.horizontal)
 
@@ -194,19 +194,19 @@ struct InsulinSensitivityStepView: View {
 
                         VStack(alignment: .leading, spacing: 4) {
                             // Current glucose is 40 mg/dL or 2.2 mmol/L above target
-                            let aboveTarget = onboardingData.units == .mgdL ? 40.0 : 2.2
+                            let aboveTarget = state.units == .mgdL ? 40.0 : 2.2
 
-                            let isfValue = onboardingData.rateValues.isEmpty || onboardingData.isfItems.isEmpty ?
-                                Double(truncating: onboardingData.isf as NSNumber) :
+                            let isfValue = state.rateValues.isEmpty || state.isfItems.isEmpty ?
+                                Double(truncating: state.isf as NSNumber) :
                                 Double(
-                                    truncating: onboardingData
-                                        .rateValues[onboardingData.isfItems.first!.rateIndex] as NSNumber
+                                    truncating: state
+                                        .rateValues[state.isfItems.first!.rateIndex] as NSNumber
                                 )
 
                             let insulinNeeded = aboveTarget / isfValue
 
                             Text(
-                                "If you are \(numberFormatter.string(from: NSNumber(value: aboveTarget)) ?? "--") \(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L") above target:"
+                                "If you are \(numberFormatter.string(from: NSNumber(value: aboveTarget)) ?? "--") \(state.units == .mgdL ? "mg/dL" : "mmol/L") above target:"
                             )
                             .font(.subheadline)
                             .padding(.horizontal)
@@ -233,7 +233,7 @@ struct InsulinSensitivityStepView: View {
                             .padding(.horizontal)
 
                         VStack(alignment: .leading, spacing: 4) {
-                            if onboardingData.units == .mgdL {
+                            if state.units == .mgdL {
                                 Text("• An ISF of 50 mg/dL means 1 unit of insulin lowers your BG by 50 mg/dL")
                                 Text("• A lower number means you're more sensitive to insulin")
                                 Text("• A higher number means you're less sensitive to insulin")
@@ -260,19 +260,19 @@ struct InsulinSensitivityStepView: View {
             for hour in 0 ..< 24 {
                 let hourInMinutes = hour * 60
                 // Calculate timeIndex for this hour
-                let timeIndex = onboardingData.isfTimeValues
+                let timeIndex = state.isfTimeValues
                     .firstIndex { abs($0 - Double(hourInMinutes * 60)) < 10 } ?? 0
 
                 // Check if this hour is already in the profile
-                if !onboardingData.isfItems.contains(where: { $0.timeIndex == timeIndex }) {
+                if !state.isfItems.contains(where: { $0.timeIndex == timeIndex }) {
                     buttons.append(.default(Text("\(String(format: "%02d:00", hour))")) {
                         // Get the current rate from the last item
-                        let rateIndex = onboardingData.isfItems.last?.rateIndex ?? 45 // Default to 45 mg/dL
+                        let rateIndex = state.isfItems.last?.rateIndex ?? 45 // Default to 45 mg/dL
                         // Create new item with the specified time
                         let newItem = ISFEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
                         // Add the new item and sort the list
-                        onboardingData.isfItems.append(newItem)
-                        onboardingData.isfItems.sort(by: { $0.timeIndex < $1.timeIndex })
+                        state.isfItems.append(newItem)
+                        state.isfItems.sort(by: { $0.timeIndex < $1.timeIndex })
                     })
                 }
             }
@@ -297,36 +297,36 @@ 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 = onboardingData.isfTimeValues.firstIndex { abs($0 - 0) < 1 } ?? 0
-        let defaultISF = onboardingData.units == .mgdL ? 50.0 : 2.8
-        let rateIndex = onboardingData.rateValues.firstIndex { abs(Double($0) - defaultISF) < 0.5 } ?? 45
+        let timeIndex = state.isfTimeValues.firstIndex { abs($0 - 0) < 1 } ?? 0
+        let defaultISF = state.units == .mgdL ? 50.0 : 2.8
+        let rateIndex = state.rateValues.firstIndex { abs(Double($0) - defaultISF) < 0.5 } ?? 45
 
         let newItem = ISFEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
-        onboardingData.isfItems.append(newItem)
+        state.isfItems.append(newItem)
     }
 
     // Computed property to check if we can add more ISF values
     private var canAddISF: Bool {
-        guard let lastItem = onboardingData.isfItems.last else { return true }
-        return lastItem.timeIndex < onboardingData.isfTimeValues.count - 1
+        guard let lastItem = state.isfItems.last else { return true }
+        return lastItem.timeIndex < state.isfTimeValues.count - 1
     }
 
     // Chart for visualizing ISF profile
     private var isfChart: some View {
         Chart {
-            ForEach(Array(onboardingData.isfItems.enumerated()), id: \.element.id) { index, item in
-                let displayValue = onboardingData.rateValues[item.rateIndex]
+            ForEach(Array(state.isfItems.enumerated()), id: \.element.id) { index, item in
+                let displayValue = state.rateValues[item.rateIndex]
 
                 let tzOffset = TimeZone.current.secondsFromGMT() * -1
-                let startDate = Date(timeIntervalSinceReferenceDate: onboardingData.isfTimeValues[item.timeIndex])
+                let startDate = Date(timeIntervalSinceReferenceDate: state.isfTimeValues[item.timeIndex])
                     .addingTimeInterval(TimeInterval(tzOffset))
-                let endDate = onboardingData.isfItems.count > index + 1 ?
+                let endDate = state.isfItems.count > index + 1 ?
                     Date(
-                        timeIntervalSinceReferenceDate: onboardingData
-                            .isfTimeValues[onboardingData.isfItems[index + 1].timeIndex]
+                        timeIntervalSinceReferenceDate: state
+                            .isfTimeValues[state.isfItems[index + 1].timeIndex]
                     )
                     .addingTimeInterval(TimeInterval(tzOffset)) :
-                    Date(timeIntervalSinceReferenceDate: onboardingData.isfTimeValues.last!).addingTimeInterval(30 * 60)
+                    Date(timeIntervalSinceReferenceDate: state.isfTimeValues.last!).addingTimeInterval(30 * 60)
                     .addingTimeInterval(TimeInterval(tzOffset))
 
                 RectangleMark(

Trio/Sources/Modules/Main/View/OnboardingStepViews.swift → Trio/Sources/Modules/Onboarding/View/OnboardingSteps/OnboardingStepViews.swift


+ 145 - 144
Trio/Sources/Modules/Main/View/OnboardingView.swift

@@ -1,169 +1,173 @@
 import SwiftUI
+import Swinject
 
 /// The main onboarding view that manages navigation between onboarding steps.
-struct OnboardingView: View {
-    let manager: OnboardingManager
-    @State private var onboardingData = OnboardingData()
-    @State private var currentStep: OnboardingStep = .welcome
-
-    // Animation states
-    @State private var animationScale: CGFloat = 1.0
-    @State private var animationOpacity: Double = 0
-    @State private var isAnimating = false
-
-    var body: some View {
-        NavigationView {
-            ZStack {
-                // Background gradient
-                LinearGradient(
-                    gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
-                    startPoint: .top,
-                    endPoint: .bottom
-                )
-                .ignoresSafeArea()
-
-                VStack(spacing: 0) {
-                    // Progress bar
-                    OnboardingProgressBar(
-                        currentStep: OnboardingStep.allCases.firstIndex(of: currentStep) ?? 0,
-                        totalSteps: OnboardingStep.allCases.count - 1
+extension Onboarding {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @State var state = StateModel()
+        let onboardingManager: OnboardingManager
+        @State private var currentStep: OnboardingStep = .welcome
+
+        // Animation states
+        @State private var animationScale: CGFloat = 1.0
+        @State private var animationOpacity: Double = 0
+        @State private var isAnimating = false
+
+        var body: some View {
+            NavigationView {
+                ZStack {
+                    // Background gradient
+                    LinearGradient(
+                        gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+                        startPoint: .top,
+                        endPoint: .bottom
                     )
-                    .padding(.top)
-
-                    // Step content
-                    ScrollView {
-                        VStack(alignment: .leading, spacing: 20) {
-                            // Header
-                            HStack {
-                                Image(systemName: currentStep.iconName)
-                                    .font(.system(size: 40))
-                                    .foregroundColor(currentStep.accentColor)
-                                    .frame(width: 60, height: 60)
-                                    .background(
-                                        Circle()
-                                            .fill(currentStep.accentColor.opacity(0.2))
-                                    )
-
-                                VStack(alignment: .leading) {
-                                    Text(currentStep.title)
-                                        .font(.largeTitle)
-                                        .fontWeight(.bold)
-                                        .foregroundColor(.primary)
-
-                                    Text(currentStep.description)
-                                        .font(.subheadline)
-                                        .foregroundColor(.secondary)
-                                        .fixedSize(horizontal: false, vertical: true)
+                    .ignoresSafeArea()
+
+                    VStack(spacing: 0) {
+                        // Progress bar
+                        OnboardingProgressBar(
+                            currentStep: OnboardingStep.allCases.firstIndex(of: currentStep) ?? 0,
+                            totalSteps: OnboardingStep.allCases.count - 1
+                        )
+                        .padding(.top)
+
+                        // Step content
+                        ScrollView {
+                            VStack(alignment: .leading, spacing: 20) {
+                                // Header
+                                HStack {
+                                    Image(systemName: currentStep.iconName)
+                                        .font(.system(size: 40))
+                                        .foregroundColor(currentStep.accentColor)
+                                        .frame(width: 60, height: 60)
+                                        .background(
+                                            Circle()
+                                                .fill(currentStep.accentColor.opacity(0.2))
+                                        )
+
+                                    VStack(alignment: .leading) {
+                                        Text(currentStep.title)
+                                            .font(.largeTitle)
+                                            .fontWeight(.bold)
+                                            .foregroundColor(.primary)
+
+                                        Text(currentStep.description)
+                                            .font(.subheadline)
+                                            .foregroundColor(.secondary)
+                                            .fixedSize(horizontal: false, vertical: true)
+                                    }
                                 }
-                            }
-                            .padding(.horizontal)
-                            .padding(.top)
+                                .padding(.horizontal)
+                                .padding(.top)
+
+                                // Animation container (for steps that include animations)
+                                AnimationPlaceholder(for: currentStep)
+                                    .padding()
+                                    .scaleEffect(animationScale)
+                                    .opacity(animationOpacity)
+                                    .onAppear {
+                                        withAnimation(.easeInOut(duration: 0.7)) {
+                                            animationOpacity = 1
+                                            animationScale = 1.0
+                                        }
+                                        // Start pulse animation
+                                        isAnimating = true
+                                    }
 
-                            // Animation container (for steps that include animations)
-                            AnimationPlaceholder(for: currentStep)
-                                .padding()
-                                .scaleEffect(animationScale)
-                                .opacity(animationOpacity)
-                                .onAppear {
-                                    withAnimation(.easeInOut(duration: 0.7)) {
-                                        animationOpacity = 1
-                                        animationScale = 1.0
+                                // Step-specific content
+                                Group {
+                                    switch currentStep {
+                                    case .welcome:
+                                        WelcomeStepView()
+                                    case .glucoseTarget:
+                                        GlucoseTargetStepView(state: state)
+                                    case .basalProfile:
+                                        BasalProfileStepView(state: state)
+                                    case .carbRatio:
+                                        CarbRatioStepView(state: state)
+                                    case .insulinSensitivity:
+                                        InsulinSensitivityStepView(state: state)
+                                    case .completed:
+                                        CompletedStepView()
                                     }
-                                    // Start pulse animation
-                                    isAnimating = true
                                 }
+                                .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
+                                .padding(.horizontal)
+                                .id(currentStep.id) // Force view recreation when step changes
+                            }
+                            .padding(.bottom, 80) // Make room for buttons at bottom
+                        }
 
-                            // Step-specific content
-                            Group {
-                                switch currentStep {
-                                case .welcome:
-                                    WelcomeStepView()
-                                case .glucoseTarget:
-                                    GlucoseTargetStepView(onboardingData: onboardingData)
-                                case .basalProfile:
-                                    BasalProfileStepView(onboardingData: onboardingData)
-                                case .carbRatio:
-                                    CarbRatioStepView(onboardingData: onboardingData)
-                                case .insulinSensitivity:
-                                    InsulinSensitivityStepView(onboardingData: onboardingData)
-                                case .completed:
-                                    CompletedStepView()
+                        Spacer()
+
+                        // Navigation buttons
+                        HStack {
+                            // Back button
+                            if currentStep != .welcome {
+                                Button(action: {
+                                    withAnimation {
+                                        if let previous = currentStep.previous {
+                                            currentStep = previous
+                                        }
+                                    }
+                                }) {
+                                    HStack {
+                                        Image(systemName: "chevron.left")
+                                        Text("Back")
+                                    }
+                                    .padding()
+                                    .foregroundColor(.primary)
                                 }
                             }
-                            .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
-                            .padding(.horizontal)
-                            .id(currentStep.id) // Force view recreation when step changes
-                        }
-                        .padding(.bottom, 80) // Make room for buttons at bottom
-                    }
 
-                    Spacer()
+                            Spacer()
 
-                    // Navigation buttons
-                    HStack {
-                        // Back button
-                        if currentStep != .welcome {
+                            // Next/Finish button
                             Button(action: {
                                 withAnimation {
-                                    if let previous = currentStep.previous {
-                                        currentStep = previous
+                                    if currentStep == .completed {
+                                        // Apply settings and complete onboarding
+                                        state.applyToSettings()
+                                        onboardingManager.completeOnboarding()
+                                    } else if let next = currentStep.next {
+                                        currentStep = next
                                     }
                                 }
                             }) {
                                 HStack {
-                                    Image(systemName: "chevron.left")
-                                    Text("Back")
+                                    Text(currentStep == .completed ? "Get Started" : "Next")
+                                    Image(systemName: "chevron.right")
                                 }
                                 .padding()
-                                .foregroundColor(.primary)
+                                .foregroundColor(.white)
+                                .background(
+                                    Capsule()
+                                        .fill(currentStep.accentColor)
+                                )
                             }
                         }
-
-                        Spacer()
-
-                        // Next/Finish button
-                        Button(action: {
-                            withAnimation {
-                                if currentStep == .completed {
-                                    // Apply settings and complete onboarding
-                                    onboardingData.applyToSettings()
-                                    manager.completeOnboarding()
-                                } else if let next = currentStep.next {
-                                    currentStep = next
-                                }
-                            }
-                        }) {
-                            HStack {
-                                Text(currentStep == .completed ? "Get Started" : "Next")
-                                Image(systemName: "chevron.right")
-                            }
-                            .padding()
-                            .foregroundColor(.white)
-                            .background(
-                                Capsule()
-                                    .fill(currentStep.accentColor)
-                            )
-                        }
+                        .padding(.horizontal)
+                        .padding(.bottom)
                     }
-                    .padding(.horizontal)
-                    .padding(.bottom)
                 }
+                .navigationBarHidden(true)
             }
-            .navigationBarHidden(true)
-        }
-        .onChange(of: currentStep) { _, _ in
-            // Reset animation when step changes
-            animationScale = 0.9
-            animationOpacity = 0
-            isAnimating = false
-
-            // Start new animation
-            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
-                withAnimation(.easeInOut(duration: 0.7)) {
-                    animationOpacity = 1
-                    animationScale = 1.0
+            .onChange(of: currentStep) { _, _ in
+                // Reset animation when step changes
+                animationScale = 0.9
+                animationOpacity = 0
+                isAnimating = false
+
+                // Start new animation
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+                    withAnimation(.easeInOut(duration: 0.7)) {
+                        animationOpacity = 1
+                        animationScale = 1.0
+                    }
+                    isAnimating = true
                 }
-                isAnimating = true
             }
         }
     }
@@ -423,13 +427,10 @@ struct AnimationPlaceholder: View {
 struct Onboarding_Preview: PreviewProvider {
     static var previews: some View {
         Group {
+            let resolver = TrioApp.resolver
             let onboardingManager = OnboardingManager()
-            OnboardingView(manager: onboardingManager)
+            Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager)
                 .previewDisplayName("Onboarding Flow")
-
-            OnboardingView(manager: onboardingManager)
-                .environment(\.colorScheme, .dark)
-                .previewDisplayName("Onboarding Flow (Dark)")
         }
     }
 }

+ 12 - 12
Trio/Sources/Services/OnboardingManager/OnboardingManager.swift

@@ -2,18 +2,6 @@ import Foundation
 import SwiftUI
 import Swinject
 
-extension UserDefaults {
-    /// Flag that indicates if onboarding has been completed.
-    var onboardingCompleted: Bool {
-        get {
-            bool(forKey: "onboardingCompleted")
-        }
-        set {
-            set(newValue, forKey: "onboardingCompleted")
-        }
-    }
-}
-
 /// Manages the app's onboarding experience, ensuring it's only shown to new users.
 /// Coordinates the display of onboarding screens when the app is launched for the first time.
 final class OnboardingManager: ObservableObject, Injectable {
@@ -46,3 +34,15 @@ final class OnboardingManager: ObservableObject, Injectable {
         shouldShowOnboarding = true
     }
 }
+
+extension UserDefaults {
+    /// Flag that indicates if onboarding has been completed.
+    var onboardingCompleted: Bool {
+        get {
+            bool(forKey: "onboardingCompleted")
+        }
+        set {
+            set(newValue, forKey: "onboardingCompleted")
+        }
+    }
+}