// // 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() 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 = { [unowned cgmManager] isOn in 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 = { 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 }) 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 cgmManager] isOn in 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( 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: "Are you sure you want to delete this CGM?", preferredStyle: .actionSheet ) addAction(UIAlertAction( title: "Delete CGM", style: .destructive, handler: { _ in confirmHandler() } )) let cancel = "Cancel" addAction(UIAlertAction(title: cancel, style: .cancel, handler: { _ in cancelHandler?() })) } }