TrioMainWatchView.swift 8.4 KB

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