| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- 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()
- 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
- )
- .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
- if currentStep != .welcome {
- 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, .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
- // }
- // Step-specific content
- Group {
- switch currentStep {
- case .welcome:
- WelcomeStepView()
- case .unitSelection:
- UnitSelectionStepView(state: state)
- case .glucoseTarget:
- GlucoseTargetStepView(state: state)
- case .basalProfile:
- BasalProfileStepView(state: state)
- case .carbRatio:
- CarbRatioStepView(state: state)
- case .insulinSensitivity:
- InsulinSensitivityStepView(state: state)
- case .deliveryLimits:
- DeliveryLimitsStepView(state: state)
- case .completed:
- CompletedStepView()
- }
- }
- .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()
- // 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)
- }
- }
- Spacer()
- // Next/Finish button
- Button(action: {
- withAnimation {
- if currentStep == .completed {
- // Apply settings and complete onboarding
- state.applyToSettings()
- onboardingManager.completeOnboarding()
- Foundation.NotificationCenter.default.post(name: .onboardingCompleted, object: nil)
- } 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)
- .fill(Color.blue)
- )
- }
- }
- .padding(.horizontal)
- .padding(.bottom)
- }
- }
- .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: Int
- let totalSteps: Int
- 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)
- }
- }
- .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)
- // }
- // }
- // }
- // }
- 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")
- }
- }
- }
|