| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- import SwiftUI
- import Swinject
- /// The main onboarding view that manages navigation between onboarding steps.
- extension Onboarding {
- struct RootView: BaseView {
- let resolver: Resolver
- @State var state = StateModel()
- @State private var navigationDirection: OnboardingNavigationDirection = .forward
- let onboardingManager: OnboardingManager
- // Step management
- @State private var currentStep: OnboardingStep = .welcome
- @State private var currentNightscoutSubstep: NightscoutSubstep = .setupSelection
- @State private var currentDeliverySubstep: DeliveryLimitSubstep = .maxIOB
- @State private var currentAutosensSubstep: AutosensSettingsSubstep = .autosensMin
- @State private var currentSMBSubstep: SMBSettingsSubstep = .enableSMBAlways
- @State private var currentTargetBehaviorSubstep: TargetBehaviorSubstep = .highTempTargetRaisesSensitivity
- // Animation states
- @State private var animationScale: CGFloat = 1.0
- @State private var animationOpacity: Double = 0
- @State private var isAnimating = false
- // Conditional button states for Nightscout substeps
- private var didSelectNightscoutSetupOption: Bool {
- currentNightscoutSubstep == .setupSelection && state
- .nightscoutSetupOption == .noSelection
- }
- private var hasValidNightscoutConnection: Bool {
- currentNightscoutSubstep == .connectToNightscout && !state.isConnectedToNS
- }
- private var didSelectNightscoutImportOption: Bool {
- currentNightscoutSubstep == .importFromNightscout && state.nightscoutImportOption == .noSelection
- }
- private var shouldDisableNextButton: Bool {
- (currentStep == .nightscout && didSelectNightscoutSetupOption)
- ||
- (currentStep == .nightscout && hasValidNightscoutConnection)
- ||
- (currentStep == .nightscout && didSelectNightscoutImportOption)
- }
- var body: some View {
- NavigationView {
- ZStack {
- // Background gradient
- LinearGradient(
- gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
- startPoint: .top,
- endPoint: .bottom
- )
- .ignoresSafeArea()
- VStack(spacing: 0) {
- if (nonInfoOnboardingSteps + [OnboardingStep.overview, OnboardingStep.completed]).contains(currentStep) {
- // Progress bar
- OnboardingProgressBar(
- currentStep: currentStep,
- currentSubstep: {
- switch currentStep {
- case .deliveryLimits: return currentDeliverySubstep.rawValue
- case .nightscout: return currentNightscoutSubstep.rawValue
- case .autosensSettings: return currentAutosensSubstep.rawValue
- case .smbSettings: return currentSMBSubstep.rawValue
- case .targetBehavior: return currentTargetBehaviorSubstep.rawValue
- default: return nil
- }
- }(),
- stepsWithSubsteps: [
- .nightscout: NightscoutSubstep.allCases.count,
- .deliveryLimits: DeliveryLimitSubstep.allCases.count,
- .autosensSettings: AutosensSettingsSubstep.allCases.count,
- .smbSettings: SMBSettingsSubstep.allCases.count,
- .targetBehavior: TargetBehaviorSubstep.allCases.count
- ],
- nightscoutSetupOption: state.nightscoutSetupOption
- )
- .padding(.top)
- }
- OnboardingStepContent(
- currentStep: $currentStep,
- currentNightscoutSubstep: $currentNightscoutSubstep,
- currentDeliverySubstep: $currentDeliverySubstep,
- currentAutosensSubstep: $currentAutosensSubstep,
- currentSMBSubstep: $currentSMBSubstep,
- currentTargetBehaviorSubstep: $currentTargetBehaviorSubstep,
- state: state,
- navigationDirection: navigationDirection
- )
- Spacer()
- OnboardingNavigationButtons(
- currentStep: $currentStep,
- currentNightscoutSubstep: $currentNightscoutSubstep,
- currentDeliverySubstep: $currentDeliverySubstep,
- currentAutosensSubstep: $currentAutosensSubstep,
- currentSMBSubstep: $currentSMBSubstep,
- currentTargetBehaviorSubstep: $currentTargetBehaviorSubstep,
- onboardingManager: onboardingManager,
- state: state,
- shouldDisableNextButton: shouldDisableNextButton,
- navigationDirectionChanged: { navigationDirection = $0 }
- )
- }
- }
- .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
- }
- isAnimating = true
- }
- }
- .onAppear(perform: configureView)
- }
- }
- }
- /// A progress bar that shows the user's progress through the onboarding process.
- struct OnboardingProgressBar: View {
- let currentStep: OnboardingStep
- let currentSubstep: Int?
- let stepsWithSubsteps: [OnboardingStep: Int]
- let nightscoutSetupOption: NightscoutSetupOption
- var body: some View {
- HStack(spacing: 4) {
- ForEach(renderedSteps, id: \.id) { step in
- ZStack(alignment: .leading) {
- Rectangle()
- .fill(Color.gray.opacity(0.3))
- .frame(height: 4)
- .cornerRadius(2)
- GeometryReader { geo in
- Rectangle()
- .fill(Color.blue)
- .frame(
- width: geo.size.width * fillFraction(for: step.step, totalSubsteps: step.substeps),
- height: 4
- )
- .cornerRadius(2)
- }
- }
- .frame(height: 4)
- }
- }
- .padding(.horizontal)
- }
- private var renderedSteps: [(id: String, step: OnboardingStep, substeps: Int?)] {
- nonInfoOnboardingSteps.map {
- (id: "\($0.rawValue)", step: $0, substeps: stepsWithSubsteps[$0])
- }
- }
- private func fillFraction(for step: OnboardingStep, totalSubsteps: Int?) -> CGFloat {
- // If currentStep is .completed, fill everything
- if currentStep == .completed { return 1.0 }
- if let currentIndex = nonInfoOnboardingSteps.firstIndex(of: currentStep),
- let stepIndex = nonInfoOnboardingSteps.firstIndex(of: step),
- stepIndex < currentIndex
- {
- return 1.0
- }
- if step == currentStep {
- if let total = totalSubsteps, let current = currentSubstep {
- return CGFloat(current + 1) / CGFloat(total)
- } else {
- return 1.0
- }
- }
- // Handle special case: Nightscout was skipped
- if step == .nightscout,
- nightscoutSetupOption == .skipNightscoutSetup,
- let currentIndex = nonInfoOnboardingSteps.firstIndex(of: currentStep),
- let nightscoutIndex = nonInfoOnboardingSteps.firstIndex(of: .nightscout),
- currentIndex > nightscoutIndex
- {
- return 1.0
- }
- return 0.0
- }
- }
- struct OnboardingStepContent: View {
- @Binding var currentStep: OnboardingStep
- @Binding var currentNightscoutSubstep: NightscoutSubstep
- @Binding var currentDeliverySubstep: DeliveryLimitSubstep
- @Binding var currentAutosensSubstep: AutosensSettingsSubstep
- @Binding var currentSMBSubstep: SMBSettingsSubstep
- @Binding var currentTargetBehaviorSubstep: TargetBehaviorSubstep
- @Bindable var state: Onboarding.StateModel
- var navigationDirection: OnboardingNavigationDirection
- var body: some View {
- ScrollViewReader { scrollProxy in
- ScrollView(.vertical, showsIndicators: true) {
- VStack(alignment: .leading, spacing: 20) {
- Color.clear.frame(height: 0).id("top")
- if currentStep != .welcome && currentStep != .completed {
- HStack {
- if currentStep == .nightscout {
- Image(currentStep.iconName)
- .resizable()
- .scaledToFit()
- .frame(width: 60, height: 60)
- } else {
- 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(.title)
- .fontWeight(.bold)
- .foregroundColor(.primary)
- Text(currentStep.description)
- .font(.subheadline)
- .foregroundColor(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- }
- }
- .padding([.horizontal, .top])
- }
- Group {
- switch currentStep {
- case .welcome:
- WelcomeStepView()
- case .startupGuide:
- StartupGuideStepView()
- case .overview:
- OverviewStepView()
- case .diagnostics:
- DiagnosticsStepView(state: state)
- case .nightscout:
- switch currentNightscoutSubstep {
- case .setupSelection:
- NightscoutSetupStepView(state: state)
- case .connectToNightscout:
- NightscoutLoginStepView(state: state)
- case .importFromNightscout:
- NightscoutImportStepView(state: state)
- }
- case .unitSelection:
- UnitSelectionStepView(state: state)
- case .glucoseTarget:
- GlucoseTargetStepView(state: state)
- case .basalRates:
- BasalProfileStepView(state: state)
- case .carbRatio:
- CarbRatioStepView(state: state)
- case .insulinSensitivity:
- InsulinSensitivityStepView(state: state)
- case .deliveryLimits:
- DeliveryLimitsStepView(state: state, substep: currentDeliverySubstep)
- case .algorithmSettings:
- AlgorithmSettingsStepView(state: state)
- case .autosensSettings:
- AlgorithmSubstepView(state: state, substep: currentAutosensSubstep)
- case .smbSettings:
- AlgorithmSubstepView(state: state, substep: currentSMBSubstep)
- case .targetBehavior:
- AlgorithmSubstepView(state: state, substep: currentTargetBehaviorSubstep)
- case .completed:
- CompletedStepView()
- }
- }
- .transition(
- navigationDirection == .forward
- ? .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
- : .asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
- )
- .padding(.horizontal)
- .id(currentStep.id)
- }
- .padding(.bottom, 80)
- }
- .onChange(of: currentStep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
- .onChange(of: currentNightscoutSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
- .onChange(of: currentDeliverySubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
- }
- }
- }
- struct OnboardingNavigationButtons: View {
- @Binding var currentStep: OnboardingStep
- @Binding var currentNightscoutSubstep: NightscoutSubstep
- @Binding var currentDeliverySubstep: DeliveryLimitSubstep
- @Binding var currentAutosensSubstep: AutosensSettingsSubstep
- @Binding var currentSMBSubstep: SMBSettingsSubstep
- @Binding var currentTargetBehaviorSubstep: TargetBehaviorSubstep
- let onboardingManager: OnboardingManager
- @Bindable var state: Onboarding.StateModel
- var shouldDisableNextButton: Bool
- var navigationDirectionChanged: (OnboardingNavigationDirection) -> Void
- var body: some View {
- HStack {
- if currentStep != .welcome {
- Button(action: {
- navigationDirectionChanged(.backward)
- withAnimation {
- handleBackNavigation()
- }
- }) {
- HStack {
- Image(systemName: "chevron.left")
- Text("Back")
- }
- .padding()
- .foregroundColor(.primary)
- }
- }
- Spacer()
- Button(action: {
- navigationDirectionChanged(.forward)
- withAnimation {
- handleNextNavigation()
- }
- }) {
- HStack {
- Text(currentStep == .completed ? "Get Started" : "Next")
- Image(systemName: "chevron.right")
- }
- .padding()
- .foregroundColor(.white)
- .background(Capsule().fill(!shouldDisableNextButton ? Color.blue : Color(.systemGray)))
- }
- .disabled(shouldDisableNextButton)
- }
- .padding(.horizontal)
- .padding(.bottom)
- }
- // MARK: - Navigation Logic
- private func handleBackNavigation() {
- switch currentStep {
- case .completed:
- currentStep = .targetBehavior
- currentTargetBehaviorSubstep = .halfBasalTarget
- case .nightscout:
- if currentNightscoutSubstep == .setupSelection,
- let previous = currentStep.previous
- {
- currentStep = previous
- currentNightscoutSubstep = .setupSelection
- } else {
- currentNightscoutSubstep = NightscoutSubstep(rawValue: currentNightscoutSubstep.rawValue - 1)!
- }
- case .deliveryLimits:
- if let previousSub = DeliveryLimitSubstep(rawValue: currentDeliverySubstep.rawValue - 1) {
- currentDeliverySubstep = previousSub
- } else if let previous = currentStep.previous {
- currentStep = previous
- currentDeliverySubstep = .minimumSafetyThreshold
- }
- case .algorithmSettings:
- if let previous = currentStep.previous {
- currentStep = previous
- currentDeliverySubstep = .minimumSafetyThreshold
- currentAutosensSubstep = .autosensMin
- }
- case .autosensSettings:
- if let previous = AutosensSettingsSubstep(rawValue: currentAutosensSubstep.rawValue - 1) {
- currentAutosensSubstep = previous
- } else if let previousStep = currentStep.previous {
- currentStep = previousStep
- currentAutosensSubstep = .autosensMin
- }
- case .smbSettings:
- if let previous = SMBSettingsSubstep(rawValue: currentSMBSubstep.rawValue - 1) {
- currentSMBSubstep = previous
- } else if let previousStep = currentStep.previous {
- currentStep = previousStep
- currentSMBSubstep = .enableSMBAlways
- currentAutosensSubstep = .rewindResetsAutosens
- }
- case .targetBehavior:
- if let previous = TargetBehaviorSubstep(rawValue: currentTargetBehaviorSubstep.rawValue - 1) {
- currentTargetBehaviorSubstep = previous
- } else if let previousStep = currentStep.previous {
- currentStep = previousStep
- currentTargetBehaviorSubstep = .highTempTargetRaisesSensitivity
- currentSMBSubstep = .maxDeltaGlucoseThreshold
- }
- default:
- if let previous = currentStep.previous {
- currentStep = previous
- }
- }
- }
- private func handleNextNavigation() {
- switch currentStep {
- case .completed:
- state.saveOnboardingData()
- onboardingManager.completeOnboarding()
- Foundation.NotificationCenter.default.post(name: .onboardingCompleted, object: nil)
- case .nightscout:
- if currentNightscoutSubstep != .importFromNightscout {
- if currentNightscoutSubstep == .setupSelection,
- state.nightscoutSetupOption == .skipNightscoutSetup,
- let next = currentStep.next
- {
- currentStep = next
- } else {
- currentNightscoutSubstep = NightscoutSubstep(rawValue: currentNightscoutSubstep.rawValue + 1)!
- }
- } else if currentNightscoutSubstep == .importFromNightscout,
- state.nightscoutImportOption == .useImport
- {
- Task {
- await state.importSettingsFromNightscout(currentStep: $currentStep)
- }
- } else if let next = currentStep.next {
- currentStep = next
- }
- case .deliveryLimits:
- if let next = DeliveryLimitSubstep(rawValue: currentDeliverySubstep.rawValue + 1) {
- currentDeliverySubstep = next
- } else if let nextStep = currentStep.next {
- currentStep = nextStep
- currentDeliverySubstep = .maxIOB
- }
- case .autosensSettings:
- if let next = AutosensSettingsSubstep(rawValue: currentAutosensSubstep.rawValue + 1) {
- currentAutosensSubstep = next
- } else if let nextStep = currentStep.next {
- currentStep = nextStep
- currentAutosensSubstep = .autosensMin
- }
- case .smbSettings:
- if let next = SMBSettingsSubstep(rawValue: currentSMBSubstep.rawValue + 1) {
- currentSMBSubstep = next
- } else if let nextStep = currentStep.next {
- currentStep = nextStep
- currentSMBSubstep = .enableSMBAlways
- }
- case .targetBehavior:
- if let next = TargetBehaviorSubstep(rawValue: currentTargetBehaviorSubstep.rawValue + 1) {
- currentTargetBehaviorSubstep = next
- } else if let nextStep = currentStep.next {
- currentStep = nextStep
- currentTargetBehaviorSubstep = .highTempTargetRaisesSensitivity
- }
- default:
- if let next = currentStep.next {
- currentStep = next
- }
- }
- }
- }
- struct Onboarding_Preview: PreviewProvider {
- static var previews: some View {
- Group {
- let resolver = TrioApp.resolver
- let onboardingManager = OnboardingManager()
- Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager)
- .previewDisplayName("Onboarding Flow")
- }
- }
- }
|