From 612401bd3d37d8fbd0586079e61cd4261c979574 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 17 Apr 2017 19:08:11 -0500 Subject: [PATCH] Refactoring LoopDataManager prediction calculation --- Loop/Managers/DiagnosticLogger+LoopKit.swift | 50 ---- Loop/Managers/LoopDataManager.swift | 219 ++++++++---------- Loop/Managers/WatchDataManager.swift | 4 +- Loop/Models/BolusRecommendation.swift | 11 + Loop/Models/LoopSettings.swift | 11 + Loop/Models/PredictionInputEffect.swift | 38 +-- .../PredictionTableViewController.swift | 35 +-- .../StatusTableViewController.swift | 4 +- .../ComplicationController.swift | 4 +- .../StatusInterfaceController.swift | 2 +- 10 files changed, 165 insertions(+), 213 deletions(-) diff --git a/Loop/Managers/DiagnosticLogger+LoopKit.swift b/Loop/Managers/DiagnosticLogger+LoopKit.swift index e70cf3f819..5e610942a6 100644 --- a/Loop/Managers/DiagnosticLogger+LoopKit.swift +++ b/Loop/Managers/DiagnosticLogger+LoopKit.swift @@ -25,54 +25,4 @@ extension DiagnosticLogger { func addError(_ message: Error, fromSource source: String) { addError(String(describing: message), fromSource: source) } - - func addLoopStatus(startDate: Date, endDate: Date, glucose: GlucoseValue, effects: [String: [GlucoseEffect]], error: Error?, prediction: [GlucoseValue], predictionWithRetrospectiveEffect: Double, eventualBGWithRetrospectiveEffect: Double, eventualBGWithoutMomentum: Double, recommendedTempBasal: LoopDataManager.TempBasalRecommendation?) { - - let dateFormatter = DateFormatter.ISO8601StrictDateFormatter() - let unit = HKUnit.milligramsPerDeciliterUnit() - - var message: [String: Any] = [ - "startDate": dateFormatter.string(from: startDate), - "duration": endDate.timeIntervalSince(startDate), - "glucose": [ - "startDate": dateFormatter.string(from: glucose.startDate), - "value": glucose.quantity.doubleValue(for: unit), - "unit": unit.unitString - ], - "input": effects.reduce([:], { (previous, item) -> [String: Any] in - var input = previous - input[item.0] = item.1.map { - [ - "startDate": dateFormatter.string(from: $0.startDate), - "value": $0.quantity.doubleValue(for: unit), - "unit": unit.unitString - ] - } - return input - }), - "prediction": prediction.map { (value) -> [String: Any] in - [ - "startDate": dateFormatter.string(from: value.startDate), - "value": value.quantity.doubleValue(for: unit), - "unit": unit.unitString - ] - }, - "prediction_retrospect_delta": predictionWithRetrospectiveEffect, - "eventualBGWithRetrospectiveEffect": eventualBGWithRetrospectiveEffect, - "eventualBGWithoutMomentum": eventualBGWithoutMomentum - ] - - if let error = error { - message["error"] = String(describing: error) - } - - if let recommendedTempBasal = recommendedTempBasal { - message["recommendedTempBasal"] = [ - "rate": recommendedTempBasal.rate, - "minutes": recommendedTempBasal.duration.minutes - ] - } - - addMessage(message, toCollection: "loop") - } } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index ccdebef422..609c0941f1 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -181,6 +181,9 @@ final class LoopDataManager { } } + /// The amount of time since a given date that data should be considered valid + var recencyInterval = TimeInterval(minutes: 15) + /// Sets a new time zone for a the schedule-based settings /// /// - Parameter timeZone: The time zone @@ -328,6 +331,12 @@ final class LoopDataManager { // Actions + func enactRecommendedTempBasal(_ completion: @escaping (_ error: Error?) -> Void) { + dataAccessQueue.async { + self.setRecommendedTempBasal(completion) + } + } + /// Runs the "loop" /// /// Executes an analysis of the current data, and recommends an adjustment to the current @@ -342,7 +351,7 @@ final class LoopDataManager { try self.update() if self.settings.dosingEnabled { - self.setRecommendedTempBasal { (success, error) -> Void in + self.setRecommendedTempBasal { (error) -> Void in self.lastLoopError = error if let error = error { @@ -366,6 +375,27 @@ final class LoopDataManager { } } + func getPredictedGlucose(using inputs: PredictionInputEffect, completion: @escaping (_ prediction: Result<[GlucoseValue]>) -> Void) { + dataAccessQueue.async { + do { + completion(.success(try self.predictGlucoseFromCurrentData(using: inputs))) + } catch let error { + completion(.failure(error)) + } + } + } + + func getRecommendedBolus(_ resultsHandler: @escaping (_ units: BolusRecommendation?, _ error: Error?) -> Void) { + dataAccessQueue.async { + do { + let recommendation = try self.recommendBolus() + resultsHandler(recommendation, nil) + } catch let error { + resultsHandler(nil, error) + } + } + } + // References to registered notification center observers private var carbUpdateObserver: Any? @@ -376,6 +406,7 @@ final class LoopDataManager { } private func update() throws { + dispatchPrecondition(condition: .onQueue(dataAccessQueue)) let updateGroup = DispatchGroup() // Fetch glucose effects as far back as we want to make retroactive analysis @@ -533,6 +564,8 @@ final class LoopDataManager { **/ private func getPendingInsulin() throws -> Double { + dispatchPrecondition(condition: .onQueue(dataAccessQueue)) + guard let basalRates = basalRateSchedule else { throw LoopError.configurationError("Basal Rate Schedule") } @@ -556,39 +589,33 @@ final class LoopDataManager { return pendingTempBasalInsulin + pendingBolusAmount } - func modelPredictedGlucose(using inputs: [PredictionInputEffect], resultsHandler: @escaping (_ predictedGlucose: [GlucoseValue]?, _ error: Error?) -> Void) { - dataAccessQueue.async { - guard let glucose = self.glucoseStore.latestGlucose else { - resultsHandler(nil, LoopError.missingDataError(details: "Cannot predict glucose due to missing input data", recovery: "Check your CGM data source")) - return - } + private func predictGlucoseFromCurrentData(using inputs: PredictionInputEffect) throws -> [GlucoseValue] { + dispatchPrecondition(condition: .onQueue(dataAccessQueue)) - var momentum: [GlucoseEffect] = [] - var effects: [[GlucoseEffect]] = [] + guard let glucose = self.glucoseStore.latestGlucose else { + throw LoopError.missingDataError(details: "Cannot predict glucose due to missing input data", recovery: "Check your CGM data source") + } - for input in inputs { - switch input { - case .carbs: - if let carbEffect = self.carbEffect { - effects.append(carbEffect) - } - case .insulin: - if let insulinEffect = self.insulinEffect { - effects.append(insulinEffect) - } - case .momentum: - if let momentumEffect = self.glucoseMomentumEffect { - momentum = momentumEffect - } - case .retrospection: - effects.append(self.retrospectiveGlucoseEffect) - } - } + var momentum: [GlucoseEffect] = [] + var effects: [[GlucoseEffect]] = [] + + if inputs.contains(.carbs), let carbEffect = self.carbEffect { + effects.append(carbEffect) + } + + if inputs.contains(.insulin), let insulinEffect = self.insulinEffect { + effects.append(insulinEffect) + } - let prediction = LoopMath.predictGlucose(glucose, momentum: momentum, effects: effects) + if inputs.contains(.momentum), let momentumEffect = self.glucoseMomentumEffect { + momentum = momentumEffect + } - resultsHandler(prediction, nil) + if inputs.contains(.retrospection) { + effects.append(self.retrospectiveGlucoseEffect) } + + return LoopMath.predictGlucose(glucose, momentum: momentum, effects: effects) } // Calculation @@ -627,10 +654,8 @@ final class LoopDataManager { private var predictedGlucose: [GlucoseValue]? { didSet { recommendedTempBasal = nil - predictedGlucoseWithoutMomentum = nil } } - private var predictedGlucoseWithoutMomentum: [GlucoseValue]? private var retrospectivePredictedGlucose: [GlucoseValue]? { didSet { retrospectiveGlucoseEffect = [] @@ -665,7 +690,9 @@ final class LoopDataManager { *This method should only be called from the `dataAccessQueue`* */ - private func updateRetrospectiveGlucoseEffect() throws { + private func updateRetrospectiveGlucoseEffect(effectDuration: TimeInterval = TimeInterval(minutes: 60)) throws { + dispatchPrecondition(condition: .onQueue(dataAccessQueue)) + guard let carbEffect = self.carbEffect, let insulinEffect = self.insulinEffect @@ -698,7 +725,7 @@ final class LoopDataManager { let type = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bloodGlucose)! let glucose = HKQuantitySample(type: type, quantity: change.end.quantity, start: change.end.startDate, end: change.end.endDate) - self.retrospectiveGlucoseEffect = LoopMath.decayEffect(from: glucose, atRate: velocity, for: TimeInterval(minutes: 60)) + self.retrospectiveGlucoseEffect = LoopMath.decayEffect(from: glucose, atRate: velocity, for: effectDuration) } /** @@ -707,6 +734,8 @@ final class LoopDataManager { *This method should only be called from the `dataAccessQueue`* */ private func updatePredictedGlucoseAndRecommendedBasal() throws { + dispatchPrecondition(condition: .onQueue(dataAccessQueue)) + guard let glucose = glucoseStore.latestGlucose else { self.predictedGlucose = nil throw LoopError.missingDataError(details: "Glucose", recovery: "Check your CGM data source") @@ -718,7 +747,6 @@ final class LoopDataManager { } let startDate = Date() - let recencyInterval = TimeInterval(minutes: 15) guard startDate.timeIntervalSince(glucose.startDate) <= recencyInterval else { self.predictedGlucose = nil @@ -730,57 +758,13 @@ final class LoopDataManager { throw LoopError.pumpDataTooOld(date: pumpStatusDate) } - guard let - momentum = self.glucoseMomentumEffect, - let carbEffect = self.carbEffect, - let insulinEffect = self.insulinEffect - else { + guard glucoseMomentumEffect != nil, carbEffect != nil, insulinEffect != nil else { self.predictedGlucose = nil throw LoopError.missingDataError(details: "Glucose effects", recovery: nil) } - var error: Error? - - let prediction = LoopMath.predictGlucose(glucose, momentum: momentum, effects: carbEffect, insulinEffect) - let predictionWithRetrospectiveEffect = LoopMath.predictGlucose(glucose, momentum: momentum, effects: carbEffect, insulinEffect, retrospectiveGlucoseEffect) - let predictionWithoutMomentum = LoopMath.predictGlucose(glucose, effects: carbEffect, insulinEffect) - - let predictDiff: Double - - let unit = HKUnit.milligramsPerDeciliterUnit() - if let lastA = prediction.last?.quantity.doubleValue(for: unit), - let lastB = predictionWithRetrospectiveEffect.last?.quantity.doubleValue(for: unit) - { - predictDiff = lastB - lastA - } else { - predictDiff = 0 - } - - let eventualBGWithRetrospectiveEffect: Double = predictionWithRetrospectiveEffect.last?.quantity.doubleValue(for: unit) ?? 0 - let eventualBGWithoutMomentum: Double = predictionWithoutMomentum.last?.quantity.doubleValue(for: unit) ?? 0 - - defer { - logger.addLoopStatus( - startDate: startDate, - endDate: Date(), - glucose: glucose, - effects: [ - "momentum": momentum, - "carbs": carbEffect, - "insulin": insulinEffect, - "retrospective_glucose": retrospectiveGlucoseEffect - ], - error: error, - prediction: prediction, - predictionWithRetrospectiveEffect: predictDiff, - eventualBGWithRetrospectiveEffect: eventualBGWithRetrospectiveEffect, - eventualBGWithoutMomentum: eventualBGWithoutMomentum, - recommendedTempBasal: recommendedTempBasal - ) - } - - self.predictedGlucose = settings.retrospectiveCorrectionEnabled ? predictionWithRetrospectiveEffect : prediction - self.predictedGlucoseWithoutMomentum = predictionWithoutMomentum + let predictedGlucose = try predictGlucoseFromCurrentData(using: settings.enabledEffects) + self.predictedGlucose = predictedGlucose guard let minimumBGGuard = settings.minimumBGGuard else { throw LoopError.configurationError("Minimum BG Guard") @@ -792,13 +776,11 @@ final class LoopDataManager { let insulinSensitivity = insulinSensitivitySchedule, let basalRates = basalRateSchedule else { - error = LoopError.configurationError("Check settings") - throw error! + throw LoopError.configurationError("Check settings") } guard lastBolus == nil, // Don't recommend changes if a bolus was just set - let predictedGlucose = self.predictedGlucose, let tempBasal = DoseMath.recommendTempBasalFromPredictedGlucose(predictedGlucose, lastTempBasal: lastTempBasal, maxBasalRate: maxBasal, @@ -815,14 +797,16 @@ final class LoopDataManager { recommendedTempBasal = (recommendedDate: Date(), rate: tempBasal.rate, duration: tempBasal.duration) } + /// *This method should only be called from the `dataAccessQueue`* private func recommendBolus() throws -> BolusRecommendation { + dispatchPrecondition(condition: .onQueue(dataAccessQueue)) + guard let minimumBGGuard = settings.minimumBGGuard else { throw LoopError.configurationError("Minimum BG Guard") } - guard let - glucose = predictedGlucose, - let glucoseWithoutMomentum = predictedGlucoseWithoutMomentum, + guard + let predictedGlucose = predictedGlucose, let maxBolus = settings.maximumBolus, let glucoseTargetRange = settings.glucoseTargetRangeSchedule, let insulinSensitivity = insulinSensitivitySchedule, @@ -831,9 +815,7 @@ final class LoopDataManager { throw LoopError.configurationError("Check Settings") } - let recencyInterval = TimeInterval(minutes: 15) - - guard let glucoseDate = glucose.first?.startDate else { + guard let glucoseDate = predictedGlucose.first?.startDate else { throw LoopError.missingDataError(details: "No glucose data found", recovery: "Check your CGM source") } @@ -843,7 +825,7 @@ final class LoopDataManager { let pendingInsulin = try self.getPendingInsulin() - let recommendationWithMomentum = DoseMath.recommendBolusFromPredictedGlucose(glucose, + let recommendation = DoseMath.recommendBolusFromPredictedGlucose(predictedGlucose, maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivity, @@ -852,41 +834,34 @@ final class LoopDataManager { minimumBGGuard: minimumBGGuard ) - let recommendationWithoutMomentum = DoseMath.recommendBolusFromPredictedGlucose(glucoseWithoutMomentum, - maxBolus: maxBolus, - glucoseTargetRange: glucoseTargetRange, - insulinSensitivity: insulinSensitivity, - basalRateSchedule: basalRates, - pendingInsulin: pendingInsulin, - minimumBGGuard: minimumBGGuard + // TODO: This was added in https://github.com/LoopKit/Loop/issues/370, but concerns were raised + // that this contradicts users expectations in https://github.com/LoopKit/Loop/issues/435 + let bolusSafetyEffects = settings.enabledEffects.subtracting([.retrospection, .momentum]) + + return min(recommendation, + DoseMath.recommendBolusFromPredictedGlucose( + try predictGlucoseFromCurrentData(using: bolusSafetyEffects), + maxBolus: maxBolus, + glucoseTargetRange: glucoseTargetRange, + insulinSensitivity: insulinSensitivity, + basalRateSchedule: basalRates, + pendingInsulin: pendingInsulin, + minimumBGGuard: minimumBGGuard + ) ) - - if recommendationWithMomentum.amount > recommendationWithoutMomentum.amount { - return recommendationWithoutMomentum - } else { - return recommendationWithMomentum - } } - func getRecommendedBolus(_ resultsHandler: @escaping (_ units: BolusRecommendation?, _ error: Error?) -> Void) { - dataAccessQueue.async { - do { - let recommendation = try self.recommendBolus() - resultsHandler(recommendation, nil) - } catch let error { - resultsHandler(nil, error) - } - } - } + /// *This method should only be called from the `dataAccessQueue`* + private func setRecommendedTempBasal(_ completion: @escaping (_ error: Error?) -> Void) { + dispatchPrecondition(condition: .onQueue(dataAccessQueue)) - private func setRecommendedTempBasal(_ resultsHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) { guard let recommendedTempBasal = self.recommendedTempBasal else { - resultsHandler(true, nil) + completion(nil) return } guard abs(recommendedTempBasal.recommendedDate.timeIntervalSinceNow) < TimeInterval(minutes: 5) else { - resultsHandler(false, LoopError.recommendationExpired(date: recommendedTempBasal.recommendedDate)) + completion(LoopError.recommendationExpired(date: recommendedTempBasal.recommendedDate)) return } @@ -897,19 +872,13 @@ final class LoopDataManager { self.lastTempBasal = basal self.recommendedTempBasal = nil - resultsHandler(true, nil) + completion(nil) case .failure(let error): - resultsHandler(false, error) + completion(error) } } } } - - func enactRecommendedTempBasal(_ resultsHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) { - dataAccessQueue.async { - self.setRecommendedTempBasal(resultsHandler) - } - } } extension LoopDataManager { diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 6f9454d361..e0f25e6810 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -66,14 +66,13 @@ final class WatchDataManager: NSObject, WCSessionDelegate { private func sendWatchContext(_ context: WatchContext) { if let session = watchSession, session.isPaired && session.isWatchAppInstalled { - let complicationShouldUpdate: Bool if let lastContext = lastComplicationContext, let lastGlucose = lastContext.glucose, let lastGlucoseDate = lastContext.glucoseDate, let newGlucose = context.glucose, let newGlucoseDate = context.glucoseDate { - let enoughTimePassed = newGlucoseDate.timeIntervalSince(lastGlucoseDate as Date).minutes >= 30 + let enoughTimePassed = newGlucoseDate.timeIntervalSince(lastGlucoseDate).minutes >= 30 let enoughTrendDrift = abs(newGlucose.doubleValue(for: minTrendUnit) - lastGlucose.doubleValue(for: minTrendUnit)) >= minTrendDrift complicationShouldUpdate = enoughTimePassed || enoughTrendDrift @@ -95,7 +94,6 @@ final class WatchDataManager: NSObject, WCSessionDelegate { } private func createWatchContext(_ completionHandler: @escaping (_ context: WatchContext?) -> Void) { - let glucose = deviceDataManager.loopManager.glucoseStore.latestGlucose let reservoir = deviceDataManager.loopManager.doseStore.lastReservoirValue let maxBolus = deviceDataManager.loopManager.settings.maximumBolus diff --git a/Loop/Models/BolusRecommendation.swift b/Loop/Models/BolusRecommendation.swift index e72e1d3f9d..9fdb83e3c4 100644 --- a/Loop/Models/BolusRecommendation.swift +++ b/Loop/Models/BolusRecommendation.swift @@ -73,3 +73,14 @@ struct BolusRecommendation { self.notice = notice } } + +extension BolusRecommendation: Comparable { + static func ==(lhs: BolusRecommendation, rhs: BolusRecommendation) -> Bool { + return lhs.amount == rhs.amount + } + + static func <(lhs: BolusRecommendation, rhs: BolusRecommendation) -> Bool { + return lhs.amount < rhs.amount + } +} + diff --git a/Loop/Models/LoopSettings.swift b/Loop/Models/LoopSettings.swift index a88c5317be..be8490fe51 100644 --- a/Loop/Models/LoopSettings.swift +++ b/Loop/Models/LoopSettings.swift @@ -23,6 +23,17 @@ struct LoopSettings { } +extension LoopSettings { + var enabledEffects: PredictionInputEffect { + var inputs = PredictionInputEffect.all + if !retrospectiveCorrectionEnabled { + inputs.remove(.retrospection) + } + return inputs + } +} + + extension LoopSettings: RawRepresentable { typealias RawValue = [String: Any] private static let version = 1 diff --git a/Loop/Models/PredictionInputEffect.swift b/Loop/Models/PredictionInputEffect.swift index 1b257e3d51..9aabe0d918 100644 --- a/Loop/Models/PredictionInputEffect.swift +++ b/Loop/Models/PredictionInputEffect.swift @@ -10,35 +10,43 @@ import Foundation import HealthKit -enum PredictionInputEffect { - case carbs - case insulin - case momentum - case retrospection +struct PredictionInputEffect: OptionSet { + let rawValue: Int - var localizedTitle: String { + static let carbs = PredictionInputEffect(rawValue: 1 << 0) + static let insulin = PredictionInputEffect(rawValue: 1 << 1) + static let momentum = PredictionInputEffect(rawValue: 1 << 2) + static let retrospection = PredictionInputEffect(rawValue: 1 << 3) + + static let all: PredictionInputEffect = [.carbs, .insulin, .momentum, .retrospection] + + var localizedTitle: String? { switch self { - case .carbs: + case [.carbs]: return NSLocalizedString("Carbohydrates", comment: "Title of the prediction input effect for carbohydrates") - case .insulin: + case [.insulin]: return NSLocalizedString("Insulin", comment: "Title of the prediction input effect for insulin") - case .momentum: + case [.momentum]: return NSLocalizedString("Glucose Momentum", comment: "Title of the prediction input effect for glucose momentum") - case .retrospection: + case [.retrospection]: return NSLocalizedString("Retrospective Correction", comment: "Title of the prediction input effect for retrospective correction") + default: + return nil } } - func localizedDescription(forGlucoseUnit unit: HKUnit) -> String { + func localizedDescription(forGlucoseUnit unit: HKUnit) -> String? { switch self { - case .carbs: + case [.carbs]: return String(format: NSLocalizedString("Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)", comment: "Description of the prediction input effect for carbohydrates. (1: The glucose unit string)"), unit.glucoseUnitDisplayString) - case .insulin: + case [.insulin]: return String(format: NSLocalizedString("Insulin Absorbed (U) × Insulin Sensitivity (%1$@/U)", comment: "Description of the prediction input effect for insulin"), unit.glucoseUnitDisplayString) - case .momentum: + case [.momentum]: return NSLocalizedString("15 min glucose regression coefficient (b₁), continued with decay over 30 min", comment: "Description of the prediction input effect for glucose momentum") - case .retrospection: + case [.retrospection]: return NSLocalizedString("30 min comparison of glucose prediction vs actual, continued with decay over 60 min", comment: "Description of the prediction input effect for retrospective correction") + default: + return nil } } } diff --git a/Loop/View Controllers/PredictionTableViewController.swift b/Loop/View Controllers/PredictionTableViewController.swift index ee7c22a223..db5652e4df 100644 --- a/Loop/View Controllers/PredictionTableViewController.swift +++ b/Loop/View Controllers/PredictionTableViewController.swift @@ -164,15 +164,19 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass, U } reloadGroup.enter() - self.dataManager.loopManager.modelPredictedGlucose(using: self.selectedInputs.flatMap { $0.selected ? $0.input : nil }) { (predictedGlucose, error) in - if error != nil { + self.dataManager.loopManager.getPredictedGlucose(using: self.selectedInputs) { (result) in + switch result { + case .failure: self.needsRefresh = true + self.charts.setAlternatePredictedGlucoseValues([]) + case .success(let predictedGlucose): + self.charts.setAlternatePredictedGlucoseValues(predictedGlucose) } - self.charts.setAlternatePredictedGlucoseValues(predictedGlucose ?? []) - if let lastPoint = self.charts.alternatePredictedGlucosePoints?.last?.y { self.eventualGlucoseDescription = String(describing: lastPoint) + } else { + self.eventualGlucoseDescription = nil } reloadGroup.leave() @@ -211,9 +215,9 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass, U private var eventualGlucoseDescription: String? - private lazy var selectedInputs: [(input: PredictionInputEffect, selected: Bool)] = [ - (.carbs, true), (.insulin, true), (.momentum, true), (.retrospection, true) - ] + private var availableInputs: [PredictionInputEffect] = [.carbs, .insulin, .momentum, .retrospection] + + private var selectedInputs = PredictionInputEffect.all override func numberOfSections(in tableView: UITableView) -> Int { return Section.count @@ -224,7 +228,7 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass, U case .charts: return 1 case .inputs: - return selectedInputs.count + return availableInputs.count case .settings: return 1 } @@ -251,13 +255,13 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass, U case .inputs: let cell = tableView.dequeueReusableCell(withIdentifier: PredictionInputEffectTableViewCell.className, for: indexPath) as! PredictionInputEffectTableViewCell - let (input, selected) = selectedInputs[indexPath.row] + let input = availableInputs[indexPath.row] cell.titleLabel?.text = input.localizedTitle - cell.accessoryType = selected ? .checkmark : .none + cell.accessoryType = selectedInputs.contains(input) ? .checkmark : .none cell.enabled = input != .retrospection || dataManager.loopManager.settings.retrospectiveCorrectionEnabled - var subtitleText = input.localizedDescription(forGlucoseUnit: charts.glucoseUnit) + var subtitleText = input.localizedDescription(forGlucoseUnit: charts.glucoseUnit) ?? "" if input == .retrospection, let startGlucose = retrospectivePredictedGlucose?.first, @@ -330,13 +334,14 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass, U override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard Section(rawValue: indexPath.section) == .inputs else { return } - let (input, selected) = selectedInputs[indexPath.row] + let input = availableInputs[indexPath.row] + let isSelected = selectedInputs.contains(input) if let cell = tableView.cellForRow(at: indexPath) { - cell.accessoryType = !selected ? .checkmark : .none + cell.accessoryType = !isSelected ? .checkmark : .none } - selectedInputs[indexPath.row] = (input, !selected) + selectedInputs.formSymmetricDifference(input) tableView.deselectRow(at: indexPath, animated: true) @@ -371,7 +376,7 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass, U @objc private func retrospectiveCorrectionSwitchChanged(_ sender: UISwitch) { dataManager.loopManager.settings.retrospectiveCorrectionEnabled = sender.isOn - if let row = selectedInputs.index(where: { $0.input == PredictionInputEffect.retrospection }), + if let row = availableInputs.index(where: { $0 == .retrospection }), let cell = tableView.cellForRow(at: IndexPath(row: row, section: Section.inputs.rawValue)) as? PredictionInputEffectTableViewCell { cell.enabled = self.dataManager.loopManager.settings.retrospectiveCorrectionEnabled diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index e6c241dae7..e3e0ae1895 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -656,14 +656,14 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize if recommendedTempBasal != nil && !settingTempBasal { settingTempBasal = true - self.dataManager.loopManager.enactRecommendedTempBasal { (success, error) -> Void in + self.dataManager.loopManager.enactRecommendedTempBasal { (error) in DispatchQueue.main.async { self.settingTempBasal = false if let error = error { self.dataManager.logger.addError(error, fromSource: "TempBasal") self.presentAlertController(with: error) - } else if success { + } else { self.refreshContext.update(with: .status) self.reloadData() } diff --git a/WatchApp Extension/ComplicationController.swift b/WatchApp Extension/ComplicationController.swift index 29d21d309c..ccf748ec08 100644 --- a/WatchApp Extension/ComplicationController.swift +++ b/WatchApp Extension/ComplicationController.swift @@ -20,7 +20,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { func getTimelineStartDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { if let date = ExtensionDelegate.shared().lastContext?.glucoseDate { - handler(date as Date) + handler(date) } else { handler(nil) } @@ -28,7 +28,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { if let date = ExtensionDelegate.shared().lastContext?.glucoseDate { - handler(date as Date) + handler(date) } else { handler(nil) } diff --git a/WatchApp Extension/Controllers/StatusInterfaceController.swift b/WatchApp Extension/Controllers/StatusInterfaceController.swift index 62a7781012..8aac1f18b1 100644 --- a/WatchApp Extension/Controllers/StatusInterfaceController.swift +++ b/WatchApp Extension/Controllers/StatusInterfaceController.swift @@ -25,7 +25,7 @@ final class StatusInterfaceController: WKInterfaceController, ContextUpdatable { lastContext = context if let date = context?.loopLastRunDate { - self.loopTimer.setDate(date as Date) + self.loopTimer.setDate(date) self.loopTimer.setHidden(false) self.loopTimer.start()