OnboardingContainerView.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. // LoopFollow
  2. // OnboardingContainerView.swift
  3. import SwiftUI
  4. /// Root of the first-run onboarding wizard. Owns the shared chrome — progress
  5. /// bar and Back/Next footer — and swaps in the view for the current step.
  6. struct OnboardingContainerView: View {
  7. @StateObject private var viewModel: OnboardingViewModel
  8. @Environment(\.accessibilityReduceMotion) private var reduceMotion
  9. init(onClose: @escaping () -> Void) {
  10. _viewModel = StateObject(wrappedValue: OnboardingViewModel(onClose: onClose))
  11. }
  12. var body: some View {
  13. VStack(spacing: 0) {
  14. if viewModel.step.showsProgressHeader {
  15. header
  16. }
  17. stepContent
  18. .frame(maxWidth: .infinity, maxHeight: .infinity)
  19. if viewModel.step.usesSharedFooter {
  20. footer
  21. }
  22. }
  23. .background(Color(.systemGroupedBackground).ignoresSafeArea())
  24. .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
  25. }
  26. // MARK: - Chrome
  27. private var header: some View {
  28. VStack(spacing: 6) {
  29. HStack(spacing: 12) {
  30. OnboardingProgressBar(progress: viewModel.progress)
  31. Button("Skip") { viewModel.skip() }
  32. .font(.subheadline)
  33. .foregroundColor(.secondary)
  34. }
  35. if !viewModel.progressLabel.isEmpty {
  36. Text(viewModel.progressLabel)
  37. .font(.caption)
  38. .foregroundColor(.secondary)
  39. .frame(maxWidth: .infinity, alignment: .leading)
  40. }
  41. }
  42. .padding(.horizontal)
  43. .padding(.top, 12)
  44. .padding(.bottom, 4)
  45. }
  46. @ViewBuilder
  47. private var stepContent: some View {
  48. let content = Group {
  49. switch viewModel.step {
  50. case .welcome:
  51. WelcomeStepView(viewModel: viewModel)
  52. case .overview:
  53. OverviewStepView(onboarding: viewModel)
  54. case .dataSource:
  55. DataSourceChoiceStepView(viewModel: viewModel)
  56. case .connect:
  57. switch viewModel.dataSource {
  58. case .dexcom:
  59. DexcomConnectStepView(viewModel: viewModel.dexcomViewModel, onboarding: viewModel)
  60. case .copyFromPhone:
  61. ConnectImportStepView(viewModel: viewModel)
  62. default:
  63. NightscoutConnectStepView(viewModel: viewModel.nightscoutViewModel, onboarding: viewModel)
  64. }
  65. case .units:
  66. UnitsStepView(onboarding: viewModel)
  67. case .generalAlarms:
  68. GeneralAlarmsStepView()
  69. case .alarms:
  70. AlarmsStepView(viewModel: viewModel)
  71. case .tabOrder:
  72. TabOrderStepView()
  73. case .notifications:
  74. NotificationsStepView(viewModel: viewModel)
  75. case .telemetry:
  76. TelemetryStepView(viewModel: viewModel)
  77. case .completion:
  78. CompletionStepView(viewModel: viewModel)
  79. }
  80. }
  81. if reduceMotion {
  82. content.id(viewModel.step)
  83. } else {
  84. content
  85. .id(viewModel.step)
  86. .transition(.asymmetric(
  87. insertion: .move(edge: .trailing).combined(with: .opacity),
  88. removal: .move(edge: .leading).combined(with: .opacity)
  89. ))
  90. }
  91. }
  92. private var footer: some View {
  93. OnboardingNavFooter(
  94. continueEnabled: viewModel.canProceed,
  95. showBack: viewModel.canGoBack,
  96. onBack: { withStepAnimation { viewModel.goBack() } },
  97. onContinue: { withStepAnimation { viewModel.advance() } }
  98. )
  99. }
  100. private func withStepAnimation(_ change: () -> Void) {
  101. if reduceMotion {
  102. change()
  103. } else {
  104. withAnimation(.easeInOut(duration: 0.3)) { change() }
  105. }
  106. }
  107. }
  108. /// Thin segmented progress indicator shown at the top of each chrome'd step.
  109. private struct OnboardingProgressBar: View {
  110. let progress: Double
  111. var body: some View {
  112. GeometryReader { geo in
  113. ZStack(alignment: .leading) {
  114. Capsule()
  115. .fill(Color(.systemGray5))
  116. Capsule()
  117. .fill(Color.accentColor)
  118. .frame(width: max(0, min(1, progress)) * geo.size.width)
  119. }
  120. }
  121. .frame(height: 6)
  122. }
  123. }