OnboardingView.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import SwiftUI
  2. import Swinject
  3. /// The main onboarding view that manages navigation between onboarding steps.
  4. extension Onboarding {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. @State var state = StateModel()
  8. let onboardingManager: OnboardingManager
  9. @State private var currentStep: OnboardingStep = .welcome
  10. // Animation states
  11. @State private var animationScale: CGFloat = 1.0
  12. @State private var animationOpacity: Double = 0
  13. @State private var isAnimating = false
  14. var body: some View {
  15. NavigationView {
  16. ZStack {
  17. // Background gradient
  18. LinearGradient(
  19. gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
  20. startPoint: .top,
  21. endPoint: .bottom
  22. )
  23. .ignoresSafeArea()
  24. VStack(spacing: 0) {
  25. // Progress bar
  26. OnboardingProgressBar(
  27. currentStep: OnboardingStep.allCases.firstIndex(of: currentStep) ?? 0,
  28. totalSteps: OnboardingStep.allCases.count - 1
  29. )
  30. .padding(.top)
  31. // Step content
  32. ScrollView {
  33. VStack(alignment: .leading, spacing: 20) {
  34. // Header
  35. if currentStep != .welcome {
  36. HStack {
  37. Image(systemName: currentStep.iconName)
  38. .font(.system(size: 40))
  39. .foregroundColor(currentStep.accentColor)
  40. .frame(width: 60, height: 60)
  41. .background(
  42. Circle()
  43. .fill(currentStep.accentColor.opacity(0.2))
  44. )
  45. VStack(alignment: .leading) {
  46. Text(currentStep.title)
  47. .font(.largeTitle)
  48. .fontWeight(.bold)
  49. .foregroundColor(.primary)
  50. Text(currentStep.description)
  51. .font(.subheadline)
  52. .foregroundColor(.secondary)
  53. .fixedSize(horizontal: false, vertical: true)
  54. }
  55. }
  56. .padding([.horizontal, .top])
  57. }
  58. // Animation container (for steps that include animations)
  59. // AnimationPlaceholder(for: currentStep)
  60. // .padding()
  61. // .scaleEffect(animationScale)
  62. // .opacity(animationOpacity)
  63. // .onAppear {
  64. // withAnimation(.easeInOut(duration: 0.7)) {
  65. // animationOpacity = 1
  66. // animationScale = 1.0
  67. // }
  68. // // Start pulse animation
  69. // isAnimating = true
  70. // }
  71. // Step-specific content
  72. Group {
  73. switch currentStep {
  74. case .welcome:
  75. WelcomeStepView()
  76. case .unitSelection:
  77. UnitSelectionStepView(state: state)
  78. case .glucoseTarget:
  79. GlucoseTargetStepView(state: state)
  80. case .basalProfile:
  81. BasalProfileStepView(state: state)
  82. case .carbRatio:
  83. CarbRatioStepView(state: state)
  84. case .insulinSensitivity:
  85. InsulinSensitivityStepView(state: state)
  86. case .deliveryLimits:
  87. DeliveryLimitsStepView(state: state)
  88. case .completed:
  89. CompletedStepView()
  90. }
  91. }
  92. .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
  93. .padding(.horizontal)
  94. .id(currentStep.id) // Force view recreation when step changes
  95. }
  96. .padding(.bottom, 80) // Make room for buttons at bottom
  97. }
  98. Spacer()
  99. // Navigation buttons
  100. HStack {
  101. // Back button
  102. if currentStep != .welcome {
  103. Button(action: {
  104. withAnimation {
  105. if let previous = currentStep.previous {
  106. currentStep = previous
  107. }
  108. }
  109. }) {
  110. HStack {
  111. Image(systemName: "chevron.left")
  112. Text("Back")
  113. }
  114. .padding()
  115. .foregroundColor(.primary)
  116. }
  117. }
  118. Spacer()
  119. // Next/Finish button
  120. Button(action: {
  121. withAnimation {
  122. if currentStep == .completed {
  123. // Apply settings and complete onboarding
  124. state.applyToSettings()
  125. onboardingManager.completeOnboarding()
  126. Foundation.NotificationCenter.default.post(name: .onboardingCompleted, object: nil)
  127. } else if let next = currentStep.next {
  128. currentStep = next
  129. }
  130. }
  131. }) {
  132. HStack {
  133. Text(currentStep == .completed ? "Get Started" : "Next")
  134. Image(systemName: "chevron.right")
  135. }
  136. .padding()
  137. .foregroundColor(.white)
  138. .background(
  139. Capsule()
  140. // .fill(currentStep.accentColor)
  141. .fill(Color.blue)
  142. )
  143. }
  144. }
  145. .padding(.horizontal)
  146. .padding(.bottom)
  147. }
  148. }
  149. .navigationBarHidden(true)
  150. }
  151. .onChange(of: currentStep) { _, _ in
  152. // Reset animation when step changes
  153. animationScale = 0.9
  154. animationOpacity = 0
  155. isAnimating = false
  156. // Start new animation
  157. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  158. withAnimation(.easeInOut(duration: 0.7)) {
  159. animationOpacity = 1
  160. animationScale = 1.0
  161. }
  162. isAnimating = true
  163. }
  164. }
  165. .onAppear(perform: configureView)
  166. }
  167. }
  168. }
  169. /// A progress bar that shows the user's progress through the onboarding process.
  170. struct OnboardingProgressBar: View {
  171. let currentStep: Int
  172. let totalSteps: Int
  173. var body: some View {
  174. HStack(spacing: 4) {
  175. ForEach(0 ..< totalSteps, id: \.self) { step in
  176. Rectangle()
  177. .fill(step <= currentStep ? Color.blue : Color.gray.opacity(0.3))
  178. .frame(height: 4)
  179. .cornerRadius(2)
  180. }
  181. }
  182. .padding(.horizontal)
  183. }
  184. }
  185. ///// A simple animated placeholder for each step
  186. // struct AnimationPlaceholder: View {
  187. // let step: OnboardingStep
  188. // @State private var animationValue: Double = 0
  189. //
  190. // init(for step: OnboardingStep) {
  191. // self.step = step
  192. // }
  193. //
  194. // var body: some View {
  195. // VStack {
  196. // Group {
  197. // switch step {
  198. // case .welcome:
  199. // welcomeAnimation
  200. // case .glucoseTarget:
  201. // glucoseTargetAnimation
  202. // case .basalProfile:
  203. // basalProfileAnimation
  204. // case .carbRatio:
  205. // carbRatioAnimation
  206. // case .insulinSensitivity:
  207. // insulinSensitivityAnimation
  208. // case .completed:
  209. // completedAnimation
  210. // }
  211. // }
  212. // .frame(height: 180)
  213. // }
  214. // .onAppear {
  215. // withAnimation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
  216. // animationValue = 1.0
  217. // }
  218. // }
  219. // }
  220. //
  221. // // Custom animated views for each step
  222. // var welcomeAnimation: some View {
  223. // ZStack {
  224. // ForEach(0 ..< 5) { index in
  225. // Image(systemName: "heart.fill")
  226. // .font(.system(size: 40))
  227. // .foregroundColor(step.accentColor.opacity(0.8 - Double(index) * 0.15))
  228. // .offset(x: CGFloat.random(in: -100 ... 100), y: CGFloat.random(in: -60 ... 60))
  229. // .scaleEffect(1.0 + animationValue * 0.3)
  230. // .rotationEffect(.degrees(animationValue * Double.random(in: -30 ... 30)))
  231. // }
  232. //
  233. // Image(systemName: "syringe.fill")
  234. // .font(.system(size: 80))
  235. // .foregroundColor(step.accentColor)
  236. // .scaleEffect(1.0 + animationValue * 0.2)
  237. // .shadow(color: step.accentColor.opacity(0.5), radius: 10 * animationValue, x: 0, y: 0)
  238. // }
  239. // }
  240. //
  241. // var glucoseTargetAnimation: some View {
  242. // ZStack {
  243. // // Target rings
  244. // ForEach(0 ..< 3) { index in
  245. // Circle()
  246. // .stroke(step.accentColor.opacity(Double(3 - index) * 0.3), lineWidth: 8)
  247. // .frame(width: 120 + CGFloat(index * 40))
  248. // .scaleEffect(1.0 + animationValue * 0.05)
  249. // }
  250. //
  251. // // Arrow
  252. // Image(systemName: "arrow.down.to.line")
  253. // .font(.system(size: 50))
  254. // .foregroundColor(step.accentColor)
  255. // .offset(y: -10 + animationValue * 20)
  256. // .rotationEffect(.degrees(animationValue * 360))
  257. // }
  258. // }
  259. //
  260. // var basalProfileAnimation: some View {
  261. // ZStack {
  262. // // Line graph representation
  263. // Path { path in
  264. // let width: CGFloat = 300
  265. // let height: CGFloat = 100
  266. //
  267. // path.move(to: CGPoint(x: 0, y: height * 0.5))
  268. //
  269. // for i in 0 ..< 8 {
  270. // let x = width * CGFloat(i) / 7
  271. // let y = height * (0.5 + (sin(Double(i) * .pi / 3) * 0.4))
  272. //
  273. // if i == 0 {
  274. // path.move(to: CGPoint(x: x, y: y))
  275. // } else {
  276. // path.addLine(to: CGPoint(x: x, y: y))
  277. // }
  278. // }
  279. // }
  280. // .trim(from: 0, to: animationValue)
  281. // .stroke(step.accentColor, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
  282. // .frame(width: 300, height: 100)
  283. //
  284. // // Clock symbols to represent time
  285. // HStack(spacing: 50) {
  286. // Image(systemName: "clock")
  287. // .font(.system(size: 20))
  288. // .foregroundColor(step.accentColor)
  289. // .opacity(animationValue)
  290. //
  291. // Image(systemName: "clock.fill")
  292. // .font(.system(size: 20))
  293. // .foregroundColor(step.accentColor)
  294. // .opacity(animationValue)
  295. //
  296. // Image(systemName: "clock")
  297. // .font(.system(size: 20))
  298. // .foregroundColor(step.accentColor)
  299. // .opacity(animationValue)
  300. // }
  301. // .offset(y: 70)
  302. // }
  303. // }
  304. //
  305. // var carbRatioAnimation: some View {
  306. // ZStack {
  307. // // Plate
  308. // Circle()
  309. // .fill(Color.gray.opacity(0.1))
  310. // .frame(width: 150)
  311. //
  312. // // Food items
  313. // ForEach(0 ..< 5) { index in
  314. // Image(systemName: [
  315. // "carrot.fill",
  316. // "fork.knife",
  317. // "takeoutbag.and.cup.and.straw.fill",
  318. // "wallet.pass.fill",
  319. // "cup.and.saucer.fill"
  320. // ][index % 5])
  321. // .font(.system(size: 25))
  322. // .foregroundColor(step.accentColor)
  323. // .offset(
  324. // x: cos(Double(index) * .pi * 2 / 5) * 50 * animationValue,
  325. // y: sin(Double(index) * .pi * 2 / 5) * 50 * animationValue
  326. // )
  327. // .rotationEffect(.degrees(animationValue * 360))
  328. // }
  329. //
  330. // // Insulin
  331. // Image(systemName: "drop.fill")
  332. // .font(.system(size: 40))
  333. // .foregroundColor(.blue)
  334. // .scaleEffect(0.8 + animationValue * 0.3)
  335. // .shadow(color: .blue.opacity(0.5), radius: 5, x: 0, y: 0)
  336. // }
  337. // }
  338. //
  339. // var insulinSensitivityAnimation: some View {
  340. // ZStack {
  341. // // Glucose meter
  342. // RoundedRectangle(cornerRadius: 20)
  343. // .fill(Color.gray.opacity(0.1))
  344. // .frame(width: 120, height: 200)
  345. //
  346. // // Display screen
  347. // RoundedRectangle(cornerRadius: 10)
  348. // .fill(Color.black.opacity(0.1))
  349. // .frame(width: 100, height: 60)
  350. // .offset(y: -60)
  351. //
  352. // // Value on screen
  353. // Text("120")
  354. // .font(.system(size: 24, weight: .bold, design: .monospaced))
  355. // .foregroundColor(step.accentColor)
  356. // .offset(y: -60)
  357. // .opacity(animationValue)
  358. //
  359. // // Insulin drop
  360. // Image(systemName: "drop.fill")
  361. // .font(.system(size: 30))
  362. // .foregroundColor(.blue)
  363. // .offset(y: 20)
  364. // .opacity(1)
  365. //
  366. // // Arrow showing decrease
  367. // Image(systemName: "arrow.down")
  368. // .font(.system(size: 30))
  369. // .foregroundColor(step.accentColor)
  370. // .offset(y: 60)
  371. // .opacity(animationValue)
  372. // .scaleEffect(1.0 + animationValue * 0.5)
  373. //
  374. // // Lower value
  375. // Text("80")
  376. // .font(.system(size: 24, weight: .bold, design: .monospaced))
  377. // .foregroundColor(step.accentColor)
  378. // .offset(y: 100)
  379. // .opacity(animationValue)
  380. // }
  381. // }
  382. //
  383. // var completedAnimation: some View {
  384. // ZStack {
  385. // // Success checkmark
  386. // Circle()
  387. // .fill(step.accentColor.opacity(0.2))
  388. // .frame(width: 150)
  389. // .scaleEffect(animationValue)
  390. //
  391. // Circle()
  392. // .stroke(step.accentColor, lineWidth: 5)
  393. // .frame(width: 150)
  394. // .scaleEffect(animationValue)
  395. //
  396. // Image(systemName: "checkmark")
  397. // .font(.system(size: 80, weight: .bold))
  398. // .foregroundColor(step.accentColor)
  399. // .offset(y: animationValue * 5)
  400. // .scaleEffect(animationValue)
  401. //
  402. // // Celebrate particles
  403. // ForEach(0 ..< 8) { index in
  404. // Image(systemName: "star.fill")
  405. // .font(.system(size: 20))
  406. // .foregroundColor(step.accentColor)
  407. // .offset(
  408. // x: cos(Double(index) * .pi / 4) * 100 * animationValue,
  409. // y: sin(Double(index) * .pi / 4) * 100 * animationValue
  410. // )
  411. // .opacity(animationValue)
  412. // .scaleEffect(animationValue)
  413. // }
  414. // }
  415. // }
  416. // }
  417. struct Onboarding_Preview: PreviewProvider {
  418. static var previews: some View {
  419. Group {
  420. let resolver = TrioApp.resolver
  421. let onboardingManager = OnboardingManager()
  422. Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager)
  423. .previewDisplayName("Onboarding Flow")
  424. }
  425. }
  426. }