Browse Source

Refactor saving func and add units and limits in WIP

Deniz Cengiz 1 year ago
parent
commit
6fe0fdbf56

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -552,6 +552,7 @@
 		DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */; };
 		DD3F1F832D9DC78800DCE7B3 /* UnitSelectionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */; };
 		DD3F1F852D9DD84000DCE7B3 /* DeliveryLimitsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */; };
+		DD3F1F872D9DDB1200DCE7B3 /* AnimationPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F862D9DDB1200DCE7B3 /* AnimationPlaceholder.swift */; };
 		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
@@ -1331,6 +1332,7 @@
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
 		DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitSelectionStepView.swift; sourceTree = "<group>"; };
 		DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryLimitsStepView.swift; sourceTree = "<group>"; };
+		DD3F1F862D9DDB1200DCE7B3 /* AnimationPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationPlaceholder.swift; sourceTree = "<group>"; };
 		DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntent.swift; sourceTree = "<group>"; };
 		DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntentRequest.swift; sourceTree = "<group>"; };
 		DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatsView.swift; sourceTree = "<group>"; };
@@ -2727,6 +2729,7 @@
 		BD47FD152D88AAD80043966B /* View */ = {
 			isa = PBXGroup;
 			children = (
+				DD3F1F862D9DDB1200DCE7B3 /* AnimationPlaceholder.swift */,
 				DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */,
 				BD47FD182D88AAF90043966B /* OnboardingView.swift */,
 				BD47FDD52D8B64AE0043966B /* OnboardingSteps */,
@@ -3910,6 +3913,7 @@
 				3811DF1025CAAAE200A708ED /* APSManager.swift in Sources */,
 				3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */,
 				38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */,
+				DD3F1F872D9DDB1200DCE7B3 /* AnimationPlaceholder.swift in Sources */,
 				3811DE1725C9D40400A708ED /* Screen.swift in Sources */,
 				383948DA25CD64D500E91849 /* Glucose.swift in Sources */,
 				CE94598029E9E3BD0047C9C6 /* WatchConfigDataFlow.swift in Sources */,

+ 1 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -125905,7 +125905,7 @@
         }
       }
     },
