| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802 |
- //
- // MockCGMManagerSettingsViewController.swift
- // LoopKitUI
- //
- // Created by Michael Pangburn on 11/23/18.
- // Copyright © 2018 LoopKit Authors. All rights reserved.
- //
- import UIKit
- import Combine
- import HealthKit
- import LoopKit
- import LoopKitUI
- import MockKit
- final class MockCGMManagerSettingsViewController: UITableViewController {
- let cgmManager: MockCGMManager
- private let displayGlucoseUnitObservable: DisplayGlucoseUnitObservable
- private lazy var cancellables = Set<AnyCancellable>()
- private var glucoseUnit: HKUnit {
- displayGlucoseUnitObservable.displayGlucoseUnit
- }
- init(cgmManager: MockCGMManager, displayGlucoseUnitObservable: DisplayGlucoseUnitObservable) {
- self.cgmManager = cgmManager
- self.displayGlucoseUnitObservable = displayGlucoseUnitObservable
- super.init(style: .grouped)
- title = NSLocalizedString("CGM Settings", comment: "Title for CGM simulator settings")
- displayGlucoseUnitObservable.$displayGlucoseUnit
- .sink { [weak self] _ in self?.tableView.reloadData() }
- .store(in: &cancellables)
- }
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- override func viewDidLoad() {
- super.viewDidLoad()
- tableView.rowHeight = UITableView.automaticDimension
- tableView.estimatedRowHeight = 44
- tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className)
- tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
- tableView.register(BoundSwitchTableViewCell.self, forCellReuseIdentifier: BoundSwitchTableViewCell.className)
- let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:)))
- self.navigationItem.setRightBarButton(button, animated: false)
- }
- @objc func doneTapped(_ sender: Any) {
- done()
- }
- private func done() {
- if let nav = navigationController as? SettingsNavigationViewController {
- nav.notifyComplete()
- }
- }
- // MARK: - Data Source
- private enum Section: Int, CaseIterable {
- case model = 0
- case glucoseThresholds
- case effects
- case cgmStatus
- case history
- case alerts
- case lifecycleProgress
- case healthKit
- case uploading
- case deleteCGM
- }
- private enum ModelRow: Int, CaseIterable {
- case constant = 0
- case sineCurve
- case noData
- case signalLoss
- case unreliableData
- case frequency
- }
-
- private enum GlucoseThresholds: Int, CaseIterable {
- case enableAlerting
- case cgmLowerLimit
- case urgentLowGlucoseThreshold
- case lowGlucoseThreshold
- case highGlucoseThreshold
- case cgmUpperLimit
- }
- private enum EffectsRow: Int, CaseIterable {
- case noise = 0
- case lowOutlier
- case highOutlier
- case error
- }
-
- private enum CGMStatusRow: Int, CaseIterable {
- case batteryRemaining = 0
- case requestCalibration
- }
- private enum HistoryRow: Int, CaseIterable {
- case trend = 0
- case backfill
- }
-
- private enum AlertsRow: Int, CaseIterable {
- case issueAlert = 0
- }
-
- private enum LifecycleProgressRow: Int, CaseIterable {
- case percentComplete
- case warningThreshold
- case criticalThreshold
- }
-
- private enum HealthKitRow: Int, CaseIterable {
- case healthKitStorageDelayEnabled = 0
- }
- private enum UploadingRow: Int, CaseIterable {
- case uploadEnabled = 0
- }
- // MARK: - UITableViewDataSource
- override func numberOfSections(in tableView: UITableView) -> Int {
- return Section.allCases.count
- }
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- switch Section(rawValue: section)! {
- case .model:
- return ModelRow.allCases.count
- case .glucoseThresholds:
- return GlucoseThresholds.allCases.count
- case .effects:
- return EffectsRow.allCases.count
- case .cgmStatus:
- return CGMStatusRow.allCases.count
- case .history:
- return HistoryRow.allCases.count
- case .alerts:
- return AlertsRow.allCases.count
- case .lifecycleProgress:
- return LifecycleProgressRow.allCases.count
- case .healthKit:
- return HealthKitRow.allCases.count
- case .uploading:
- return UploadingRow.allCases.count
- case .deleteCGM:
- return 1
- }
- }
- override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
- switch Section(rawValue: section)! {
- case .model:
- return "Model"
- case .glucoseThresholds:
- return "Glucose Thresholds"
- case .effects:
- return "Effects"
- case .cgmStatus:
- return "CGM Status"
- case .history:
- return "History"
- case .alerts:
- return "Alerts"
- case .lifecycleProgress:
- return "Lifecycle Progress"
- case .healthKit:
- return "HealthKit"
- case .uploading:
- return "Uploading"
- case .deleteCGM:
- return " " // Use an empty string for more dramatic spacing
- }
- }
- override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
- switch Section(rawValue: section) {
- case .healthKit:
- 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!"
- default:
- return nil
- }
- }
- private lazy var quantityFormatter = QuantityFormatter()
- private lazy var percentageFormatter: NumberFormatter = {
- let formatter = NumberFormatter()
- formatter.minimumIntegerDigits = 1
- formatter.maximumFractionDigits = 1
- return formatter
- }()
-
- private lazy var durationFormatter: DateComponentsFormatter = {
- let durationFormatter = DateComponentsFormatter()
- durationFormatter.allowedUnits = [.hour, .minute]
- durationFormatter.unitsStyle = .full
- return durationFormatter
- }()
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- switch Section(rawValue: indexPath.section)! {
- case .model:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
- switch ModelRow(rawValue: indexPath.row)! {
- case .constant:
- cell.textLabel?.text = "Constant"
- if case .constant(let glucose) = cgmManager.dataSource.model {
- cell.detailTextLabel?.text = quantityFormatter.string(from: glucose, for: glucoseUnit)
- cell.accessoryType = .checkmark
- } else {
- cell.accessoryType = .disclosureIndicator
- }
- case .sineCurve:
- cell.textLabel?.text = "Sine Curve"
- if case .sineCurve(parameters: (baseGlucose: let baseGlucose, amplitude: let amplitude, period: _, referenceDate: _)) = cgmManager.dataSource.model {
- if let baseGlucoseText = quantityFormatter.numberFormatter.string(from: baseGlucose.doubleValue(for: glucoseUnit)),
- let amplitudeText = quantityFormatter.string(from: amplitude, for: glucoseUnit) {
- cell.detailTextLabel?.text = "\(baseGlucoseText) ± \(amplitudeText)"
- }
- cell.accessoryType = .checkmark
- } else {
- cell.accessoryType = .disclosureIndicator
- }
- case .noData:
- cell.textLabel?.text = "No Data"
- if case .noData = cgmManager.dataSource.model {
- cell.accessoryType = .checkmark
- }
- case .signalLoss:
- cell.textLabel?.text = "Signal Loss"
- if case .signalLoss = cgmManager.dataSource.model {
- cell.accessoryType = .checkmark
- }
- case .unreliableData:
- cell.textLabel?.text = "Unreliable Data"
- if case .unreliableData = cgmManager.dataSource.model {
- cell.accessoryType = .checkmark
- }
- case .frequency:
- cell.textLabel?.text = "Measurement Frequency"
- cell.detailTextLabel?.text = cgmManager.dataSource.dataPointFrequency.localizedDescription
- cell.accessoryType = .disclosureIndicator
- }
- return cell
- case .glucoseThresholds:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
- switch GlucoseThresholds(rawValue: indexPath.row)! {
- case .enableAlerting:
- let cell = tableView.dequeueReusableCell(withIdentifier: BoundSwitchTableViewCell.className, for: indexPath) as! BoundSwitchTableViewCell
- cell.textLabel?.text = "Glucose Value Alerting"
- cell.switch?.isOn = cgmManager.mockSensorState.glucoseAlertingEnabled
- cell.onToggle = { [weak self] isOn in
- self?.cgmManager.mockSensorState.glucoseAlertingEnabled = isOn
- }
- cell.selectionStyle = .none
- return cell
- case .cgmLowerLimit:
- cell.textLabel?.text = "CGM Lower Limit"
- cell.detailTextLabel?.text = quantityFormatter.string(from: cgmManager.mockSensorState.cgmLowerLimit, for: glucoseUnit)
- case .urgentLowGlucoseThreshold:
- cell.textLabel?.text = "Urgent Low Glucose Threshold"
- cell.detailTextLabel?.text = quantityFormatter.string(from: cgmManager.mockSensorState.urgentLowGlucoseThreshold, for: glucoseUnit)
- case .lowGlucoseThreshold:
- cell.textLabel?.text = "Low Glucose Threshold"
- cell.detailTextLabel?.text = quantityFormatter.string(from: cgmManager.mockSensorState.lowGlucoseThreshold, for: glucoseUnit)
- case .highGlucoseThreshold:
- cell.textLabel?.text = "High Glucose Threshold"
- cell.detailTextLabel?.text = quantityFormatter.string(from: cgmManager.mockSensorState.highGlucoseThreshold, for: glucoseUnit)
- case .cgmUpperLimit:
- cell.textLabel?.text = "CGM Upper Limit"
- cell.detailTextLabel?.text = quantityFormatter.string(from: cgmManager.mockSensorState.cgmUpperLimit, for: glucoseUnit)
- }
- cell.accessoryType = .disclosureIndicator
- return cell
- case .effects:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
- switch EffectsRow(rawValue: indexPath.row)! {
- case .noise:
- cell.textLabel?.text = "Glucose Noise"
- if let maximumDeltaMagnitude = cgmManager.dataSource.effects.glucoseNoise {
- cell.detailTextLabel?.text = quantityFormatter.string(from: maximumDeltaMagnitude, for: glucoseUnit)
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- case .lowOutlier:
- cell.textLabel?.text = "Random Low Outlier"
- if let chance = cgmManager.dataSource.effects.randomLowOutlier?.chance,
- let percentageString = percentageFormatter.string(from: chance * 100)
- {
- cell.detailTextLabel?.text = "\(percentageString)% chance"
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- case .highOutlier:
- cell.textLabel?.text = "Random High Outlier"
- if let chance = cgmManager.dataSource.effects.randomHighOutlier?.chance,
- let percentageString = percentageFormatter.string(from: chance * 100)
- {
- cell.detailTextLabel?.text = "\(percentageString)% chance"
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- case .error:
- cell.textLabel?.text = "Random Error"
- if let chance = cgmManager.dataSource.effects.randomErrorChance,
- let percentageString = percentageFormatter.string(from: chance * 100)
- {
- cell.detailTextLabel?.text = "\(percentageString)% chance"
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- }
- cell.accessoryType = .disclosureIndicator
- return cell
- case .cgmStatus:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
- switch CGMStatusRow(rawValue: indexPath.row)! {
- case .batteryRemaining:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
- cell.textLabel?.text = "Battery Remaining"
- if let remainingCharge = cgmManager.cgmBatteryChargeRemaining {
- cell.detailTextLabel?.text = "\(Int(round(remainingCharge * 100)))%"
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- cell.accessoryType = .disclosureIndicator
- return cell
- case .requestCalibration:
- cell.textLabel?.text = "Request Calibration"
- if cgmManager.isCalibrationRequested {
- cell.accessoryType = .checkmark
- }
- }
- return cell
- case .history:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
- switch HistoryRow(rawValue: indexPath.row)! {
- case .trend:
- cell.textLabel?.text = "Trend"
- cell.detailTextLabel?.text = cgmManager.mockSensorState.trendType?.symbol
- case .backfill:
- cell.textLabel?.text = "Backfill Glucose"
- }
- cell.accessoryType = .disclosureIndicator
- return cell
- case .alerts:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
- switch AlertsRow(rawValue: indexPath.row)! {
- case .issueAlert:
- cell.textLabel?.text = "Issue Alerts"
- cell.accessoryType = .disclosureIndicator
- }
- return cell
- case .lifecycleProgress:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
- switch LifecycleProgressRow(rawValue: indexPath.row)! {
- case .percentComplete:
- cell.textLabel?.text = "Percent Completed"
- if let percentCompleted = cgmManager.mockSensorState.cgmLifecycleProgress?.percentComplete {
- cell.detailTextLabel?.text = "\(Int(round(percentCompleted * 100)))%"
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- case .warningThreshold:
- cell.textLabel?.text = "Warning Threshold"
- if let warningThreshold = cgmManager.mockSensorState.progressWarningThresholdPercentValue {
- cell.detailTextLabel?.text = "\(Int(round(warningThreshold * 100)))%"
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- case .criticalThreshold:
- cell.textLabel?.text = "Critical Threshold"
- if let criticalThreshold = cgmManager.mockSensorState.progressCriticalThresholdPercentValue {
- cell.detailTextLabel?.text = "\(Int(round(criticalThreshold * 100)))%"
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- }
- cell.accessoryType = .disclosureIndicator
- return cell
- case .healthKit:
- switch HealthKitRow(rawValue: indexPath.row)! {
- case .healthKitStorageDelayEnabled:
- let cell = tableView.dequeueReusableCell(withIdentifier: BoundSwitchTableViewCell.className, for: indexPath) as! BoundSwitchTableViewCell
- cell.textLabel?.text = "Storage Delay"
- cell.switch?.isOn = cgmManager.healthKitStorageDelayEnabled
- cell.onToggle = { [weak self] isOn in
- let confirmVC = UIAlertController(cgmDeletionHandler: {
- self?.cgmManager.healthKitStorageDelayEnabled = isOn
- self?.cgmManager.notifyDelegateOfDeletion {
- DispatchQueue.main.async {
- self?.done()
- }
- }
- }, cancelHandler: { cell.switch?.isOn = self?.cgmManager.healthKitStorageDelayEnabled ?? false })
- self?.present(confirmVC, animated: true) {
- tableView.deselectRow(at: indexPath, animated: true)
- }
- }
- cell.selectionStyle = .none
- return cell
- }
- case .uploading:
- switch UploadingRow(rawValue: indexPath.row)! {
- case .uploadEnabled:
- let cell = tableView.dequeueReusableCell(withIdentifier: BoundSwitchTableViewCell.className, for: indexPath) as! BoundSwitchTableViewCell
- cell.textLabel?.text = "Upload CGM Samples"
- cell.switch?.isOn = cgmManager.mockSensorState.samplesShouldBeUploaded
- cell.onToggle = { [weak self] isOn in
- self?.cgmManager.mockSensorState.samplesShouldBeUploaded = isOn
- }
- cell.selectionStyle = .none
- return cell
- }
- case .deleteCGM:
- let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
- cell.textLabel?.text = "Delete CGM"
- cell.textLabel?.textAlignment = .center
- cell.tintColor = .delete
- cell.isEnabled = true
- return cell
- }
- }
-
- // MARK: - UITableViewDelegate
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- let sender = tableView.cellForRow(at: indexPath)
- switch Section(rawValue: indexPath.section)! {
- case .model:
- switch ModelRow(rawValue: indexPath.row)! {
- case .constant:
- let vc = GlucoseEntryTableViewController(glucoseUnit: glucoseUnit)
- vc.title = "Constant"
- vc.indexPath = indexPath
- vc.contextHelp = "A constant glucose model returns a fixed glucose value regardless of context."
- vc.glucoseEntryDelegate = self
- show(vc, sender: sender)
- case .sineCurve:
- let vc = SineCurveParametersTableViewController(glucoseUnit: glucoseUnit)
- if case .sineCurve(parameters: let parameters) = cgmManager.dataSource.model {
- vc.parameters = parameters
- } else {
- vc.parameters = nil
- }
- vc.contextHelp = "The sine curve parameters describe a mathematical model for glucose value production."
- vc.delegate = self
- show(vc, sender: sender)
- case .noData:
- cgmManager.dataSource.model = .noData
- cgmManager.retractSignalLossAlert()
- tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
- case .signalLoss:
- cgmManager.dataSource.model = .signalLoss
- cgmManager.issueSignalLossAlert()
- tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
- case .unreliableData:
- cgmManager.dataSource.model = .unreliableData
- tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
- case .frequency:
- let vc = MeasurementFrequencyTableViewController()
- vc.measurementFrequency = cgmManager.dataSource.dataPointFrequency
- vc.title = "Measurement Frequency"
- vc.measurementFrequencyDelegate = self
- show(vc, sender: sender)
- }
- case .glucoseThresholds:
- let vc = GlucoseEntryTableViewController(glucoseUnit: glucoseUnit)
- vc.indexPath = indexPath
- vc.glucoseEntryDelegate = self
- switch GlucoseThresholds(rawValue: indexPath.row)! {
- case .enableAlerting:
- return
- case .cgmLowerLimit:
- vc.title = "CGM Lower Limit"
- 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."
- case .urgentLowGlucoseThreshold:
- vc.title = "Urgent Low Glucose Threshold"
- 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."
- case .lowGlucoseThreshold:
- vc.title = "Low Glucose Threshold"
- 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."
- case .highGlucoseThreshold:
- vc.title = "High Glucose Threshold"
- 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."
- case .cgmUpperLimit:
- vc.title = "CGM Upper Limit"
- 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."
- }
- show(vc, sender: sender)
- case .effects:
- switch EffectsRow(rawValue: indexPath.row)! {
- case .noise:
- let vc = GlucoseEntryTableViewController(glucoseUnit: glucoseUnit)
- if let maximumDeltaMagnitude = cgmManager.dataSource.effects.glucoseNoise {
- vc.glucose = maximumDeltaMagnitude
- }
- vc.title = "Glucose Noise"
- vc.contextHelp = "The magnitude of glucose noise applied to CGM values determines the maximum random amount of variation applied to each glucose value."
- vc.indexPath = indexPath
- vc.glucoseEntryDelegate = self
- show(vc, sender: sender)
- case .lowOutlier:
- let vc = RandomOutlierTableViewController(glucoseUnit: glucoseUnit)
- vc.title = "Low Outlier"
- vc.randomOutlier = cgmManager.dataSource.effects.randomLowOutlier
- vc.contextHelp = "Produced glucose values will have a chance of being decreased by the delta quantity."
- vc.indexPath = indexPath
- vc.delegate = self
- show(vc, sender: sender)
- case .highOutlier:
- let vc = RandomOutlierTableViewController(glucoseUnit: glucoseUnit)
- vc.title = "High Outlier"
- vc.randomOutlier = cgmManager.dataSource.effects.randomHighOutlier
- vc.contextHelp = "Produced glucose values will have a chance of being increased by the delta quantity."
- vc.indexPath = indexPath
- vc.delegate = self
- show(vc, sender: sender)
- case .error:
- let vc = PercentageTextFieldTableViewController()
- if let chance = cgmManager.dataSource.effects.randomErrorChance {
- vc.percentage = chance
- }
- vc.title = "Random Error"
- vc.contextHelp = "The percentage determines the chance with which the CGM will error when a glucose value is requested."
- vc.indexPath = indexPath
- vc.percentageDelegate = self
- show(vc, sender: sender)
- }
- case .cgmStatus:
- switch CGMStatusRow(rawValue: indexPath.row)! {
- case .batteryRemaining:
- let vc = PercentageTextFieldTableViewController()
- vc.percentage = cgmManager.cgmBatteryChargeRemaining
- vc.indexPath = indexPath
- vc.percentageDelegate = self
- show(vc, sender: sender)
- case .requestCalibration:
- cgmManager.requestCalibration(!cgmManager.isCalibrationRequested)
- tableView.reloadRows(at: [indexPath], with: .automatic)
- }
- case .history:
- switch HistoryRow(rawValue: indexPath.row)! {
- case .trend:
- let vc = GlucoseTrendTableViewController()
- vc.glucoseTrend = cgmManager.mockSensorState.trendType
- vc.title = "Glucose Trend"
- vc.glucoseTrendDelegate = self
- show(vc, sender: sender)
- case .backfill:
- let vc = DateAndDurationTableViewController()
- vc.inputMode = .duration(.hours(3))
- vc.title = "Backfill"
- vc.contextHelp = "Performing a backfill will not delete existing prior glucose values."
- vc.indexPath = indexPath
- vc.onSave { inputMode in
- guard case .duration(let duration) = inputMode else {
- assertionFailure()
- return
- }
- self.cgmManager.backfillData(datingBack: duration)
- }
- show(vc, sender: sender)
- }
- case .alerts:
- switch AlertsRow(rawValue: indexPath.row)! {
- case .issueAlert:
- let vc = IssueAlertTableViewController(cgmManager: cgmManager)
- show(vc, sender: sender)
- }
- case .lifecycleProgress:
- let vc = PercentageTextFieldTableViewController()
- vc.indexPath = indexPath
- vc.percentageDelegate = self
- switch LifecycleProgressRow(rawValue: indexPath.row)! {
- case .percentComplete:
- vc.percentage = cgmManager.mockSensorState.cgmLifecycleProgress?.percentComplete
- case .warningThreshold:
- vc.percentage = cgmManager.mockSensorState.progressWarningThresholdPercentValue
- case .criticalThreshold:
- vc.percentage = cgmManager.mockSensorState.progressCriticalThresholdPercentValue
- }
- show(vc, sender: sender)
- case .healthKit:
- return
- case .uploading:
- return
- case .deleteCGM:
- let confirmVC = UIAlertController(cgmDeletionHandler: {
- self.cgmManager.notifyDelegateOfDeletion {
- DispatchQueue.main.async {
- self.done()
- }
- }
- })
- present(confirmVC, animated: true) {
- tableView.deselectRow(at: indexPath, animated: true)
- }
- }
- }
- private func indexPaths<Row: CaseIterable & RawRepresentable>(
- forSection section: Section,
- rows _: Row.Type
- ) -> [IndexPath] where Row.RawValue == Int {
- let rows = Row.allCases
- return zip(rows, repeatElement(section, count: rows.count)).map { row, section in
- return IndexPath(row: row.rawValue, section: section.rawValue)
- }
- }
- }
- extension MockCGMManagerSettingsViewController: GlucoseEntryTableViewControllerDelegate {
- func glucoseEntryTableViewControllerDidChangeGlucose(_ controller: GlucoseEntryTableViewController) {
- guard let indexPath = controller.indexPath else {
- assertionFailure()
- return
- }
- tableView.deselectRow(at: indexPath, animated: true)
-
- switch Section(rawValue: indexPath.section)! {
- case .model:
- switch ModelRow(rawValue: indexPath.row)! {
- case .constant:
- if let glucose = controller.glucose {
- cgmManager.dataSource.model = .constant(glucose)
- cgmManager.retractSignalLossAlert()
- tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
- }
- default:
- assertionFailure()
- }
- case .effects:
- switch EffectsRow(rawValue: indexPath.row) {
- case .noise:
- if let glucose = controller.glucose {
- cgmManager.dataSource.effects.glucoseNoise = glucose
- }
- default:
- assertionFailure()
- }
- case .glucoseThresholds:
- if let glucose = controller.glucose {
- switch GlucoseThresholds(rawValue: indexPath.row)! {
- case .cgmLowerLimit:
- cgmManager.mockSensorState.cgmLowerLimit = glucose
- case .urgentLowGlucoseThreshold:
- cgmManager.mockSensorState.urgentLowGlucoseThreshold = glucose
- case .lowGlucoseThreshold:
- cgmManager.mockSensorState.lowGlucoseThreshold = glucose
- case .highGlucoseThreshold:
- cgmManager.mockSensorState.highGlucoseThreshold = glucose
- case .cgmUpperLimit:
- cgmManager.mockSensorState.cgmUpperLimit = glucose
- default:
- assertionFailure()
- }
- }
- default:
- assertionFailure()
- }
- tableView.reloadRows(at: [indexPath], with: .automatic)
- }
- }
- extension MockCGMManagerSettingsViewController: SineCurveParametersTableViewControllerDelegate {
- func sineCurveParametersTableViewControllerDidUpdateParameters(_ controller: SineCurveParametersTableViewController) {
- if let parameters = controller.parameters {
- cgmManager.dataSource.model = .sineCurve(parameters: parameters)
- cgmManager.retractSignalLossAlert()
- tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic)
- }
- }
- }
- extension MockCGMManagerSettingsViewController: RandomOutlierTableViewControllerDelegate {
- func randomOutlierTableViewControllerDidChangeOutlier(_ controller: RandomOutlierTableViewController) {
- guard let indexPath = controller.indexPath else {
- assertionFailure()
- return
- }
- switch Section(rawValue: indexPath.section)! {
- case .effects:
- switch EffectsRow(rawValue: indexPath.row)! {
- case .lowOutlier:
- cgmManager.dataSource.effects.randomLowOutlier = controller.randomOutlier
- case .highOutlier:
- cgmManager.dataSource.effects.randomHighOutlier = controller.randomOutlier
- default:
- assertionFailure()
- }
- default:
- assertionFailure()
- }
- tableView.reloadRows(at: [indexPath], with: .automatic)
- }
- }
- extension MockCGMManagerSettingsViewController: PercentageTextFieldTableViewControllerDelegate {
- func percentageTextFieldTableViewControllerDidChangePercentage(_ controller: PercentageTextFieldTableViewController) {
- guard let indexPath = controller.indexPath else {
- assertionFailure()
- return
- }
- switch Section(rawValue: indexPath.section)! {
- case .effects:
- switch EffectsRow(rawValue: indexPath.row)! {
- case .error:
- cgmManager.dataSource.effects.randomErrorChance = controller.percentage?.clamped(to: 0...1)
- default:
- assertionFailure()
- }
- case .cgmStatus:
- switch CGMStatusRow(rawValue: indexPath.row)! {
- case .batteryRemaining:
- if let batteryRemaining = controller.percentage.map({ $0.clamped(to: 0...1) }) {
- cgmManager.cgmBatteryChargeRemaining = batteryRemaining
- }
- default:
- assertionFailure()
- }
- case .lifecycleProgress:
- switch LifecycleProgressRow(rawValue: indexPath.row)! {
- case .percentComplete:
- if let percentComplete = controller.percentage.map({ $0.clamped(to: 0...1) }) {
- cgmManager.mockSensorState.cgmLifecycleProgress = MockCGMLifecycleProgress(percentComplete: percentComplete)
- } else {
- cgmManager.mockSensorState.cgmLifecycleProgress = nil
- }
- case .warningThreshold:
- cgmManager.mockSensorState.progressWarningThresholdPercentValue = controller.percentage.map { $0.clamped(to: 0...1) }
- case .criticalThreshold:
- cgmManager.mockSensorState.progressCriticalThresholdPercentValue = controller.percentage.map { $0.clamped(to: 0...1) }
- }
- default:
- assertionFailure()
- }
- tableView.reloadRows(at: [indexPath], with: .automatic)
- }
- }
- extension MockCGMManagerSettingsViewController: GlucoseTrendTableViewControllerDelegate {
- func glucoseTrendTableViewControllerDidChangeTrend(_ controller: GlucoseTrendTableViewController) {
- cgmManager.mockSensorState.trendType = controller.glucoseTrend
- tableView.reloadRows(at: [[Section.history.rawValue, HistoryRow.trend.rawValue]], with: .automatic)
- }
- }
- extension MockCGMManagerSettingsViewController: MeasurementFrequencyTableViewControllerDelegate {
- func measurementFrequencyTableViewControllerDidChangeFrequency(_ controller: MeasurementFrequencyTableViewController) {
- if let measurementFrequency = controller.measurementFrequency {
- cgmManager.dataSource.dataPointFrequency = measurementFrequency
- cgmManager.updateGlucoseUpdateTimer()
- tableView.reloadRows(at: [[Section.model.rawValue, ModelRow.frequency.rawValue]], with: .automatic)
- }
- }
- }
- private extension UIAlertController {
- convenience init(cgmDeletionHandler confirmHandler: @escaping () -> Void, cancelHandler: (() -> Void)? = nil) {
- self.init(
- title: nil,
- message: NSLocalizedString("Are you sure you want to delete this CGM?", comment: ""),
- preferredStyle: .actionSheet
- )
- addAction(UIAlertAction(
- title: "Delete CGM",
- style: .destructive,
- handler: { _ in
- confirmHandler()
- }
- ))
- let cancel = "Cancel"
- addAction(UIAlertAction(title: cancel, style: .cancel, handler: { _ in cancelHandler?() }))
- }
- }
|