MockCGMManagerSettingsViewController.swift 30 KB

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