MockCGMManagerSettingsViewController.swift 36 KB

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