MockCGMManagerSettingsView.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. //
  2. // MockCGMManagerSettingsView.swift
  3. // MockKitUI
  4. //
  5. // Created by Nathaniel Hamming on 2023-05-18.
  6. // Copyright © 2023 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. import LoopKit
  10. import LoopKitUI
  11. import MockKit
  12. struct MockCGMManagerSettingsView: View {
  13. fileprivate enum PresentedAlert {
  14. case resumeInsulinDeliveryError(Error)
  15. case suspendInsulinDeliveryError(Error)
  16. }
  17. @Environment(\.dismissAction) private var dismiss
  18. @Environment(\.guidanceColors) private var guidanceColors
  19. @Environment(\.glucoseTintColor) private var glucoseTintColor
  20. @ObservedObject var viewModel: MockCGMManagerSettingsViewModel
  21. @State private var showSuspendOptions = false
  22. @State private var presentedAlert: PresentedAlert?
  23. private var displayGlucosePreference: DisplayGlucosePreference
  24. private let appName: String
  25. private let allowDebugFeatures : Bool
  26. private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
  27. init(cgmManager: MockCGMManager, displayGlucosePreference: DisplayGlucosePreference, appName: String, allowDebugFeatures: Bool) {
  28. viewModel = MockCGMManagerSettingsViewModel(cgmManager: cgmManager, displayGlucosePreference: displayGlucosePreference)
  29. self.displayGlucosePreference = displayGlucosePreference
  30. self.appName = appName
  31. self.allowDebugFeatures = allowDebugFeatures
  32. }
  33. var body: some View {
  34. List {
  35. statusSection
  36. sensorSection
  37. lastReadingSection
  38. supportSection
  39. }
  40. .insetGroupedListStyle()
  41. .navigationBarItems(trailing: doneButton)
  42. .navigationBarTitle(Text("CGM Simulator"), displayMode: .large)
  43. .alert(item: $presentedAlert, content: alert(for:))
  44. }
  45. @ViewBuilder
  46. private var statusSection: some View {
  47. statusCardSubSection
  48. notificationSubSection
  49. if (allowDebugFeatures) {
  50. settingsSubSection
  51. }
  52. }
  53. private var statusCardSubSection: some View {
  54. Section {
  55. VStack(spacing: 8) {
  56. sensorProgressView
  57. .openMockCGMSettingsOnLongPress(enabled: true, cgmManager: viewModel.cgmManager, displayGlucosePreference: displayGlucosePreference)
  58. Divider()
  59. lastReadingInfo
  60. }
  61. }
  62. }
  63. private var sensorProgressView: some View {
  64. HStack(alignment: .center, spacing: 16) {
  65. pumpImage
  66. expirationArea
  67. .offset(y: -3)
  68. }
  69. }
  70. private var pumpImage: some View {
  71. ZStack {
  72. RoundedRectangle(cornerRadius: 5)
  73. .fill(Color(frameworkColor: "LightGrey")!)
  74. .frame(width: 77, height: 76)
  75. Image(frameworkImage: "CGM Simulator")
  76. .resizable()
  77. .aspectRatio(contentMode: ContentMode.fit)
  78. .frame(maxHeight: 70)
  79. .frame(width: 70)
  80. }
  81. }
  82. private var expirationArea: some View {
  83. VStack(alignment: .leading) {
  84. expirationText
  85. .offset(y: 4)
  86. expirationTime
  87. .offset(y: 10)
  88. progressBar
  89. }
  90. }
  91. private var expirationText: some View {
  92. Text("Sensor expires in ")
  93. .font(.subheadline)
  94. .foregroundColor(.secondary)
  95. }
  96. private var expirationTime: some View {
  97. HStack(alignment: .lastTextBaseline) {
  98. Text("5")
  99. .font(.system(size: 24, weight: .heavy, design: .default))
  100. Text("days")
  101. .font(.system(size: 15, weight: .regular, design: .default))
  102. .foregroundColor(.secondary)
  103. .offset(x: -3)
  104. }
  105. }
  106. private var progressBar: some View {
  107. ProgressView(progress: viewModel.sensorExpirationPercentComplete)
  108. .accentColor(glucoseTintColor)
  109. }
  110. var lastReadingInfo: some View {
  111. HStack(alignment: .lastTextBaseline) {
  112. lastGlucoseReading
  113. .frame(idealWidth: 100)
  114. Spacer()
  115. lastReadingTime
  116. .onReceive(timer) { _ in
  117. // Update every second
  118. viewModel.updateLastReadingTime()
  119. }
  120. }
  121. }
  122. @ViewBuilder
  123. private var lastGlucoseReading: some View {
  124. VStack(alignment: .leading, spacing: 5) {
  125. Text("Last Reading")
  126. .foregroundColor(.secondary)
  127. HStack(alignment: .center, spacing: 16) {
  128. viewModel.lastGlucoseTrend?.filledImage
  129. .scaleEffect(1.7, anchor: .leading)
  130. .foregroundColor(glucoseTintColor)
  131. HStack(alignment: .firstTextBaseline, spacing: 4) {
  132. Text(viewModel.lastGlucoseValueFormatted)
  133. .font(.title)
  134. .fontWeight(.heavy)
  135. Text(viewModel.glucoseUnitString)
  136. .foregroundColor(.secondary)
  137. }
  138. }
  139. }
  140. }
  141. @ViewBuilder
  142. private var lastReadingTime: some View {
  143. HStack(alignment: .center, spacing: 16) {
  144. Image(systemName: "arrow.triangle.2.circlepath.circle.fill")
  145. .scaleEffect(1.7, anchor: .leading)
  146. .foregroundColor(glucoseTintColor)
  147. HStack(alignment: .firstTextBaseline, spacing: 4) {
  148. Text("\(viewModel.lastReadingMinutesFromNow)")
  149. .font(.title)
  150. .fontWeight(.heavy)
  151. Text("min")
  152. .foregroundColor(.secondary)
  153. }
  154. }
  155. .frame(height: 40.0)
  156. }
  157. private var notificationSubSection: some View {
  158. Section {
  159. NavigationLink(destination: DemoPlaceHolderView(appName: appName)) {
  160. Text("Notification Settings")
  161. }
  162. }
  163. }
  164. private var settingsSubSection: some View {
  165. Section {
  166. NavigationLink(destination: MockCGMManagerControlsView(cgmManager: viewModel.cgmManager, displayGlucosePreference: displayGlucosePreference)) {
  167. Text("Simulator Settings")
  168. }
  169. }
  170. }
  171. @ViewBuilder
  172. private var sensorSection: some View {
  173. deviceDetailsSubSection
  174. stopSensorSubSection
  175. }
  176. private var deviceDetailsSubSection: some View {
  177. Section(header: SectionHeader(label: "Sensor")) {
  178. LabeledValueView(label: "Insertion Time", value: viewModel.sensorInsertionDateTimeString)
  179. LabeledValueView(label: "Sensor Expires", value: viewModel.sensorExpirationDateTimeString)
  180. }
  181. }
  182. private var stopSensorSubSection: some View {
  183. Section {
  184. NavigationLink(destination: DemoPlaceHolderView(appName: appName)) {
  185. Text("Stop Sensor")
  186. .foregroundColor(guidanceColors.critical)
  187. }
  188. }
  189. }
  190. private var lastReadingSection: some View {
  191. Section(header: SectionHeader(label: "Last Reading")) {
  192. LabeledValueView(label: "Glucose", value: viewModel.lastGlucoseValueWithUnitFormatted)
  193. LabeledValueView(label: "Time", value: viewModel.lastGlucoseDateFormatted)
  194. LabeledValueView(label: "Trend", value: viewModel.lastGlucoseTrendFormatted)
  195. }
  196. }
  197. private var supportSection: some View {
  198. Section(header: SectionHeader(label: "Support")) {
  199. NavigationLink(destination: DemoPlaceHolderView(appName: appName)) {
  200. Text("Get help with your CGM")
  201. }
  202. }
  203. }
  204. private var doneButton: some View {
  205. Button(LocalizedString("Done", comment: "Settings done button label"), action: dismiss)
  206. }
  207. private func alert(for presentedAlert: PresentedAlert) -> SwiftUI.Alert {
  208. switch presentedAlert {
  209. case .suspendInsulinDeliveryError(let error):
  210. return Alert(
  211. title: Text("Failed to Suspend Insulin Delivery"),
  212. message: Text(error.localizedDescription)
  213. )
  214. case .resumeInsulinDeliveryError(let error):
  215. return Alert(
  216. title: Text("Failed to Resume Insulin Delivery"),
  217. message: Text(error.localizedDescription)
  218. )
  219. }
  220. }
  221. }
  222. extension MockCGMManagerSettingsView.PresentedAlert: Identifiable {
  223. var id: Int {
  224. switch self {
  225. case .resumeInsulinDeliveryError:
  226. return 0
  227. case .suspendInsulinDeliveryError:
  228. return 1
  229. }
  230. }
  231. }
  232. struct MockCGMManagerSettingsView_Previews: PreviewProvider {
  233. static var previews: some View {
  234. MockCGMManagerSettingsView(cgmManager: MockCGMManager(), displayGlucosePreference: DisplayGlucosePreference(displayGlucoseUnit: .milligramsPerDeciliter), appName: "Loop", allowDebugFeatures: false)
  235. }
  236. }