CGMRootView.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import LoopKitUI
  2. import SwiftUI
  3. import Swinject
  4. extension CGMSettings {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. let displayClose: Bool
  8. let bluetoothManager: BluetoothStateManager
  9. @StateObject var state = StateModel()
  10. @State private var shouldDisplayHint: Bool = false
  11. @State var hintDetent = PresentationDetent.large
  12. @State var selectedVerboseHint: AnyView?
  13. @State var hintLabel: String?
  14. @State private var decimalPlaceholder: Decimal = 0.0
  15. @State private var booleanPlaceholder: Bool = false
  16. @State var showCGMSelection: Bool = false
  17. @Environment(\.colorScheme) var colorScheme
  18. @Environment(AppState.self) var appState
  19. var cgmSelectionButtons: some View {
  20. ForEach(cgmOptions, id: \.name) { option in
  21. if let cgm = state.listOfCGM.first(where: option.predicate) {
  22. Button(option.name) {
  23. state.addCGM(cgm: cgm)
  24. }
  25. }
  26. }
  27. }
  28. var body: some View {
  29. NavigationView {
  30. Form {
  31. Section(
  32. header: Text("CGM Integration to Trio"),
  33. content: {
  34. if bluetoothManager.bluetoothAuthorization != .authorized {
  35. HStack {
  36. Spacer()
  37. BluetoothRequiredView()
  38. Spacer()
  39. }
  40. } else {
  41. let cgmState = state.cgmCurrent
  42. if cgmState.type != .none {
  43. Button {
  44. state.shouldDisplayCGMSetupSheet = true
  45. } label: {
  46. HStack {
  47. Image(systemName: "sensor.tag.radiowaves.forward.fill")
  48. Text(cgmState.displayName)
  49. }
  50. .frame(maxWidth: .infinity, minHeight: 50, alignment: .center)
  51. .font(.title2)
  52. }.padding()
  53. } else {
  54. VStack {
  55. Button {
  56. showCGMSelection.toggle()
  57. } label: {
  58. Text("Add CGM")
  59. .font(.title3) }
  60. .frame(maxWidth: .infinity, alignment: .center)
  61. .buttonStyle(.bordered)
  62. HStack(alignment: .center) {
  63. Text(
  64. "Pair your CGM with Trio. See hint for compatible devices."
  65. )
  66. .font(.footnote)
  67. .foregroundColor(.secondary)
  68. .lineLimit(nil)
  69. Spacer()
  70. Button(
  71. action: {
  72. shouldDisplayHint.toggle()
  73. },
  74. label: {
  75. HStack {
  76. Image(systemName: "questionmark.circle")
  77. }
  78. }
  79. ).buttonStyle(BorderlessButtonStyle())
  80. }.padding(.top)
  81. }.padding(.vertical)
  82. }
  83. }
  84. }
  85. )
  86. .listRowBackground(Color.chart)
  87. if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
  88. Section {
  89. NavigationLink(
  90. destination: Calibrations.RootView(resolver: resolver),
  91. label: { Text("Libre Calibrations") }
  92. )
  93. }.listRowBackground(Color.chart)
  94. }
  95. SettingInputSection(
  96. decimalValue: $decimalPlaceholder,
  97. booleanValue: $state.smoothGlucose,
  98. shouldDisplayHint: $shouldDisplayHint,
  99. selectedVerboseHint: Binding(
  100. get: { selectedVerboseHint },
  101. set: {
  102. selectedVerboseHint = $0.map { AnyView($0) }
  103. hintLabel = String(localized: "Smooth Glucose Value")
  104. }
  105. ),
  106. units: state.units,
  107. type: .boolean,
  108. label: String(localized: "Smooth Glucose Value"),
  109. miniHint: String(localized: "Smooth CGM readings using exponential smoothing."),
  110. verboseHint: VStack(alignment: .leading, spacing: 10) {
  111. Text("Default: OFF").bold()
  112. Text(
  113. "This feature smooths your CGM readings to reduce noise and make them easier to read. It is based on a method used in AndroidAPS (AAPS). It uses two approaches: one that reacts quickly to recent changes, and one that looks at longer trends. These are combined to give a balanced result."
  114. )
  115. Text(
  116. "Trio will always display values based on your actual (raw) CGM readings. Smoothing does not change your real values or alerts."
  117. )
  118. Text("When this feature is enabled:")
  119. VStack(alignment: .leading) {
  120. Text(
  121. "• The main chart and treatment chart show a light gray trend line for the smoothed values. The glucose dots always show your original CGM readings."
  122. )
  123. Text("• In Trio history, you will see the smoothed value next to the original reading.")
  124. Text("• When you long-press a chart, the pop-up will show both the original and smoothed values.")
  125. }
  126. Text(
  127. "It can handle small gaps in data and ignores sensor error values. It needs at least 4 readings within 12 minutes to work properly. Only CGM readings are smoothed—manual entries are not changed."
  128. )
  129. Text(
  130. "This helps Trio make more stable dosing decisions by avoiding over-reactions to small or short-term changes. Important trends are kept, while unreliable fluctuations are filtered out."
  131. )
  132. }
  133. )
  134. }
  135. .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
  136. .onAppear(perform: configureView)
  137. .navigationTitle("CGM")
  138. .settingsHighlightScroll()
  139. .navigationBarTitleDisplayMode(.automatic)
  140. .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
  141. .sheet(isPresented: $state.shouldDisplayCGMSetupSheet) {
  142. switch state.cgmCurrent.type {
  143. case .enlite,
  144. .nightscout,
  145. .none,
  146. .simulator,
  147. .xdrip:
  148. CustomCGMOptionsView(
  149. resolver: self.resolver,
  150. state: state,
  151. cgmCurrent: state.cgmCurrent,
  152. deleteCGM: state.deleteCGM
  153. )
  154. case .plugin:
  155. if let fetchGlucoseManager = state.fetchGlucoseManager,
  156. let cgmManager = fetchGlucoseManager.cgmManager,
  157. state.cgmCurrent.type == fetchGlucoseManager.cgmGlucoseSourceType,
  158. state.cgmCurrent.id == fetchGlucoseManager.cgmGlucosePluginId
  159. {
  160. CGMSettingsView(
  161. cgmManager: cgmManager,
  162. bluetoothManager: state.provider.apsManager.bluetoothManager!,
  163. unit: state.settingsManager.settings.units,
  164. completionDelegate: state
  165. )
  166. } else {
  167. CGMSetupView(
  168. CGMType: state.cgmCurrent,
  169. bluetoothManager: state.provider.apsManager.bluetoothManager!,
  170. unit: state.settingsManager.settings.units,
  171. completionDelegate: state,
  172. setupDelegate: state,
  173. pluginCGMManager: self.state.pluginCGMManager
  174. ).onDisappear {
  175. if state.fetchGlucoseManager.cgmGlucoseSourceType == .none {
  176. state.cgmCurrent = cgmDefaultModel
  177. }
  178. }
  179. }
  180. }
  181. }
  182. .sheet(isPresented: $shouldDisplayHint) {
  183. SettingInputHintView(
  184. hintDetent: $hintDetent,
  185. shouldDisplayHint: $shouldDisplayHint,
  186. hintLabel: hintLabel ?? "",
  187. hintText: selectedVerboseHint ?? AnyView(
  188. VStack(alignment: .leading, spacing: 10) {
  189. Text(
  190. "Current CGM Models Supported:"
  191. )
  192. VStack(alignment: .leading) {
  193. Text("• Dexcom G5")
  194. Text("• Dexcom G6 / ONE")
  195. Text("• Dexcom G7 / ONE+")
  196. Text("• Dexcom Share")
  197. Text("• Freestyle Libre")
  198. Text("• Freestyle Libre Demo")
  199. Text("• Glucose Simulator")
  200. Text("• Medtronic Enlite")
  201. Text("• Nightscout")
  202. Text("• xDrip4iOS")
  203. }
  204. Text(
  205. "Note: The CGM Heartbeat can come from either a CGM or a pump to wake up Trio when phone is locked or in the background. If CGM is on the same phone as Trio and xDrip4iOS is configured to use the same AppGroup as Trio and the heartbeat feature is turned on in xDrip4iOS, then the CGM can provide a heartbeat to wake up Trio when phone is locked or app is in the background."
  206. )
  207. }
  208. ),
  209. sheetTitle: String(localized: "Help", comment: "Help sheet title")
  210. )
  211. }
  212. .confirmationDialog("CGM Model", isPresented: $showCGMSelection) {
  213. cgmSelectionButtons
  214. } message: {
  215. Text("Select CGM Model")
  216. }
  217. }
  218. }
  219. }
  220. }