-    "Note: Choosing your pump model determines which increments for setting up your basal rates (0.1 or 0.05) are available. You will pair your actual pump after finishing the onboarding process through Trio's main screen, the Home View." : {
+    "Note: Choosing your pump model determines which increments for setting up your basal rates are available. You will pair your actual pump after finishing the onboarding process." : {
 
     },
     "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app." : {

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

@@ -17,6 +17,15 @@ enum OnboardingStep: Int, CaseIterable, Identifiable {
 
     var id: Int { rawValue }
 
+    var hasSubsteps: Bool {
+        self == .deliveryLimits
+    }
+
+    var substeps: [DeliveryLimitSubstep] {
+        guard hasSubsteps else { return [] }
+        return DeliveryLimitSubstep.allCases
+    }
+
     /// The title to display for this onboarding step.
     var title: String {
         switch self {
@@ -102,9 +111,10 @@ enum OnboardingStep: Int, CaseIterable, Identifiable {
     /// The accent color to use for this step.
     var accentColor: Color {
         switch self {
-        case .welcome:
-            return Color.blue
-        case .unitSelection:
+        case .completed,
+             .deliveryLimits,
+             .unitSelection,
+             .welcome:
             return Color.blue
         case .glucoseTarget:
             return Color.green
@@ -114,8 +124,77 @@ enum OnboardingStep: Int, CaseIterable, Identifiable {
             return Color.orange
         case .insulinSensitivity:
             return Color.red
-        case .completed:
-            return Color.blue
+        }
+    }
+}
+
+enum DeliveryLimitSubstep: Int, CaseIterable, Identifiable {
+    case maxIOB
+    case maxBolus
+    case maxBasal
+    case maxCOB
+
+    var id: Int { rawValue }
+
+    var title: String {
+        switch self {
+        case .maxIOB: return String(localized: "Max IOB", comment: "Max IOB")
+        case .maxBolus: return String(localized: "Max Bolus")
+        case .maxBasal: return String(localized: "Max Basal")
+        case .maxCOB: return String(localized: "Max COB", comment: "Max COB")
+        }
+    }
+
+    var hint: String {
+        switch self {
+        case .maxIOB: return String(localized: "Maximum units of insulin allowed to be active.")
+        case .maxBolus: return String(localized: "Largest bolus of insulin allowed.")
+        case .maxBasal: return String(localized: "Largest basal rate allowed.")
+        case .maxCOB: return String(localized: "Maximum Carbs On Board (COB) allowed.")
+        }
+    }
+
+    var description: any View {
+        switch self {
+        case .maxIOB:
+            return VStack(alignment: .leading, spacing: 8) {
+                Text(
+                    "This is the maximum amount of Insulin On Board (IOB) above profile basal rates from all sources - positive temporary basal rates, manual or meal boluses, and SMBs - that Trio is allowed to accumulate to address an above target glucose."
+                )
+                Text(
+                    "If a calculated amount exceeds this limit, the suggested and / or delivered amount will be reduced so that active insulin on board (IOB) will not exceed this safety limit."
+                )
+                Text(
+                    "Note: You can still manually bolus above this limit, but the suggested bolus amount will never exceed this in the bolus calculator."
+                )
+            }
+        case .maxBolus:
+            return VStack(alignment: .leading, spacing: 8) {
+                Text(
+                    "This is the maximum bolus allowed to be delivered at one time. This limits manual and automatic bolus."
+                )
+                Text("Most set this to their largest meal bolus. Then, adjust if needed.")
+                Text("If you attempt to request a bolus larger than this, the bolus will not be accepted.")
+            }
+        case .maxBasal:
+            return VStack(alignment: .leading, spacing: 8) {
+                Text(
+                    "This is the maximum basal rate allowed to be set or scheduled. This applies to both automatic and manual basal rates."
+                )
+                Text(
+                    "Note to Medtronic Pump Users: You must also manually set the max basal rate on the pump to this value or higher."
+                )
+            }
+        case .maxCOB:
+            return VStack(alignment: .leading, spacing: 8) {
+                Text(
+                    "This setting defines the maximum amount of Carbs On Board (COB) at any given time for Trio to use in dosing calculations. If more carbs are entered than allowed by this limit, Trio will cap the current COB in calculations to Max COB and remain at max until all remaining carbs have shown to be absorbed."
+                )
+                Text(
+                    "For example, if Max COB is 120 g and you enter a meal containing 150 g of carbs, your COB will remain at 120 g until the remaining 30 g have been absorbed."
+                )
+                Text("This is an important limit when UAM is ON.")
+            }
         }
     }
 }
@@ -145,8 +224,11 @@ enum PumpOptionsForOnboardingUnits: String, Equatable, CaseIterable, Identifiabl
 /// Model that holds the data collected during onboarding.
 extension Onboarding {
     @Observable final class StateModel: BaseStateModel<Provider> {
-        @ObservationIgnored @Injected() var storage: FileStorage!
+        @ObservationIgnored @Injected() var fileStorage: FileStorage!
         @ObservationIgnored @Injected() var deviceManager: DeviceDataManager!
+        @ObservationIgnored @Injected() private var broadcaster: Broadcaster!
+
+        private let settingsProvider = PickerSettingsProvider.shared
 
         // Carb Ratio related
         var carbRatioItems: [CarbRatioEditor.Item] = []
@@ -189,7 +271,6 @@ extension Onboarding {
         let targetTimeValues = stride(from: 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)
         }
@@ -208,6 +289,11 @@ extension Onboarding {
 
         var pumpModel: PumpOptionsForOnboardingUnits = .omnipodDash
 
+        var maxBolus: Decimal = 10
+        var maxBasal: Decimal = 2
+        var maxIOB: Decimal = 0
+        var maxCOB: Decimal = 120
+
         struct BasalRateEntry: Identifiable {
             var id = UUID()
             var startTime: Int // Minutes from midnight
@@ -221,7 +307,14 @@ extension Onboarding {
         }
 
         override func subscribe() {
+            // TODO: why are we immediately storing to settings?
+//            saveOnboardingData()
+        }
+
+        func saveOnboardingData() {
             applyToSettings()
+            applyToPreferences()
+            applyToPumpSettings()
         }
 
         /// Applies the onboarding data to the app's settings.
@@ -229,27 +322,41 @@ extension Onboarding {
             // Make a copy of the current settings that we can mutate
             var settingsCopy = settingsManager.settings
 
-            // Apply glucose units
             settingsCopy.units = units
 
-            // Apply targets
+            // Store therapy settings
             saveTargets()
-
-            // Apply basal profile
-            // TODO: - should we use the return value or modify the function to not return anything?
-            _ = saveBasalProfile()
-
-            // Apply carb ratio
+            saveBasalProfile()
             saveCarbRatios()
-
-            // Apply ISF values
             saveISFValues()
 
-            // Instead of using updateSettings which doesn't exist,
-            // we'll directly set the settings property which will trigger the didSet observer
+            // We'll directly set the settings property which will trigger the didSet observer
             settingsManager.settings = settingsCopy
         }
 
+        func applyToPreferences() {
+            var preferencesCopy = settingsManager.preferences
+
+            preferencesCopy.maxIOB = maxIOB
+            preferencesCopy.maxCOB = maxCOB
+
+            // We'll directly set the preferences property which will trigger the didSet observer
+            settingsManager.preferences = preferencesCopy
+        }
+
+        func applyToPumpSettings() {
+            let defaultDIA = settingsProvider.settings.insulinPeakTime.value
+            let pumpSettings = PumpSettings(insulinActionCurve: defaultDIA, maxBolus: maxBolus, maxBasal: maxBasal)
+            fileStorage.save(pumpSettings, as: OpenAPS.Settings.settings)
+
+            // TODO: is this actually necessary at this point? Nothing is set up yet, nothing is subscribed to this observer...
+            DispatchQueue.main.async {
+                self.broadcaster.notify(PumpSettingsObserver.self, on: DispatchQueue.main) {
+                    $0.pumpSettingsDidChange(pumpSettings)
+                }
+            }
+        }
+
         // TODO: clean up these function and unify them
         func getTargetTherapyItems(from targets: [TargetsEditor.Item]) -> [TherapySettingItem] {
             targets.map {
@@ -398,7 +505,7 @@ extension Onboarding.StateModel {
 //    }
 
     func saveCarbRatioProfile(_ profile: CarbRatios) {
-        storage.save(profile, as: OpenAPS.Settings.carbRatios)
+        fileStorage.save(profile, as: OpenAPS.Settings.carbRatios)
     }
 }
 
@@ -460,7 +567,7 @@ extension Onboarding.StateModel {
 //    }
 
     func saveTargets(_ profile: BGTargets) {
-        storage.save(profile, as: OpenAPS.Settings.bgTargets)
+        fileStorage.save(profile, as: OpenAPS.Settings.bgTargets)
     }
 }
 
@@ -522,7 +629,7 @@ extension Onboarding.StateModel {
 //    }
 
     func saveISFProfile(_ profile: InsulinSensitivities) {
-        storage.save(profile, as: OpenAPS.Settings.insulinSensitivities)
+        fileStorage.save(profile, as: OpenAPS.Settings.insulinSensitivities)
     }
 }
 
@@ -556,7 +663,7 @@ extension Onboarding.StateModel {
         basalProfileItems.append(newItem)
     }
 
-    func saveBasalProfile() -> AnyPublisher<Void, Error> {
+    func saveBasalProfile() {
         let profile = basalProfileItems.map { item -> BasalProfileEntry in
             let formatter = DateFormatter()
             formatter.timeZone = TimeZone(secondsFromGMT: 0)
@@ -567,25 +674,6 @@ extension Onboarding.StateModel {
             return BasalProfileEntry(start: formatter.string(from: date), minutes: minutes, rate: rate)
         }
 
-        guard let pump = deviceManager?.pumpManager else {
-            debugPrint("\(DebuggingIdentifiers.failed) No pump found; cannot save basal profile!")
-            return Fail(error: NSError()).eraseToAnyPublisher()
-        }
-
-        let syncValues = profile.map {
-            RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate))
-        }
-
-        return Future { promise in
-            pump.syncBasalRateSchedule(items: syncValues) { result in
-                switch result {
-                case .success:
-                    self.storage.save(profile, as: OpenAPS.Settings.basalProfile)
-                    promise(.success(()))
-                case let .failure(error):
-                    promise(.failure(error))
-                }
-            }
-        }.eraseToAnyPublisher()
+        fileStorage.save(profile, as: OpenAPS.Settings.basalProfile)
     }
 }

+ 232 - 0
Trio/Sources/Modules/Onboarding/View/AnimationPlaceholder.swift

@@ -0,0 +1,232 @@
+///// A simple animated placeholder for each step
+// struct AnimationPlaceholder: View {
+//    let step: OnboardingStep
+//    @State private var animationValue: Double = 0
+//
+//    init(for step: OnboardingStep) {
+//        self.step = step
+//    }
+//
+//    var body: some View {
+//        VStack {
+//            Group {
+//                switch step {
+//                case .welcome:
+//                    welcomeAnimation
+//                case .glucoseTarget:
+//                    glucoseTargetAnimation
+//                case .basalProfile:
+//                    basalProfileAnimation
+//                case .carbRatio:
+//                    carbRatioAnimation
+//                case .insulinSensitivity:
+//                    insulinSensitivityAnimation
+//                case .completed:
+//                    completedAnimation
+//                }
+//            }
+//            .frame(height: 180)
+//        }
+//        .onAppear {
+//            withAnimation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
+//                animationValue = 1.0
+//            }
+//        }
+//    }
+//
+//    // Custom animated views for each step
+//    var welcomeAnimation: some View {
+//        ZStack {
+//            ForEach(0 ..< 5) { index in
+//                Image(systemName: "heart.fill")
+//                    .font(.system(size: 40))
+//                    .foregroundColor(step.accentColor.opacity(0.8 - Double(index) * 0.15))
+//                    .offset(x: CGFloat.random(in: -100 ... 100), y: CGFloat.random(in: -60 ... 60))
+//                    .scaleEffect(1.0 + animationValue * 0.3)
+//                    .rotationEffect(.degrees(animationValue * Double.random(in: -30 ... 30)))
+//            }
+//
+//            Image(systemName: "syringe.fill")
+//                .font(.system(size: 80))
+//                .foregroundColor(step.accentColor)
+//                .scaleEffect(1.0 + animationValue * 0.2)
+//                .shadow(color: step.accentColor.opacity(0.5), radius: 10 * animationValue, x: 0, y: 0)
+//        }
+//    }
+//
+//    var glucoseTargetAnimation: some View {
+//        ZStack {
+//            // Target rings
+//            ForEach(0 ..< 3) { index in
+//                Circle()
+//                    .stroke(step.accentColor.opacity(Double(3 - index) * 0.3), lineWidth: 8)
+//                    .frame(width: 120 + CGFloat(index * 40))
+//                    .scaleEffect(1.0 + animationValue * 0.05)
+//            }
+//
+//            // Arrow
+//            Image(systemName: "arrow.down.to.line")
+//                .font(.system(size: 50))
+//                .foregroundColor(step.accentColor)
+//                .offset(y: -10 + animationValue * 20)
+//                .rotationEffect(.degrees(animationValue * 360))
+//        }
+//    }
+//
+//    var basalProfileAnimation: some View {
+//        ZStack {
+//            // Line graph representation
+//            Path { path in
+//                let width: CGFloat = 300
+//                let height: CGFloat = 100
+//
+//                path.move(to: CGPoint(x: 0, y: height * 0.5))
+//
+//                for i in 0 ..< 8 {
+//                    let x = width * CGFloat(i) / 7
+//                    let y = height * (0.5 + (sin(Double(i) * .pi / 3) * 0.4))
+//
+//                    if i == 0 {
+//                        path.move(to: CGPoint(x: x, y: y))
+//                    } else {
+//                        path.addLine(to: CGPoint(x: x, y: y))
+//                    }
+//                }
+//            }
+//            .trim(from: 0, to: animationValue)
+//            .stroke(step.accentColor, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
+//            .frame(width: 300, height: 100)
+//
+//            // Clock symbols to represent time
+//            HStack(spacing: 50) {
+//                Image(systemName: "clock")
+//                    .font(.system(size: 20))
+//                    .foregroundColor(step.accentColor)
+//                    .opacity(animationValue)
+//
+//                Image(systemName: "clock.fill")
+//                    .font(.system(size: 20))
+//                    .foregroundColor(step.accentColor)
+//                    .opacity(animationValue)
+//
+//                Image(systemName: "clock")
+//                    .font(.system(size: 20))
+//                    .foregroundColor(step.accentColor)
+//                    .opacity(animationValue)
+//            }
+//            .offset(y: 70)
+//        }
+//    }
+//
+//    var carbRatioAnimation: some View {
+//        ZStack {
+//            // Plate
+//            Circle()
+//                .fill(Color.gray.opacity(0.1))
+//                .frame(width: 150)
+//
+//            // Food items
+//            ForEach(0 ..< 5) { index in
+//                Image(systemName: [
+//                    "carrot.fill",
+//                    "fork.knife",
+//                    "takeoutbag.and.cup.and.straw.fill",
+//                    "wallet.pass.fill",
+//                    "cup.and.saucer.fill"
+//                ][index % 5])
+//                    .font(.system(size: 25))
+//                    .foregroundColor(step.accentColor)
+//                    .offset(
+//                        x: cos(Double(index) * .pi * 2 / 5) * 50 * animationValue,
+//                        y: sin(Double(index) * .pi * 2 / 5) * 50 * animationValue
+//                    )
+//                    .rotationEffect(.degrees(animationValue * 360))
+//            }
+//
+//            // Insulin
+//            Image(systemName: "drop.fill")
+//                .font(.system(size: 40))
+//                .foregroundColor(.blue)
+//                .scaleEffect(0.8 + animationValue * 0.3)
+//                .shadow(color: .blue.opacity(0.5), radius: 5, x: 0, y: 0)
+//        }
+//    }
+//
+//    var insulinSensitivityAnimation: some View {
+//        ZStack {
+//            // Glucose meter
+//            RoundedRectangle(cornerRadius: 20)
+//                .fill(Color.gray.opacity(0.1))
+//                .frame(width: 120, height: 200)
+//
+//            // Display screen
+//            RoundedRectangle(cornerRadius: 10)
+//                .fill(Color.black.opacity(0.1))
+//                .frame(width: 100, height: 60)
+//                .offset(y: -60)
+//
+//            // Value on screen
+//            Text("120")
+//                .font(.system(size: 24, weight: .bold, design: .monospaced))
+//                .foregroundColor(step.accentColor)
+//                .offset(y: -60)
+//                .opacity(animationValue)
+//
+//            // Insulin drop
+//            Image(systemName: "drop.fill")
+//                .font(.system(size: 30))
+//                .foregroundColor(.blue)
+//                .offset(y: 20)
+//                .opacity(1)
+//
+//            // Arrow showing decrease
+//            Image(systemName: "arrow.down")
+//                .font(.system(size: 30))
+//                .foregroundColor(step.accentColor)
+//                .offset(y: 60)
+//                .opacity(animationValue)
+//                .scaleEffect(1.0 + animationValue * 0.5)
+//
+//            // Lower value
+//            Text("80")
+//                .font(.system(size: 24, weight: .bold, design: .monospaced))
+//                .foregroundColor(step.accentColor)
+//                .offset(y: 100)
+//                .opacity(animationValue)
+//        }
+//    }
+//
+//    var completedAnimation: some View {
+//        ZStack {
+//            // Success checkmark
+//            Circle()
+//                .fill(step.accentColor.opacity(0.2))
+//                .frame(width: 150)
+//                .scaleEffect(animationValue)
+//
+//            Circle()
+//                .stroke(step.accentColor, lineWidth: 5)
+//                .frame(width: 150)
+//                .scaleEffect(animationValue)
+//
+//            Image(systemName: "checkmark")
+//                .font(.system(size: 80, weight: .bold))
+//                .foregroundColor(step.accentColor)
+//                .offset(y: animationValue * 5)
+//                .scaleEffect(animationValue)
+//
+//            // Celebrate particles
+//            ForEach(0 ..< 8) { index in
+//                Image(systemName: "star.fill")
+//                    .font(.system(size: 20))
+//                    .foregroundColor(step.accentColor)
+//                    .offset(
+//                        x: cos(Double(index) * .pi / 4) * 100 * animationValue,
+//                        y: sin(Double(index) * .pi / 4) * 100 * animationValue
+//                    )
+//                    .opacity(animationValue)
+//                    .scaleEffect(animationValue)
+//            }
+//        }
+//    }
+// }

+ 87 - 12
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/DeliveryLimitsStepView.swift

@@ -1,21 +1,96 @@
-//
-//  DeliveryLimitsStepView.swift
-//  Trio
-//
-//  Created by Cengiz Deniz on 02.04.25.
-//
-
 import SwiftUI
 
 struct DeliveryLimitsStepView: View {
     @Bindable var state: Onboarding.StateModel
+    let substep: DeliveryLimitSubstep
+
+    @State private var shouldDisplayPicker: Bool = false
+
+    private let settingsProvider = PickerSettingsProvider.shared
 
     var body: some View {
-        VStack(alignment: .leading, spacing: 8) {
-            Text("Max IOB")
-            Text("Max Bolus")
-            Text("Max Basal")
-            Text("Max COB")
+        VStack(alignment: .leading, spacing: 16) {
+            Text(substep.hint)
+                .font(.headline)
+
+            // Replace with real pickers or sliders later
+            switch substep {
+            case .maxIOB:
+                deliveryLimitInputSection(
+                    label: substep.title,
+                    displayPicker: $shouldDisplayPicker,
+                    setting: settingsProvider.settings.maxIOB,
+                    decimalValue: $state.maxIOB
+                )
+            case .maxBolus:
+                deliveryLimitInputSection(
+                    label: substep.title,
+                    displayPicker: $shouldDisplayPicker,
+                    setting: settingsProvider.settings.maxBolus,
+                    decimalValue: $state.maxBolus
+                )
+            case .maxBasal:
+                deliveryLimitInputSection(
+                    label: substep.title,
+                    displayPicker: $shouldDisplayPicker,
+                    setting: settingsProvider.settings.maxBasal,
+                    decimalValue: $state.maxBasal
+                )
+            case .maxCOB:
+                deliveryLimitInputSection(
+                    label: substep.title,
+                    displayPicker: $shouldDisplayPicker,
+                    setting: settingsProvider.settings.maxCOB,
+                    decimalValue: $state.maxCOB
+                )
+            }
+
+            AnyView(substep.description)
+                .font(.footnote)
+                .foregroundStyle(.secondary)
+        }
+    }
+
+    @ViewBuilder private func deliveryLimitInputSection(
+        label: String,
+        displayPicker: Binding<Bool>,
+        setting: PickerSetting,
+        decimalValue: Binding<Decimal>
+    ) -> some View {
+        VStack {
+            HStack {
+                Text(label)
+                Spacer()
+                displayText(for: substep, decimalValue: decimalValue.wrappedValue)
+                    .foregroundColor(!displayPicker.wrappedValue ? .primary : .accentColor)
+                    .onTapGesture {
+                        displayPicker.wrappedValue.toggle()
+                    }
+            }
+
+            if displayPicker.wrappedValue {
+                Picker(selection: decimalValue, label: Text(label)) {
+                    ForEach(settingsProvider.generatePickerValues(from: setting, units: state.units), id: \.self) { value in
+                        displayText(for: substep, decimalValue: value).tag(value)
+                    }
+                }
+                .pickerStyle(WheelPickerStyle())
+                .frame(maxWidth: .infinity)
+            }
+        }
+        .padding()
+        .background(Color.chart.opacity(0.45))
+        .cornerRadius(10)
+    }
+
+    private func displayText(for substep: DeliveryLimitSubstep, decimalValue: Decimal) -> Text {
+        switch substep {
+        case .maxBasal,
+             .maxBolus,
+             .maxIOB:
+            return Text("\(decimalValue) \(String(localized: "U", comment: "Insulin unit abbreviation"))")
+        case .maxCOB:
+            return Text("\(decimalValue) \(String(localized: "g", comment: "Gram abbreviation"))")
         }
     }
 }

+ 3 - 13
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/OnboardingStepViews.swift

@@ -58,14 +58,9 @@ struct CompletedStepView: View {
             .foregroundColor(.secondary)
 
             VStack(alignment: .leading, spacing: 12) {
-                SettingItemView(icon: "target", title: "Glucose Target", description: "Your target range is set")
-                SettingItemView(
-                    icon: "chart.xyaxis.line",
-                    title: "Basal Profile",
-                    description: "Your basal profile is configured"
-                )
-                SettingItemView(icon: "fork.knife", title: "Carb Ratio", description: "Your carb ratio is defined")
-                SettingItemView(icon: "drop.fill", title: "Insulin Sensitivity", description: "Your ISF is established")
+                ForEach(OnboardingStep.allCases.filter { $0 != .welcome && $0 != .completed }, id: \.self) { step in
+                    SettingItemView(icon: step.iconName, title: step.title)
+                }
             }
             .padding()
             .background(Color.green.opacity(0.1))
@@ -86,7 +81,6 @@ struct CompletedStepView: View {
 struct SettingItemView: View {
     let icon: String
     let title: String
-    let description: String
 
     var body: some View {
         HStack(spacing: 15) {
@@ -98,10 +92,6 @@ struct SettingItemView: View {
             VStack(alignment: .leading, spacing: 2) {
                 Text(title)
                     .font(.headline)
-
-                Text(description)
-                    .font(.subheadline)
-                    .foregroundColor(.secondary)
             }
 
             Spacer()

+ 104 - 252
Trio/Sources/Modules/Onboarding/View/OnboardingView.swift

@@ -8,6 +8,7 @@ extension Onboarding {
         @State var state = StateModel()
         let onboardingManager: OnboardingManager
         @State private var currentStep: OnboardingStep = .welcome
+        @State private var currentDeliverySubstep: DeliveryLimitSubstep = .maxIOB
 
         // Animation states
         @State private var animationScale: CGFloat = 1.0
@@ -28,16 +29,18 @@ extension Onboarding {
                     VStack(spacing: 0) {
                         // Progress bar
                         OnboardingProgressBar(
-                            currentStep: OnboardingStep.allCases.firstIndex(of: currentStep) ?? 0,
-                            totalSteps: OnboardingStep.allCases.count - 1
+                            currentStep: currentStep,
+                            currentSubstep: currentStep == .deliveryLimits ? currentDeliverySubstep.rawValue : nil,
+                            stepsWithSubsteps: [.deliveryLimits: DeliveryLimitSubstep.allCases.count]
                         )
+
                         .padding(.top)
 
                         // Step content
                         ScrollView {
                             VStack(alignment: .leading, spacing: 20) {
                                 // Header
-                                if currentStep != .welcome {
+                                if currentStep != .welcome && currentStep != .completed {
                                     HStack {
                                         Image(systemName: currentStep.iconName)
                                             .font(.system(size: 40))
@@ -93,7 +96,7 @@ extension Onboarding {
                                     case .insulinSensitivity:
                                         InsulinSensitivityStepView(state: state)
                                     case .deliveryLimits:
-                                        DeliveryLimitsStepView(state: state)
+                                        DeliveryLimitsStepView(state: state, substep: currentDeliverySubstep)
                                     case .completed:
                                         CompletedStepView()
                                     }
@@ -113,7 +116,20 @@ extension Onboarding {
                             if currentStep != .welcome {
                                 Button(action: {
                                     withAnimation {
-                                        if let previous = currentStep.previous {
+                                        if currentStep == .completed {
+                                            currentStep = .deliveryLimits
+                                            currentDeliverySubstep = .maxCOB // ensure we land on the last substep visually
+                                        } else if currentStep == .deliveryLimits {
+                                            if let previousSub = DeliveryLimitSubstep(
+                                                rawValue: currentDeliverySubstep
+                                                    .rawValue - 1
+                                            ) {
+                                                currentDeliverySubstep = previousSub
+                                            } else if let previousMainStep = currentStep.previous {
+                                                currentStep = previousMainStep
+                                                currentDeliverySubstep = .maxIOB // reset to first substep for later return
+                                            }
+                                        } else if let previous = currentStep.previous {
                                             currentStep = previous
                                         }
                                     }
@@ -133,10 +149,16 @@ extension Onboarding {
                             Button(action: {
                                 withAnimation {
                                     if currentStep == .completed {
-                                        // Apply settings and complete onboarding
-                                        state.applyToSettings()
+                                        state.saveOnboardingData()
                                         onboardingManager.completeOnboarding()
                                         Foundation.NotificationCenter.default.post(name: .onboardingCompleted, object: nil)
+                                    } else if currentStep == .deliveryLimits {
+                                        if let nextSub = DeliveryLimitSubstep(rawValue: currentDeliverySubstep.rawValue + 1) {
+                                            currentDeliverySubstep = nextSub
+                                        } else if let next = currentStep.next {
+                                            currentStep = next
+                                            currentDeliverySubstep = .maxIOB
+                                        }
                                     } else if let next = currentStep.next {
                                         currentStep = next
                                     }
@@ -148,11 +170,7 @@ extension Onboarding {
                                 }
                                 .padding()
                                 .foregroundColor(.white)
-                                .background(
-                                    Capsule()
-//                                        .fill(currentStep.accentColor)
-                                        .fill(Color.blue)
-                                )
+                                .background(Capsule().fill(Color.blue))
                             }
                         }
                         .padding(.horizontal)
@@ -183,254 +201,88 @@ extension Onboarding {
 
 /// A progress bar that shows the user's progress through the onboarding process.
 struct OnboardingProgressBar: View {
-    let currentStep: Int
-    let totalSteps: Int
+    let currentStep: OnboardingStep
+    let currentSubstep: Int?
+    let stepsWithSubsteps: [OnboardingStep: Int] // e.g. [.deliveryLimits: 4]
 
     var body: some View {
         HStack(spacing: 4) {
-            ForEach(0 ..< totalSteps, id: \.self) { step in
-                Rectangle()
-                    .fill(step <= currentStep ? Color.blue : Color.gray.opacity(0.3))
-                    .frame(height: 4)
-                    .cornerRadius(2)
+            ForEach(renderedSteps, id: \.self.id) { element in
+                if let substeps = element.substeps {
+                    HStack(spacing: 2) {
+                        ForEach(0 ..< substeps, id: \.self) { i in
+                            Rectangle()
+                                .fill(isSubstepActive(for: element.step, index: i) ? Color.blue : Color.gray.opacity(0.3))
+                                .frame(height: 4)
+                                .cornerRadius(2)
+                        }
+                    }
+                } else {
+                    Rectangle()
+                        .fill(isStepActive(element.step) ? Color.blue : Color.gray.opacity(0.3))
+                        .frame(height: 4)
+                        .cornerRadius(2)
+                }
             }
         }
         .padding(.horizontal)
     }
-}
 
-///// A simple animated placeholder for each step
-// struct AnimationPlaceholder: View {
-//    let step: OnboardingStep
-//    @State private var animationValue: Double = 0
-//
-//    init(for step: OnboardingStep) {
-//        self.step = step
-//    }
-//
-//    var body: some View {
-//        VStack {
-//            Group {
-//                switch step {
-//                case .welcome:
-//                    welcomeAnimation
-//                case .glucoseTarget:
-//                    glucoseTargetAnimation
-//                case .basalProfile:
-//                    basalProfileAnimation
-//                case .carbRatio:
-//                    carbRatioAnimation
-//                case .insulinSensitivity:
-//                    insulinSensitivityAnimation
-//                case .completed:
-//                    completedAnimation
-//                }
-//            }
-//            .frame(height: 180)
-//        }
-//        .onAppear {
-//            withAnimation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
-//                animationValue = 1.0
-//            }
-//        }
-//    }
-//
-//    // Custom animated views for each step
-//    var welcomeAnimation: some View {
-//        ZStack {
-//            ForEach(0 ..< 5) { index in
-//                Image(systemName: "heart.fill")
-//                    .font(.system(size: 40))
-//                    .foregroundColor(step.accentColor.opacity(0.8 - Double(index) * 0.15))
-//                    .offset(x: CGFloat.random(in: -100 ... 100), y: CGFloat.random(in: -60 ... 60))
-//                    .scaleEffect(1.0 + animationValue * 0.3)
-//                    .rotationEffect(.degrees(animationValue * Double.random(in: -30 ... 30)))
-//            }
-//
-//            Image(systemName: "syringe.fill")
-//                .font(.system(size: 80))
-//                .foregroundColor(step.accentColor)
-//                .scaleEffect(1.0 + animationValue * 0.2)
-//                .shadow(color: step.accentColor.opacity(0.5), radius: 10 * animationValue, x: 0, y: 0)
-//        }
-//    }
-//
-//    var glucoseTargetAnimation: some View {
-//        ZStack {
-//            // Target rings
-//            ForEach(0 ..< 3) { index in
-//                Circle()
-//                    .stroke(step.accentColor.opacity(Double(3 - index) * 0.3), lineWidth: 8)
-//                    .frame(width: 120 + CGFloat(index * 40))
-//                    .scaleEffect(1.0 + animationValue * 0.05)
-//            }
-//
-//            // Arrow
-//            Image(systemName: "arrow.down.to.line")
-//                .font(.system(size: 50))
-//                .foregroundColor(step.accentColor)
-//                .offset(y: -10 + animationValue * 20)
-//                .rotationEffect(.degrees(animationValue * 360))
-//        }
-//    }
-//
-//    var basalProfileAnimation: some View {
-//        ZStack {
-//            // Line graph representation
-//            Path { path in
-//                let width: CGFloat = 300
-//                let height: CGFloat = 100
-//
-//                path.move(to: CGPoint(x: 0, y: height * 0.5))
-//
-//                for i in 0 ..< 8 {
-//                    let x = width * CGFloat(i) / 7
-//                    let y = height * (0.5 + (sin(Double(i) * .pi / 3) * 0.4))
-//
-//                    if i == 0 {
-//                        path.move(to: CGPoint(x: x, y: y))
-//                    } else {
-//                        path.addLine(to: CGPoint(x: x, y: y))
-//                    }
-//                }
-//            }
-//            .trim(from: 0, to: animationValue)
-//            .stroke(step.accentColor, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
-//            .frame(width: 300, height: 100)
-//
-//            // Clock symbols to represent time
-//            HStack(spacing: 50) {
-//                Image(systemName: "clock")
-//                    .font(.system(size: 20))
-//                    .foregroundColor(step.accentColor)
-//                    .opacity(animationValue)
-//
-//                Image(systemName: "clock.fill")
-//                    .font(.system(size: 20))
-//                    .foregroundColor(step.accentColor)
-//                    .opacity(animationValue)
-//
-//                Image(systemName: "clock")
-//                    .font(.system(size: 20))
-//                    .foregroundColor(step.accentColor)
-//                    .opacity(animationValue)
-//            }
-//            .offset(y: 70)
-//        }
-//    }
-//
-//    var carbRatioAnimation: some View {
-//        ZStack {
-//            // Plate
-//            Circle()
-//                .fill(Color.gray.opacity(0.1))
-//                .frame(width: 150)
-//
-//            // Food items
-//            ForEach(0 ..< 5) { index in
-//                Image(systemName: [
-//                    "carrot.fill",
-//                    "fork.knife",
-//                    "takeoutbag.and.cup.and.straw.fill",
-//                    "wallet.pass.fill",
-//                    "cup.and.saucer.fill"
-//                ][index % 5])
-//                    .font(.system(size: 25))
-//                    .foregroundColor(step.accentColor)
-//                    .offset(
-//                        x: cos(Double(index) * .pi * 2 / 5) * 50 * animationValue,
-//                        y: sin(Double(index) * .pi * 2 / 5) * 50 * animationValue
-//                    )
-//                    .rotationEffect(.degrees(animationValue * 360))
-//            }
-//
-//            // Insulin
-//            Image(systemName: "drop.fill")
-//                .font(.system(size: 40))
-//                .foregroundColor(.blue)
-//                .scaleEffect(0.8 + animationValue * 0.3)
-//                .shadow(color: .blue.opacity(0.5), radius: 5, x: 0, y: 0)
-//        }
-//    }
-//
-//    var insulinSensitivityAnimation: some View {
-//        ZStack {
-//            // Glucose meter
-//            RoundedRectangle(cornerRadius: 20)
-//                .fill(Color.gray.opacity(0.1))
-//                .frame(width: 120, height: 200)
-//
-//            // Display screen
-//            RoundedRectangle(cornerRadius: 10)
-//                .fill(Color.black.opacity(0.1))
-//                .frame(width: 100, height: 60)
-//                .offset(y: -60)
-//
-//            // Value on screen
-//            Text("120")
-//                .font(.system(size: 24, weight: .bold, design: .monospaced))
-//                .foregroundColor(step.accentColor)
-//                .offset(y: -60)
-//                .opacity(animationValue)
-//
-//            // Insulin drop
-//            Image(systemName: "drop.fill")
-//                .font(.system(size: 30))
-//                .foregroundColor(.blue)
-//                .offset(y: 20)
-//                .opacity(1)
-//
-//            // Arrow showing decrease
-//            Image(systemName: "arrow.down")
-//                .font(.system(size: 30))
-//                .foregroundColor(step.accentColor)
-//                .offset(y: 60)
-//                .opacity(animationValue)
-//                .scaleEffect(1.0 + animationValue * 0.5)
-//
-//            // Lower value
-//            Text("80")
-//                .font(.system(size: 24, weight: .bold, design: .monospaced))
-//                .foregroundColor(step.accentColor)
-//                .offset(y: 100)
-//                .opacity(animationValue)
-//        }
-//    }
-//
-//    var completedAnimation: some View {
-//        ZStack {
-//            // Success checkmark
-//            Circle()
-//                .fill(step.accentColor.opacity(0.2))
-//                .frame(width: 150)
-//                .scaleEffect(animationValue)
-//
-//            Circle()
-//                .stroke(step.accentColor, lineWidth: 5)
-//                .frame(width: 150)
-//                .scaleEffect(animationValue)
-//
-//            Image(systemName: "checkmark")
-//                .font(.system(size: 80, weight: .bold))
-//                .foregroundColor(step.accentColor)
-//                .offset(y: animationValue * 5)
-//                .scaleEffect(animationValue)
-//
-//            // Celebrate particles
-//            ForEach(0 ..< 8) { index in
-//                Image(systemName: "star.fill")
-//                    .font(.system(size: 20))
-//                    .foregroundColor(step.accentColor)
-//                    .offset(
-//                        x: cos(Double(index) * .pi / 4) * 100 * animationValue,
-//                        y: sin(Double(index) * .pi / 4) * 100 * animationValue
-//                    )
-//                    .opacity(animationValue)
-//                    .scaleEffect(animationValue)
-//            }
-//        }
-//    }
-// }
+    // Filter only the visible steps (exclude welcome and completed)
+    private var visibleSteps: [OnboardingStep] {
+        OnboardingStep.allCases.filter { $0 != .welcome && $0 != .completed }
+    }
+
+    // Combine steps with info on whether they have substeps
+    private var renderedSteps: [(id: String, step: OnboardingStep, substeps: Int?)] {
+        visibleSteps.map {
+            let sub = stepsWithSubsteps[$0]
+            return (id: "\($0.id)", step: $0, substeps: sub)
+        }
+    }
+
+    private func isStepActive(_ step: OnboardingStep) -> Bool {
+        // If we’re at .completed, everything should be filled
+        if currentStep == .completed { return true }
+
+        // Current step should be filled
+        if step == currentStep { return true }
+
+        // Steps before the current one should be filled
+        if let currentIndex = visibleSteps.firstIndex(of: currentStep),
+           let stepIndex = visibleSteps.firstIndex(of: step),
+           stepIndex < currentIndex
+        {
+            return true
+        }
+
+        return false
+    }
+
+    private func isSubstepActive(for step: OnboardingStep, index: Int) -> Bool {
+        guard let current = currentSubstep else {
+            // Special case: if currentStep is `.completed`, show all substeps as filled
+            if currentStep == .completed && step == .deliveryLimits {
+                return true
+            }
+            return false
+        }
+
+        if step == currentStep {
+            return index <= current
+        }
+
+        // If step comes before currentStep, mark all substeps filled
+        if let currentIndex = visibleSteps.firstIndex(of: currentStep),
+           let stepIndex = visibleSteps.firstIndex(of: step),
+           stepIndex < currentIndex
+        {
+            return true
+        }
+
+        return false
+    }
+}
 
 struct Onboarding_Preview: PreviewProvider {
     static var previews: some View {