MockCGMManagerSettingsViewController.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  1. //
  2. // MockCGMManagerSettingsViewController.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 11/23/18.
  6. // Copyright © 2018 LoopKit Authors. All rights reserved.
  7. //
  8. import UIKit
  9. import Combine
  10. import HealthKit
  11. import LoopKit
  12. import LoopKitUI
  13. import LoopTestingKit
  14. import MockKit
  15. final class MockCGMManagerSettingsViewController: UITableViewController {
  16. let cgmManager: MockCGMManager
  17. private let displayGlucosePreference: DisplayGlucosePreference
  18. private lazy var cancellables = Set<AnyCancellable>()
  19. private var glucoseUnit: HKUnit {
  20. displayGlucosePreference.unit
  21. }
  22. init(cgmManager: MockCGMManager, displayGlucosePreference: DisplayGlucosePreference) {
  23. self.cgmManager = cgmManager
  24. self.displayGlucosePreference = displayGlucosePreference
  25. self.quantityFormatter = QuantityFormatter(for: displayGlucosePreference.unit)
  26. super.init(style: .grouped)
  27. title = LocalizedString("CGM Settings", comment: "Title for CGM simulator settings")
  28. displayGlucosePreference.$unit
  29. .sink { [weak self] _ in self?.tableView.reloadData() }
  30. .store(in: &cancellables)
  31. }
  32. required init?(coder aDecoder: NSCoder) {
  33. fatalError("init(coder:) has not been implemented")
  34. }
  35. override func viewDidLoad() {
  36. super.viewDidLoad()
  37. tableView.rowHeight = UITableView.automaticDimension
  38. tableView.estimatedRowHeight = 44
  39. tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className)
  40. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  41. tableView.register(BoundSwitchTableViewCell.self, forCellReuseIdentifier: BoundSwitchTableViewCell.className)
  42. let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:)))
  43. self.navigationItem.setRightBarButton(button, animated: false)
  44. }
  45. @objc func doneTapped(_ sender: Any) {
  46. done()
  47. }
  48. private func done() {
  49. if let nav = navigationController as? SettingsNavigationViewController {
  50. nav.notifyComplete()
  51. }
  52. }
  53. // MARK: - Data Source
  54. private enum Section: Int, CaseIterable {
  55. case model = 0
  56. case glucoseThresholds
  57. case effects
  58. case cgmStatus
  59. case history
  60. case alerts
  61. case lifecycleProgress
  62. case healthKit
  63. case uploading
  64. case deleteCGM
  65. }
  66. private enum ModelRow: Int, CaseIterable {
  67. case constant = 0
  68. case sineCurve
  69. case scenario
  70. case noData
  71. case signalLoss
  72. case unreliableData
  73. case frequency
  74. }
  75. private enum GlucoseThresholds: Int, CaseIterable {
  76. case enableAlerting
  77. case cgmLowerLimit
  78. case urgentLowGlucoseThreshold
  79. case lowGlucoseThreshold
  80. case highGlucoseThreshold
  81. case cgmUpperLimit
  82. }
  83. private enum EffectsRow: Int, CaseIterable {
  84. case noise = 0
  85. case lowOutlier
  86. case highOutlier
  87. case error
  88. }
  89. private enum CGMStatusRow: Int, CaseIterable {
  90. case batteryRemaining = 0
  91. case requestCalibration
  92. }
  93. private enum HistoryRow: Int, CaseIterable {
  94. case trend = 0
  95. case backfill
  96. }
  97. private enum AlertsRow: Int, CaseIterable {
  98. case issueAlert = 0
  99. }
  100. private enum LifecycleProgressRow: Int, CaseIterable {
  101. case percentComplete
  102. case warningThreshold
  103. case criticalThreshold
  104. }
  105. private enum HealthKitRow: Int, CaseIterable {
  106. case healthKitStorageDelayEnabled = 0
  107. }
  108. private enum UploadingRow: Int, CaseIterable {
  109. case uploadEnabled = 0
  110. }
  111. // MARK: - UITableViewDataSource
  112. override func numberOfSections(in tableView: UITableView) -> Int {
  113. return Section.allCases.count
  114. }
  115. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  116. switch Section(rawValue: section)! {
  117. case .model:
  118. return ModelRow.allCases.count
  119. case .glucoseThresholds:
  120. return GlucoseThresholds.allCases.count
  121. case .effects:
  122. return EffectsRow.allCases.count
  123. case .cgmStatus:
  124. return CGMStatusRow.allCases.count
  125. case .history:
  126. return HistoryRow.allCases.count
  127. case .alerts:
  128. return AlertsRow.allCases.count
  129. case .lifecycleProgress:
  130. return LifecycleProgressRow.allCases.count
  131. case .healthKit:
  132. return HealthKitRow.allCases.count
  133. case .uploading:
  134. return UploadingRow.allCases.count
  135. case .deleteCGM:
  136. return 1
  137. }
  138. }
  139. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  140. switch Section(rawValue: section)! {
  141. case .model:
  142. return "Model"
  143. case .glucoseThresholds:
  144. return "Glucose Thresholds"
  145. case .effects:
  146. return "Effects"
  147. case .cgmStatus:
  148. return "CGM Status"
  149. case .history:
  150. return "History"
  151. case .alerts:
  152. return "Alerts"
  153. case .lifecycleProgress:
  154. return "Lifecycle Progress"
  155. case .healthKit:
  156. return "HealthKit"
  157. case .uploading:
  158. return "Uploading"
  159. case .deleteCGM:
  160. return " " // Use an empty string for more dramatic spacing
  161. }
  162. }
  163. override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  164. switch Section(rawValue: section) {
  165. case .healthKit:
  166. return "Amount of time to wait before storing CGM samples to HealthKit. If enabled, the delay is \(cgmManager.fixedHealthKitStorageDelay.minutes) minutes. NOTE: after changing this, you will need to delete and re-add the CGM simulator!"
  167. default:
  168. return nil
  169. }
  170. }
  171. private var quantityFormatter: QuantityFormatter
  172. private lazy var percentageFormatter: NumberFormatter = {
  173. let formatter = NumberFormatter()
  174. formatter.minimumIntegerDigits = 1
  175. formatter.maximumFractionDigits = 1
  176. return formatter
  177. }()
  178. private lazy var durationFormatter: DateComponentsFormatter = {
  179. let durationFormatter = DateComponentsFormatter()
  180. durationFormatter.allowedUnits = [.hour, .minute]
  181. durationFormatter.unitsStyle = .full
  182. return durationFormatter
  183. }()
  184. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  185. switch Section(rawValue: indexPath.section)! {
  186. case .model:
  187. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  188. switch ModelRow(rawValue: indexPath.row)! {
  189. case .constant:
  190. cell.textLabel?.text = "Constant"
  191. if case .constant(let glucose) = cgmManager.dataSource.model {
  192. cell.detailTextLabel?.text = quantityFormatter.string(from: glucose)
  193. cell.accessoryType = .checkmark
  194. } else {
  195. cell.accessoryType = .disclosureIndicator
  196. }
  197. case .sineCurve:
  198. cell.textLabel?.text = "Sine Curve"
  199. if case .sineCurve(parameters: (baseGlucose: let baseGlucose, amplitude: let amplitude, period: _, referenceDate: _)) = cgmManager.dataSource.model {
  200. if let baseGlucoseText = quantityFormatter.numberFormatter.string(from: baseGlucose.doubleValue(for: glucoseUnit)),
  201. let amplitudeText = quantityFormatter.string(from: amplitude) {
  202. cell.detailTextLabel?.text = "\(baseGlucoseText) ± \(amplitudeText)"
  203. }
  204. cell.accessoryType = .checkmark
  205. } else {
  206. cell.accessoryType = .disclosureIndicator
  207. }
  208. case .scenario:
  209. cell.textLabel?.text = "Scenario"
  210. if case .scenario = cgmManager.dataSource.model {
  211. cell.accessoryType = .checkmark
  212. }
  213. case .noData:
  214. cell.textLabel?.text = "No Data"
  215. if case .noData = cgmManager.dataSource.model {
  216. cell.accessoryType = .checkmark
  217. }
  218. case .signalLoss:
  219. cell.textLabel?.text = "Signal Loss"
  220. if case .signalLoss = cgmManager.dataSource.model {
  221. cell.accessoryType = .checkmark
  222. }
  223. case .unreliableData:
  224. cell.textLabel?.text = "Unreliable Data"
  225. if case .unreliableData = cgmManager.dataSource.model {
  226. cell.accessoryType = .checkmark
  227. }
  228. case .frequency:
  229. cell.textLabel?.text = "Measurement Frequency"
  230. cell.detailTextLabel?.text = cgmManager.dataSource.dataPointFrequency.localizedDescription
  231. cell.accessoryType = .disclosureIndicator
  232. }
  233. return cell
  234. case .glucoseThresholds:
  235. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  236. switch GlucoseThresholds(rawValue: indexPath.row)! {
  237. case .enableAlerting:
  238. let cell = tableView.dequeueReusableCell(withIdentifier: BoundSwitchTableViewCell.className, for: indexPath) as! BoundSwitchTableViewCell
  239. cell.textLabel?.text = "Glucose Value Alerting"
  240. cell.switch?.isOn = cgmManager.mockSensorState.glucoseAlertingEnabled
  241. cell.onToggle = { [weak self] isOn in
  242. self?.cgmManager.mockSensorState.glucoseAlertingEnabled = isOn
  243. }
  244. cell.selectionStyle = .none
  245. return cell
  246. case .cgmLowerLimit:
  247. cell.textLabel?.text = "CGM Lower Limit"
  248. cell.detailTextLabel?.text = quantityFormatter.string(from: cgmManager.mockSensorState.cgmLowerLimit)
  249. case .urgentLowGlucoseThreshold:
  250. cell.textLabel?.text = "Urgent Low Glucose Threshold"
  251. cell.detailTextLabel?.text = quantityFormatter.string(from: cgmManager.mockSensorState.urgentLowGlucoseThreshold)
  252. case .lowGlucoseThreshold:
  253. cell.textLabel?.text = "Low Glucose Threshold"
  254. cell.detailTextLabel?.text = quantityFormatter.string(from: cgmManager.mockSensorState.lowGlucoseThreshold)
  255. case .highGlucoseThreshold:
  256. cell.textLabel?.text = "High Glucose Threshold"
  257. cell.detailTextLabel?.text = quantityFormatter.string(from: cgmManager.mockSensorState.highGlucoseThreshold)
  258. case .cgmUpperLimit:
  259. cell.textLabel?.text = "CGM Upper Limit"
  260. cell.detailTextLabel?.text = quantityFormatter.string(from: cgmManager.mockSensorState.cgmUpperLimit)
  261. }
  262. cell.accessoryType = .disclosureIndicator
  263. return cell
  264. case .effects:
  265. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  266. switch EffectsRow(rawValue: indexPath.row)! {
  267. case .noise:
  268. cell.textLabel?.text = "Glucose Noise"
  269. if let maximumDeltaMagnitude = cgmManager.dataSource.effects.glucoseNoise {
  270. cell.detailTextLabel?.text = quantityFormatter.string(from: maximumDeltaMagnitude)
  271. } else {
  272. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  273. }
  274. case .lowOutlier:
  275. cell.textLabel?.text = "Random Low Outlier"
  276. if let chance = cgmManager.dataSource.effects.randomLowOutlier?.chance,
  277. let percentageString = percentageFormatter.string(from: chance * 100)
  278. {
  279. cell.detailTextLabel?.text = "\(percentageString)% chance"
  280. } else {
  281. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  282. }
  283. case .highOutlier:
  284. cell.textLabel?.text = "Random High Outlier"
  285. if let chance = cgmManager.dataSource.effects.randomHighOutlier?.chance,
  286. let percentageString = percentageFormatter.string(from: chance * 100)
  287. {
  288. cell.detailTextLabel?.text = "\(percentageString)% chance"
  289. } else {
  290. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  291. }
  292. case .error:
  293. cell.textLabel?.text = "Random Error"
  294. if let chance = cgmManager.dataSource.effects.randomErrorChance,
  295. let percentageString = percentageFormatter.string(from: chance * 100)
  296. {
  297. cell.detailTextLabel?.text = "\(percentageString)% chance"
  298. } else {
  299. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  300. }
  301. }
  302. cell.accessoryType = .disclosureIndicator
  303. return cell
  304. case .cgmStatus:
  305. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  306. switch CGMStatusRow(rawValue: indexPath.row)! {
  307. case .batteryRemaining:
  308. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  309. cell.textLabel?.text = "Battery Remaining"
  310. if let remainingCharge = cgmManager.cgmBatteryChargeRemaining {
  311. cell.detailTextLabel?.text = "\(Int(round(remainingCharge * 100)))%"
  312. } else {
  313. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  314. }
  315. cell.accessoryType = .disclosureIndicator
  316. return cell
  317. case .requestCalibration:
  318. cell.textLabel?.text = "Request Calibration"
  319. if cgmManager.isCalibrationRequested {
  320. cell.accessoryType = .checkmark
  321. }
  322. }
  323. return cell
  324. case .history:
  325. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  326. switch HistoryRow(rawValue: indexPath.row)! {
  327. case .trend:
  328. cell.textLabel?.text = "Trend"
  329. cell.detailTextLabel?.text = cgmManager.mockSensorState.trendType?.symbol
  330. case .backfill:
  331. cell.textLabel?.text = "Backfill Glucose"
  332. }
  333. cell.accessoryType = .disclosureIndicator
  334. return cell
  335. case .alerts:
  336. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  337. switch AlertsRow(rawValue: indexPath.row)! {
  338. case .issueAlert:
  339. cell.textLabel?.text = "Issue Alerts"
  340. cell.accessoryType = .disclosureIndicator
  341. }
  342. return cell
  343. case .lifecycleProgress:
  344. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  345. switch LifecycleProgressRow(rawValue: indexPath.row)! {
  346. case .percentComplete:
  347. cell.textLabel?.text = "Percent Completed"
  348. if let percentCompleted = cgmManager.mockSensorState.cgmLifecycleProgress?.percentComplete {
  349. cell.detailTextLabel?.text = "\(Int(round(percentCompleted * 100)))%"
  350. } else {
  351. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  352. }
  353. case .warningThreshold:
  354. cell.textLabel?.text = "Warning Threshold"
  355. if let warningThreshold = cgmManager.mockSensorState.progressWarningThresholdPercentValue {
  356. cell.detailTextLabel?.text = "\(Int(round(warningThreshold * 100)))%"
  357. } else {
  358. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  359. }
  360. case .criticalThreshold:
  361. cell.textLabel?.text = "Critical Threshold"
  362. if let criticalThreshold = cgmManager.mockSensorState.progressCriticalThresholdPercentValue {
  363. cell.detailTextLabel?.text = "\(Int(round(criticalThreshold * 100)))%"
  364. } else {
  365. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  366. }
  367. }
  368. cell.accessoryType = .disclosureIndicator
  369. return cell
  370. case .healthKit:
  371. switch HealthKitRow(rawValue: indexPath.row)! {
  372. case .healthKitStorageDelayEnabled:
  373. let cell = tableView.dequeueReusableCell(withIdentifier: BoundSwitchTableViewCell.className, for: indexPath) as! BoundSwitchTableViewCell
  374. cell.textLabel?.text = "Storage Delay"
  375. cell.switch?.isOn = cgmManager.healthKitStorageDelayEnabled
  376. cell.onToggle = { [weak self] isOn in
  377. let confirmVC = UIAlertController(cgmDeletionHandler: {
  378. self?.cgmManager.healthKitStorageDelayEnabled = isOn
  379. self?.cgmManager.notifyDelegateOfDeletion {
  380. DispatchQueue.main.async {
  381. self?.done()
  382. }
  383. }
  384. }, cancelHandler: { cell.switch?.isOn = self?.cgmManager.healthKitStorageDelayEnabled ?? false })
  385. self?.present(confirmVC, animated: true) {
  386. tableView.deselectRow(at: indexPath, animated: true)
  387. }
  388. }
  389. cell.selectionStyle = .none
  390. return cell
  391. }
  392. case .uploading:
  393. switch UploadingRow(rawValue: indexPath.row)! {
  394. case .uploadEnabled:
  395. let cell = tableView.dequeueReusableCell(withIdentifier: BoundSwitchTableViewCell.className, for: indexPath) as! BoundSwitchTableViewCell
  396. cell.textLabel?.text = "Upload CGM Samples"
  397. cell.switch?.isOn = cgmManager.mockSensorState.samplesShouldBeUploaded
  398. cell.onToggle = { [weak self] isOn in
  399. self?.cgmManager.mockSensorState.samplesShouldBeUploaded = isOn
  400. }
  401. cell.selectionStyle = .none
  402. return cell
  403. }
  404. case .deleteCGM:
  405. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  406. cell.textLabel?.text = "Delete CGM"
  407. cell.textLabel?.textAlignment = .center
  408. cell.tintColor = .delete
  409. cell.isEnabled = true
  410. return cell
  411. }
  412. }
  413. // MARK: - UITableViewDelegate
  414. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  415. let sender = tableView.cellForRow(at: indexPath)
  416. switch Section(rawValue: indexPath.section)! {
  417. case .model:
  418. switch ModelRow(rawValue: indexPath.row)! {
  419. case .constant:
  420. let vc = GlucoseEntryTableViewController(glucoseUnit: glucoseUnit)
  421. vc.title = "Constant"
  422. vc.indexPath = indexPath
  423. vc.contextHelp = "A constant glucose model returns a fixed glucose value regardless of context."
  424. vc.glucoseEntryDelegate = self
  425. show(vc, sender: sender)
  426. case .sineCurve:
  427. let vc = SineCurveParametersTableViewController(glucoseUnit: glucoseUnit)
  428. if case .sineCurve(parameters: let parameters) = cgmManager.dataSource.model {
  429. vc.parameters = parameters
  430. } else {
  431. vc.parameters = nil
  432. }
  433. vc.contextHelp = "The sine curve parameters describe a mathematical model for glucose value production."
  434. vc.delegate = self
  435. show(vc, sender: sender)
  436. case .scenario:
  437. cgmManager.dataSource.model = .scenario(pastSamples: [], futureSamples: [])
  438. tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
  439. case .noData:
  440. cgmManager.dataSource.model = .noData
  441. cgmManager.retractSignalLossAlert()
  442. tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
  443. case .signalLoss:
  444. cgmManager.dataSource.model = .signalLoss
  445. cgmManager.issueSignalLossAlert()
  446. tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
  447. case .unreliableData:
  448. cgmManager.dataSource.model = .unreliableData
  449. tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
  450. case .frequency:
  451. let vc = MeasurementFrequencyTableViewController()
  452. vc.measurementFrequency = cgmManager.dataSource.dataPointFrequency
  453. vc.title = "Measurement Frequency"
  454. vc.measurementFrequencyDelegate = self
  455. show(vc, sender: sender)
  456. }
  457. case .glucoseThresholds:
  458. let vc = GlucoseEntryTableViewController(glucoseUnit: glucoseUnit)
  459. vc.indexPath = indexPath
  460. vc.glucoseEntryDelegate = self
  461. switch GlucoseThresholds(rawValue: indexPath.row)! {
  462. case .enableAlerting:
  463. return
  464. case .cgmLowerLimit:
  465. vc.title = "CGM Lower Limit"
  466. vc.contextHelp = "The glucose value that marks the lower limit of the CGM. Any value at or below this value is presented at `LOW`. This value must be lower than the urgent low threshold. If not, it will be set to 1 below the urgent low glucose threshold."
  467. case .urgentLowGlucoseThreshold:
  468. vc.title = "Urgent Low Glucose Threshold"
  469. vc.contextHelp = "The glucose value that marks the urgent low glucose threshold. Any value at or below this value is considered urgent low. This value must be above the cgm lower limit and lower than the low threshold. If not, it will be set to a value above the lower limit and below the low glucose threshold."
  470. case .lowGlucoseThreshold:
  471. vc.title = "Low Glucose Threshold"
  472. vc.contextHelp = "The glucose value that marks the low glucose threshold. Any value at or below this value is considered low. This value must be above the urgent low threshold and lower than the high threshold. If not, it will be set to a value above the urgent lower limit and below the high glucose threshold."
  473. case .highGlucoseThreshold:
  474. vc.title = "High Glucose Threshold"
  475. vc.contextHelp = "The glucose value that marks the high glucose threshold. Any value at or above this value is considered high. This value must be above the low threshold and lower than the cgm upper limit. If not, it will be set to a value above the low glucose threshold and below the upper limit."
  476. case .cgmUpperLimit:
  477. vc.title = "CGM Upper Limit"
  478. vc.contextHelp = "The glucose value that marks the upper limit of the CGM. Any value at or above this value is presented at `HIGH`. This value must be above the high threshold. If not, it will be set to 1 above the high glucose threshold."
  479. }
  480. show(vc, sender: sender)
  481. case .effects:
  482. switch EffectsRow(rawValue: indexPath.row)! {
  483. case .noise:
  484. let vc = GlucoseEntryTableViewController(glucoseUnit: glucoseUnit)
  485. if let maximumDeltaMagnitude = cgmManager.dataSource.effects.glucoseNoise {
  486. vc.glucose = maximumDeltaMagnitude
  487. }
  488. vc.title = "Glucose Noise"
  489. vc.contextHelp = "The magnitude of glucose noise applied to CGM values determines the maximum random amount of variation applied to each glucose value."
  490. vc.indexPath = indexPath
  491. vc.glucoseEntryDelegate = self
  492. show(vc, sender: sender)
  493. case .lowOutlier:
  494. let vc = RandomOutlierTableViewController(glucoseUnit: glucoseUnit)
  495. vc.title = "Low Outlier"
  496. vc.randomOutlier = cgmManager.dataSource.effects.randomLowOutlier
  497. vc.contextHelp = "Produced glucose values will have a chance of being decreased by the delta quantity."
  498. vc.indexPath = indexPath
  499. vc.delegate = self
  500. show(vc, sender: sender)
  501. case .highOutlier:
  502. let vc = RandomOutlierTableViewController(glucoseUnit: glucoseUnit)
  503. vc.title = "High Outlier"
  504. vc.randomOutlier = cgmManager.dataSource.effects.randomHighOutlier
  505. vc.contextHelp = "Produced glucose values will have a chance of being increased by the delta quantity."
  506. vc.indexPath = indexPath
  507. vc.delegate = self
  508. show(vc, sender: sender)
  509. case .error:
  510. let vc = PercentageTextFieldTableViewController()
  511. if let chance = cgmManager.dataSource.effects.randomErrorChance {
  512. vc.percentage = chance
  513. }
  514. vc.title = "Random Error"
  515. vc.contextHelp = "The percentage determines the chance with which the CGM will error when a glucose value is requested."
  516. vc.indexPath = indexPath
  517. vc.percentageDelegate = self
  518. show(vc, sender: sender)
  519. }
  520. case .cgmStatus:
  521. switch CGMStatusRow(rawValue: indexPath.row)! {
  522. case .batteryRemaining:
  523. let vc = PercentageTextFieldTableViewController()
  524. vc.percentage = cgmManager.cgmBatteryChargeRemaining
  525. vc.indexPath = indexPath
  526. vc.percentageDelegate = self
  527. show(vc, sender: sender)
  528. case .requestCalibration:
  529. cgmManager.requestCalibration(!cgmManager.isCalibrationRequested)
  530. tableView.reloadRows(at: [indexPath], with: .automatic)
  531. }
  532. case .history:
  533. switch HistoryRow(rawValue: indexPath.row)! {
  534. case .trend:
  535. let vc = GlucoseTrendTableViewController()
  536. vc.glucoseTrend = cgmManager.mockSensorState.trendType
  537. vc.title = "Glucose Trend"
  538. vc.glucoseTrendDelegate = self
  539. show(vc, sender: sender)
  540. case .backfill:
  541. let vc = DateAndDurationTableViewController()
  542. vc.inputMode = .duration(.hours(3))
  543. vc.title = "Backfill"
  544. vc.contextHelp = "Performing a backfill will not delete existing prior glucose values."
  545. vc.indexPath = indexPath
  546. vc.onSave { inputMode in
  547. guard case .duration(let duration) = inputMode else {
  548. assertionFailure()
  549. return
  550. }
  551. self.cgmManager.backfillData(datingBack: duration)
  552. }
  553. show(vc, sender: sender)
  554. }
  555. case .alerts:
  556. switch AlertsRow(rawValue: indexPath.row)! {
  557. case .issueAlert:
  558. let vc = IssueAlertTableViewController(cgmManager: cgmManager)
  559. show(vc, sender: sender)
  560. }
  561. case .lifecycleProgress:
  562. let vc = PercentageTextFieldTableViewController()
  563. vc.indexPath = indexPath
  564. vc.percentageDelegate = self
  565. switch LifecycleProgressRow(rawValue: indexPath.row)! {
  566. case .percentComplete:
  567. vc.percentage = cgmManager.mockSensorState.cgmLifecycleProgress?.percentComplete
  568. case .warningThreshold:
  569. vc.percentage = cgmManager.mockSensorState.progressWarningThresholdPercentValue
  570. case .criticalThreshold:
  571. vc.percentage = cgmManager.mockSensorState.progressCriticalThresholdPercentValue
  572. }
  573. show(vc, sender: sender)
  574. case .healthKit:
  575. return
  576. case .uploading:
  577. return
  578. case .deleteCGM:
  579. let confirmVC = UIAlertController(cgmDeletionHandler: {
  580. self.cgmManager.notifyDelegateOfDeletion {
  581. DispatchQueue.main.async {
  582. self.done()
  583. }
  584. }
  585. })
  586. present(confirmVC, animated: true) {
  587. tableView.deselectRow(at: indexPath, animated: true)
  588. }
  589. }
  590. }
  591. private func indexPaths<Row: CaseIterable & RawRepresentable>(
  592. forSection section: Section,
  593. rows _: Row.Type
  594. ) -> [IndexPath] where Row.RawValue == Int {
  595. let rows = Row.allCases
  596. return zip(rows, repeatElement(section, count: rows.count)).map { row, section in
  597. return IndexPath(row: row.rawValue, section: section.rawValue)
  598. }
  599. }
  600. }
  601. extension MockCGMManagerSettingsViewController: GlucoseEntryTableViewControllerDelegate {
  602. func glucoseEntryTableViewControllerDidChangeGlucose(_ controller: GlucoseEntryTableViewController) {
  603. guard let indexPath = controller.indexPath else {
  604. assertionFailure()
  605. return
  606. }
  607. tableView.deselectRow(at: indexPath, animated: true)
  608. switch Section(rawValue: indexPath.section)! {
  609. case .model:
  610. switch ModelRow(rawValue: indexPath.row)! {
  611. case .constant:
  612. if let glucose = controller.glucose {
  613. cgmManager.dataSource.model = .constant(glucose)
  614. cgmManager.retractSignalLossAlert()
  615. tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
  616. }
  617. default:
  618. assertionFailure()
  619. }
  620. case .effects:
  621. switch EffectsRow(rawValue: indexPath.row) {
  622. case .noise:
  623. if let glucose = controller.glucose {
  624. cgmManager.dataSource.effects.glucoseNoise = glucose
  625. }
  626. default:
  627. assertionFailure()
  628. }
  629. case .glucoseThresholds:
  630. if let glucose = controller.glucose {
  631. switch GlucoseThresholds(rawValue: indexPath.row)! {
  632. case .cgmLowerLimit:
  633. cgmManager.mockSensorState.cgmLowerLimit = glucose
  634. case .urgentLowGlucoseThreshold:
  635. cgmManager.mockSensorState.urgentLowGlucoseThreshold = glucose
  636. case .lowGlucoseThreshold:
  637. cgmManager.mockSensorState.lowGlucoseThreshold = glucose
  638. case .highGlucoseThreshold:
  639. cgmManager.mockSensorState.highGlucoseThreshold = glucose
  640. case .cgmUpperLimit:
  641. cgmManager.mockSensorState.cgmUpperLimit = glucose
  642. default:
  643. assertionFailure()
  644. }
  645. }
  646. default:
  647. assertionFailure()
  648. }
  649. tableView.reloadRows(at: [indexPath], with: .automatic)
  650. }
  651. }
  652. extension MockCGMManagerSettingsViewController: SineCurveParametersTableViewControllerDelegate {
  653. func sineCurveParametersTableViewControllerDidUpdateParameters(_ controller: SineCurveParametersTableViewController) {
  654. if let parameters = controller.parameters {
  655. cgmManager.dataSource.model = .sineCurve(parameters: parameters)
  656. cgmManager.retractSignalLossAlert()
  657. tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
  658. }
  659. }
  660. }
  661. extension MockCGMManagerSettingsViewController: RandomOutlierTableViewControllerDelegate {
  662. func randomOutlierTableViewControllerDidChangeOutlier(_ controller: RandomOutlierTableViewController) {
  663. guard let indexPath = controller.indexPath else {
  664. assertionFailure()
  665. return
  666. }
  667. switch Section(rawValue: indexPath.section)! {
  668. case .effects:
  669. switch EffectsRow(rawValue: indexPath.row)! {
  670. case .lowOutlier:
  671. cgmManager.dataSource.effects.randomLowOutlier = controller.randomOutlier
  672. case .highOutlier:
  673. cgmManager.dataSource.effects.randomHighOutlier = controller.randomOutlier
  674. default:
  675. assertionFailure()
  676. }
  677. default:
  678. assertionFailure()
  679. }
  680. tableView.reloadRows(at: [indexPath], with: .automatic)
  681. }
  682. }
  683. extension MockCGMManagerSettingsViewController: PercentageTextFieldTableViewControllerDelegate {
  684. func percentageTextFieldTableViewControllerDidChangePercentage(_ controller: PercentageTextFieldTableViewController) {
  685. guard let indexPath = controller.indexPath else {
  686. assertionFailure()
  687. return
  688. }
  689. switch Section(rawValue: indexPath.section)! {
  690. case .effects:
  691. switch EffectsRow(rawValue: indexPath.row)! {
  692. case .error:
  693. cgmManager.dataSource.effects.randomErrorChance = controller.percentage?.clamped(to: 0...1)
  694. default:
  695. assertionFailure()
  696. }
  697. case .cgmStatus:
  698. switch CGMStatusRow(rawValue: indexPath.row)! {
  699. case .batteryRemaining:
  700. if let batteryRemaining = controller.percentage.map({ $0.clamped(to: 0...1) }) {
  701. cgmManager.cgmBatteryChargeRemaining = batteryRemaining
  702. }
  703. default:
  704. assertionFailure()
  705. }
  706. case .lifecycleProgress:
  707. switch LifecycleProgressRow(rawValue: indexPath.row)! {
  708. case .percentComplete:
  709. if let percentComplete = controller.percentage.map({ $0.clamped(to: 0...1) }) {
  710. cgmManager.mockSensorState.cgmLifecycleProgress = MockCGMLifecycleProgress(percentComplete: percentComplete)
  711. } else {
  712. cgmManager.mockSensorState.cgmLifecycleProgress = nil
  713. }
  714. case .warningThreshold:
  715. cgmManager.mockSensorState.progressWarningThresholdPercentValue = controller.percentage.map { $0.clamped(to: 0...1) }
  716. case .criticalThreshold:
  717. cgmManager.mockSensorState.progressCriticalThresholdPercentValue = controller.percentage.map { $0.clamped(to: 0...1) }
  718. }
  719. default:
  720. assertionFailure()
  721. }
  722. tableView.reloadRows(at: [indexPath], with: .automatic)
  723. }
  724. }
  725. extension MockCGMManagerSettingsViewController: GlucoseTrendTableViewControllerDelegate {
  726. func glucoseTrendTableViewControllerDidChangeTrend(_ controller: GlucoseTrendTableViewController) {
  727. cgmManager.mockSensorState.trendType = controller.glucoseTrend
  728. tableView.reloadRows(at: [[Section.history.rawValue, HistoryRow.trend.rawValue]], with: .automatic)
  729. }
  730. }
  731. extension MockCGMManagerSettingsViewController: MeasurementFrequencyTableViewControllerDelegate {
  732. func measurementFrequencyTableViewControllerDidChangeFrequency(_ controller: MeasurementFrequencyTableViewController) {
  733. if let measurementFrequency = controller.measurementFrequency {
  734. cgmManager.dataSource.dataPointFrequency = measurementFrequency
  735. cgmManager.updateGlucoseUpdateTimer()
  736. tableView.reloadRows(at: [[Section.model.rawValue, ModelRow.frequency.rawValue]], with: .automatic)
  737. }
  738. }
  739. }
  740. private extension UIAlertController {
  741. convenience init(cgmDeletionHandler confirmHandler: @escaping () -> Void, cancelHandler: (() -> Void)? = nil) {
  742. self.init(
  743. title: nil,
  744. message: "Are you sure you want to delete this CGM?",
  745. preferredStyle: .actionSheet
  746. )
  747. addAction(UIAlertAction(
  748. title: "Delete CGM",
  749. style: .destructive,
  750. handler: { _ in
  751. confirmHandler()
  752. }
  753. ))
  754. let cancel = "Cancel"
  755. addAction(UIAlertAction(title: cancel, style: .cancel, handler: { _ in cancelHandler?() }))
  756. }
  757. }