TrioMainWatchView.swift 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import Charts
  2. import SwiftUI
  3. class NavigationState: ObservableObject {
  4. @Published var path = NavigationPath() // Tracks the navigation stack
  5. func resetToRoot() {
  6. path.removeLast(path.count) // Clears the navigation stack to return to root
  7. }
  8. }
  9. enum NavigationDestinations: String {
  10. case carbInput
  11. case bolusInput
  12. case bolusConfirm
  13. }
  14. struct TrioMainWatchView: View {
  15. @StateObject private var navigationState = NavigationState() // Shared navigation state
  16. @State private var state = WatchState()
  17. // misc
  18. @State private var currentPage: Int = 0
  19. @State private var rotationDegrees: Double = 0.0
  20. @State private var showingTempTargetSheet = false
  21. // view visbility
  22. @State private var showingTreatmentMenuSheet: Bool = false
  23. @State private var showingOverrideSheet: Bool = false
  24. // navigation flag for meal bolus combo
  25. @State private var continueToBolus = false
  26. // treatments
  27. @State private var selectedTreatment: TreatmentOption?
  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. NavigationStack(path: $navigationState.path) {
  35. TabView(selection: $currentPage) {
  36. // Page 1: Current glucose trend in "BG bobble"
  37. GlucoseTrendView(state: state, rotationDegrees: rotationDegrees)
  38. .tag(0)
  39. // Page 2: Glucose chart
  40. GlucoseChartView(glucoseValues: state.glucoseValues)
  41. .tag(1)
  42. }
  43. .background(trioBackgroundColor)
  44. .tabViewStyle(.verticalPage)
  45. .digitalCrownRotation($currentPage.doubleBinding(), from: 0, through: 1, by: 1)
  46. .onChange(of: state.trend) { _, newTrend in
  47. withAnimation {
  48. updateRotation(for: newTrend)
  49. }
  50. }
  51. .onAppear {
  52. state.confirmationProgress = 0 // reset auth progress
  53. }
  54. .toolbar {
  55. ToolbarItem(placement: .topBarLeading) {
  56. HStack {
  57. Image(systemName: "syringe.fill")
  58. .foregroundStyle(.blue)
  59. Text(state.iob ?? "--")
  60. .foregroundStyle(.white)
  61. }.font(.caption)
  62. }
  63. ToolbarItem(placement: .topBarTrailing) {
  64. HStack {
  65. Text(state.cob ?? "--")
  66. .foregroundStyle(.white)
  67. Image(systemName: "fork.knife")
  68. .foregroundStyle(.orange)
  69. }.font(.caption)
  70. }
  71. ToolbarItemGroup(placement: .bottomBar) {
  72. Button {
  73. showingOverrideSheet = true
  74. } label: {
  75. Image(systemName: "clock.arrow.2.circlepath")
  76. .foregroundStyle(Color.primary, Color.purple)
  77. }
  78. Button {
  79. showingTreatmentMenuSheet.toggle()
  80. } label: {
  81. Image(systemName: "plus")
  82. .foregroundStyle(Color.bgDarkerDarkBlue)
  83. }
  84. .controlSize(.large)
  85. .buttonStyle(WatchOSButtonStyle())
  86. Button {
  87. showingTempTargetSheet = true
  88. } label: {
  89. Image(systemName: "target")
  90. .foregroundStyle(.green.opacity(0.75))
  91. }
  92. }
  93. }
  94. .sheet(isPresented: $showingTreatmentMenuSheet) {
  95. TreatmentMenuView(selectedTreatment: $selectedTreatment) {
  96. handleTreatmentSelection()
  97. }
  98. .onAppear {
  99. continueToBolus = false
  100. }
  101. }
  102. .sheet(isPresented: $showingOverrideSheet) {
  103. OverridePresetsView(
  104. overridePresets: state.overridePresets,
  105. state: state
  106. )
  107. }
  108. .sheet(isPresented: $showingTempTargetSheet) {
  109. TempTargetPresetsView(
  110. tempTargetPresets: state.tempTargetPresets,
  111. state: state
  112. )
  113. }
  114. .navigationDestination(for: NavigationDestinations.self) { destination in
  115. switch destination {
  116. case .carbInput:
  117. CarbsInputView(
  118. navigationState: navigationState,
  119. state: state,
  120. continueToBolus: selectedTreatment == .mealBolusCombo
  121. )
  122. case .bolusInput:
  123. BolusInputView(navigationState: navigationState, state: state)
  124. case .bolusConfirm:
  125. BolusConfirmationView(
  126. navigationState: navigationState,
  127. state: state,
  128. bolusAmount: $state.bolusAmount,
  129. confirmationProgress: $state.confirmationProgress
  130. )
  131. }
  132. }
  133. }
  134. }
  135. struct WatchOSButtonStyle: ButtonStyle {
  136. var backgroundGradient = LinearGradient(colors: [
  137. Color(red: 0.721, green: 0.341, blue: 1),
  138. Color(red: 0.486, green: 0.545, blue: 0.953),
  139. Color(red: 0.262, green: 0.733, blue: 0.914)
  140. ], startPoint: .topLeading, endPoint: .bottomTrailing)
  141. var foregroundColor: Color = .white
  142. var fontSize: Font = .title2
  143. private var is40mm: Bool {
  144. let size = WKInterfaceDevice.current().screenBounds.size
  145. return size.height < 225 && size.width < 185
  146. }
  147. func makeBody(configuration: Configuration) -> some View {
  148. configuration.label
  149. .font(fontSize)
  150. .fontWeight(is40mm ? .medium : .semibold)
  151. .padding(is40mm ? 6 : 10)
  152. .background(
  153. backgroundGradient.opacity(configuration.isPressed ? 0.8 : 1.0)
  154. )
  155. .clipShape(Circle())
  156. .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
  157. .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
  158. }
  159. }
  160. private func updateRotation(for trend: String?) {
  161. switch trend {
  162. case "DoubleUp",
  163. "SingleUp":
  164. rotationDegrees = -90
  165. case "FortyFiveUp":
  166. rotationDegrees = -45
  167. case "Flat":
  168. rotationDegrees = 0
  169. case "FortyFiveDown":
  170. rotationDegrees = 45
  171. case "DoubleDown",
  172. "SingleDown":
  173. rotationDegrees = 90
  174. default:
  175. rotationDegrees = 0
  176. }
  177. }
  178. private func handleTreatmentSelection() {
  179. showingTreatmentMenuSheet = false // Dismiss the sheet
  180. guard let treatment = selectedTreatment else { return }
  181. switch treatment {
  182. case .meal:
  183. navigationState.path.append(NavigationDestinations.carbInput)
  184. case .bolus:
  185. navigationState.path.append(NavigationDestinations.bolusInput)
  186. case .mealBolusCombo:
  187. navigationState.path.append(NavigationDestinations.carbInput)
  188. }
  189. }
  190. }
  191. extension Binding where Value == Int {
  192. func doubleBinding() -> Binding<Double> {
  193. Binding<Double>(
  194. get: { Double(self.wrappedValue) },
  195. set: { self.wrappedValue = Int($0) }
  196. )
  197. }
  198. }
  199. extension Color {
  200. static let bgDarkBlue = Color("Background_DarkBlue")
  201. static let bgDarkerDarkBlue = Color("Background_DarkerDarkBlue")
  202. }
  203. #Preview {
  204. TrioMainWatchView()
  205. }