TrioMainWatchView.swift 8.5 KB

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