TrioMainWatchView.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import Charts
  2. import SwiftUI
  3. struct TrioMainWatchView: View {
  4. @State private var state = WatchState()
  5. // misc
  6. @State private var currentPage: Int = 0
  7. @State private var rotationDegrees: Double = 0.0
  8. @State private var showingTempTargetSheet = false
  9. // view visbility
  10. @State private var showingTreatmentMenuSheet: Bool = false
  11. @State private var showingOverrideSheet: Bool = false
  12. // navigation flag for meal bolus combo
  13. @State private var continueToBolus = false
  14. @State private var navigationPath = NavigationPath()
  15. // treatments
  16. @State private var selectedTreatment: TreatmentOption?
  17. // Active adjustment indicator
  18. private func isAdjustmentActive<T>(for presets: [T], predicate: (T) -> Bool) -> Bool {
  19. let sortedPresets = presets.sorted { predicate($0) && !predicate($1) }
  20. return !sortedPresets.isEmpty && sortedPresets.first(where: predicate) != nil
  21. }
  22. private var isTempTargetActive: Bool {
  23. isAdjustmentActive(for: state.tempTargetPresets) { $0.isEnabled }
  24. }
  25. private var isOverrideActive: Bool {
  26. isAdjustmentActive(for: state.overridePresets) { $0.isEnabled }
  27. }
  28. private var trioBackgroundColor = LinearGradient(
  29. gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
  30. startPoint: .top,
  31. endPoint: .bottom
  32. )
  33. var body: some View {
  34. ZStack {
  35. if !state.showSyncingAnimation {
  36. mainTabView
  37. } else {
  38. trioBackgroundColor.ignoresSafeArea()
  39. VStack {
  40. ProgressView("Syncing...")
  41. Spacer()
  42. }
  43. }
  44. }
  45. }
  46. @ViewBuilder private var mainTabView: some View {
  47. NavigationStack(path: $navigationPath) {
  48. TabView(selection: $currentPage) {
  49. // Page 1: Current glucose trend in "BG bobble"
  50. GlucoseTrendView(state: state, rotationDegrees: rotationDegrees)
  51. .tag(0)
  52. // Page 2: Glucose chart
  53. GlucoseChartView(glucoseValues: state.glucoseValues)
  54. .tag(1)
  55. }
  56. .background(trioBackgroundColor)
  57. .tabViewStyle(.verticalPage)
  58. .digitalCrownRotation($currentPage.doubleBinding(), from: 0, through: 1, by: 1)
  59. .onChange(of: state.trend) { _, newTrend in
  60. withAnimation {
  61. updateRotation(for: newTrend)
  62. }
  63. }
  64. .toolbar {
  65. ToolbarItem(placement: .topBarLeading) {
  66. HStack {
  67. Image(systemName: "syringe.fill")
  68. .foregroundStyle(Color.insulin)
  69. Text(state.iob ?? "--")
  70. .foregroundStyle(.white)
  71. }.font(.caption)
  72. }
  73. ToolbarItem(placement: .topBarTrailing) {
  74. HStack {
  75. Text(state.cob ?? "--")
  76. .foregroundStyle(.white)
  77. Image(systemName: "fork.knife")
  78. .foregroundStyle(Color.orange)
  79. }.font(.caption)
  80. }
  81. ToolbarItemGroup(placement: .bottomBar) {
  82. Button {
  83. showingOverrideSheet = true
  84. } label: {
  85. Image(systemName: "clock.arrow.2.circlepath")
  86. .foregroundStyle(Color.primary, isOverrideActive ? Color.primary : Color.purple)
  87. }.tint(isOverrideActive ? Color.purple : nil)
  88. Button {
  89. showingTreatmentMenuSheet = true
  90. } label: {
  91. Image(systemName: "plus")
  92. .foregroundStyle(Color.bgDarkerDarkBlue)
  93. }
  94. .controlSize(.large)
  95. .buttonStyle(WatchOSButtonStyle())
  96. Button {
  97. showingTempTargetSheet = true
  98. } label: {
  99. Image(systemName: "target")
  100. .foregroundStyle(isTempTargetActive ? Color.primary : Color.loopGreen.opacity(0.75))
  101. }.tint(isTempTargetActive ? Color.loopGreen.opacity(0.75) : nil)
  102. }
  103. }
  104. .fullScreenCover(isPresented: $showingTreatmentMenuSheet) {
  105. TreatmentMenuView(selectedTreatment: $selectedTreatment) {
  106. handleTreatmentSelection()
  107. }
  108. .onAppear {
  109. // reset the conditional navigation flag when opening
  110. continueToBolus = false
  111. }
  112. }
  113. .sheet(isPresented: $showingOverrideSheet) {
  114. OverridePresetsView(
  115. state: state,
  116. overridePresets: state.overridePresets
  117. ) {
  118. showingOverrideSheet = false
  119. navigationPath.append(NavigationDestinations.acknowledgmentPending)
  120. }
  121. }
  122. .sheet(isPresented: $showingTempTargetSheet) {
  123. TempTargetPresetsView(
  124. state: state,
  125. tempTargetPresets: state.tempTargetPresets
  126. ) {
  127. showingTempTargetSheet = false
  128. navigationPath.append(NavigationDestinations.acknowledgmentPending)
  129. }
  130. }
  131. .navigationDestination(for: NavigationDestinations.self) { destination in
  132. switch destination {
  133. case .acknowledgmentPending:
  134. AcknowledgementPendingView(
  135. navigationPath: $navigationPath,
  136. state: state,
  137. shouldNavigateToRoot: $state.shouldNavigateToRoot
  138. )
  139. case .carbsInput:
  140. CarbsInputView(
  141. navigationPath: $navigationPath,
  142. state: state,
  143. continueToBolus: continueToBolus
  144. )
  145. case .bolusInput:
  146. BolusInputView(
  147. navigationPath: $navigationPath,
  148. state: state
  149. )
  150. case .bolusConfirm:
  151. BolusConfirmationView(
  152. navigationPath: $navigationPath,
  153. state: state,
  154. bolusAmount: $state.bolusAmount,
  155. confirmationProgress: $state.confirmationProgress
  156. )
  157. }
  158. }
  159. .onChange(of: navigationPath) { _, newPath in
  160. if newPath.isEmpty {
  161. // Reset conditional view navigation when returning to root view
  162. continueToBolus = false
  163. }
  164. }
  165. }
  166. .blur(radius: state.showBolusProgressOverlay ? 3 : 0)
  167. .overlay {
  168. if state.showBolusProgressOverlay {
  169. BolusProgressOverlay(state: state) {
  170. state.shouldNavigateToRoot = false
  171. navigationPath.append(NavigationDestinations.acknowledgmentPending)
  172. }.transition(.opacity)
  173. }
  174. }
  175. }
  176. private func updateRotation(for trend: String?) {
  177. switch trend {
  178. case "DoubleUp",
  179. "SingleUp":
  180. rotationDegrees = -90
  181. case "FortyFiveUp":
  182. rotationDegrees = -45
  183. case "Flat":
  184. rotationDegrees = 0
  185. case "FortyFiveDown":
  186. rotationDegrees = 45
  187. case "DoubleDown",
  188. "SingleDown":
  189. rotationDegrees = 90
  190. default:
  191. rotationDegrees = 0
  192. }
  193. }
  194. private func handleTreatmentSelection() {
  195. showingTreatmentMenuSheet = false // Dismiss the sheet
  196. guard let treatment = selectedTreatment else { return }
  197. switch treatment {
  198. case .meal:
  199. navigationPath.append(NavigationDestinations.carbsInput)
  200. case .bolus:
  201. // Reset carbs amount when directly going to bolus input
  202. state.carbsAmount = 0
  203. navigationPath.append(NavigationDestinations.bolusInput)
  204. case .mealBolusCombo:
  205. continueToBolus = true // Explicitely set subsequent view navigation
  206. navigationPath.append(NavigationDestinations.carbsInput)
  207. }
  208. }
  209. }
  210. #Preview {
  211. TrioMainWatchView()
  212. }