| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- import Charts
- import SwiftUI
- import WatchKit
- struct TrioMainWatchView: View {
- @State private var state = WatchState()
- // misc
- @State private var currentPage: Int = 0
- @State private var rotationDegrees: Double = 0.0
- @State private var showingTempTargetSheet = false
- // view visbility
- @State private var showingTreatmentMenuSheet: Bool = false
- @State private var showingOverrideSheet: Bool = false
- // navigation flag for meal bolus combo
- @State private var continueToBolus = false
- @State private var navigationPath = NavigationPath()
- // treatments
- @State private var selectedTreatment: TreatmentOption?
- var isWatchStateDated: Bool {
- // If `lastWatchStateUpdate` is nil, treat as "dated"
- guard let lastUpdateTimestamp = state.lastWatchStateUpdate else {
- return true
- }
- let now = Date().timeIntervalSince1970
- let secondsSinceUpdate = now - lastUpdateTimestamp
- // Return true if last update older than 5 min, so 1 loop cycle
- return secondsSinceUpdate > 5 * 60
- }
- var isSessionUnreachable: Bool {
- guard let session = state.session else {
- return true // No session at all => unreachable
- }
- // Return true if not .activated OR not reachable
- return session.activationState != .activated
- }
- // Active adjustment indicator
- private func isAdjustmentActive<T>(for presets: [T], predicate: (T) -> Bool) -> Bool {
- let sortedPresets = presets.sorted { predicate($0) && !predicate($1) }
- return !sortedPresets.isEmpty && sortedPresets.first(where: predicate) != nil
- }
- private var isTempTargetActive: Bool {
- isAdjustmentActive(for: state.tempTargetPresets) { $0.isEnabled }
- }
- private var isOverrideActive: Bool {
- isAdjustmentActive(for: state.overridePresets) { $0.isEnabled }
- }
- private var trioBackgroundColor = LinearGradient(
- gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
- startPoint: .top,
- endPoint: .bottom
- )
- var body: some View {
- NavigationStack(path: $navigationPath) {
- TabView(selection: $currentPage) {
- // Page 1: Current glucose trend in "BG bobble"
- ZStack {
- GlucoseTrendView(
- state: state,
- rotationDegrees: rotationDegrees,
- isWatchStateDated: isWatchStateDated || isSessionUnreachable
- )
- if state.showSyncingAnimation {
- Image(systemName: "iphone.radiowaves.left.and.right")
- .symbolRenderingMode(.palette)
- .foregroundStyle(Color.primary, Color.tabBar, Color.clear)
- .symbolEffect(
- .variableColor.iterative,
- options: .repeating,
- value: state.showSyncingAnimation
- )
- .position(
- x: 20,
- y: (WKInterfaceDevice.current().screenBounds.height / 4) -
- 7 // Font .body == 14, so half of default size for the SF Symbol image
- )
- }
- }.tag(0)
- // Page 2: Glucose chart
- GlucoseChartView(glucoseValues: state.glucoseValues)
- .tag(1)
- }
- .onAppear {
- // Hard reset variables when main view appears
- /// Reset `bolusProgress` and `activeBolusAmount` to ensure no stale bolus progressbar is stuck on home view
- state.bolusProgress = 0
- state.activeBolusAmount = 0
- /// Reset `bolusAmount` and `recommendedBolus` to ensure no stale / old value is set when user opens bolus input or meal combo the next time.
- state.bolusAmount = 0
- state.recommendedBolus = 0
- }
- .background(trioBackgroundColor)
- .tabViewStyle(.verticalPage)
- .digitalCrownRotation($currentPage.doubleBinding(), from: 0, through: 1, by: 1)
- .onChange(of: state.trend) { _, newTrend in
- withAnimation {
- updateRotation(for: newTrend)
- }
- }
- .toolbar {
- ToolbarItem(placement: .topBarLeading) {
- HStack {
- Image(systemName: "syringe.fill")
- .foregroundStyle(Color.insulin)
- Text(isWatchStateDated || isSessionUnreachable ? "--" : state.iob ?? "--")
- .foregroundStyle(isWatchStateDated ? Color.secondary : Color.white)
- }.font(.caption2)
- }
- ToolbarItem(placement: .topBarTrailing) {
- HStack {
- Text(isWatchStateDated || isSessionUnreachable ? "--" : state.cob ?? "--")
- .foregroundStyle(isWatchStateDated || isSessionUnreachable ? Color.secondary : Color.white)
- Image(systemName: "fork.knife")
- .foregroundStyle(Color.orange)
- }.font(.caption2)
- }
- ToolbarItemGroup(placement: .bottomBar) {
- Button {
- showingOverrideSheet = true
- } label: {
- Image(systemName: "clock.arrow.2.circlepath")
- .foregroundStyle(Color.primary, isOverrideActive ? Color.primary : Color.purple)
- }.tint(isOverrideActive ? Color.purple : nil)
- Button {
- showingTreatmentMenuSheet = true
- } label: {
- Image(systemName: "plus")
- .foregroundStyle(Color.bgDarkerDarkBlue)
- }
- .controlSize(.large)
- .buttonStyle(WatchOSButtonStyle(deviceType: state.deviceType))
- Button {
- showingTempTargetSheet = true
- } label: {
- Image(systemName: "target")
- .foregroundStyle(isTempTargetActive ? Color.primary : Color.loopGreen.opacity(0.75))
- }.tint(isTempTargetActive ? Color.loopGreen.opacity(0.75) : nil)
- }
- }
- .fullScreenCover(isPresented: $showingTreatmentMenuSheet) {
- TreatmentMenuView(deviceType: state.deviceType, selectedTreatment: $selectedTreatment) {
- handleTreatmentSelection()
- }
- .onAppear {
- // reset the conditional navigation flag when opening
- continueToBolus = false
- }
- }
- .sheet(isPresented: $showingOverrideSheet) {
- OverridePresetsView(
- state: state,
- overridePresets: state.overridePresets
- ) {
- showingOverrideSheet = false
- navigationPath.append(NavigationDestinations.acknowledgmentPending)
- }
- }
- .sheet(isPresented: $showingTempTargetSheet) {
- TempTargetPresetsView(
- state: state,
- tempTargetPresets: state.tempTargetPresets
- ) {
- showingTempTargetSheet = false
- navigationPath.append(NavigationDestinations.acknowledgmentPending)
- }
- }
- .navigationDestination(for: NavigationDestinations.self) { destination in
- switch destination {
- case .acknowledgmentPending:
- AcknowledgementPendingView(
- navigationPath: $navigationPath,
- state: state,
- shouldNavigateToRoot: $state.shouldNavigateToRoot
- )
- case .carbsInput:
- CarbsInputView(
- navigationPath: $navigationPath,
- state: state,
- continueToBolus: continueToBolus
- )
- case .bolusInput:
- BolusInputView(
- navigationPath: $navigationPath,
- state: state
- )
- case .bolusConfirm:
- BolusConfirmationView(
- navigationPath: $navigationPath,
- state: state,
- bolusAmount: $state.bolusAmount,
- confirmationProgress: $state.confirmationProgress
- )
- }
- }
- .onChange(of: navigationPath) { _, newPath in
- if newPath.isEmpty {
- // Reset conditional view navigation when returning to root view
- continueToBolus = false
- }
- }
- }
- .ignoresSafeArea()
- .blur(radius: state.showBolusProgressOverlay ? 3 : 0)
- .overlay {
- if state.showBolusProgressOverlay {
- BolusProgressOverlay(state: state) {
- state.shouldNavigateToRoot = false
- navigationPath.append(NavigationDestinations.acknowledgmentPending)
- }.transition(.opacity)
- }
- }
- }
- private func updateRotation(for trend: String?) {
- switch trend {
- case "DoubleUp",
- "SingleUp":
- rotationDegrees = -90
- case "FortyFiveUp":
- rotationDegrees = -45
- case "Flat":
- rotationDegrees = 0
- case "FortyFiveDown":
- rotationDegrees = 45
- case "DoubleDown",
- "SingleDown":
- rotationDegrees = 90
- default:
- rotationDegrees = 0
- }
- }
- private func handleTreatmentSelection() {
- showingTreatmentMenuSheet = false // Dismiss the sheet
- guard let treatment = selectedTreatment else { return }
- switch treatment {
- case .meal:
- navigationPath.append(NavigationDestinations.carbsInput)
- case .bolus:
- // Reset carbs amount when directly going to bolus input
- state.carbsAmount = 0
- navigationPath.append(NavigationDestinations.bolusInput)
- case .mealBolusCombo:
- continueToBolus = true // Explicitely set subsequent view navigation
- navigationPath.append(NavigationDestinations.carbsInput)
- }
- }
- }
- #Preview {
- TrioMainWatchView()
- }
|