From 4685f37c0ae66b1da961e8ab3b799f0e9b606f9f Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 1 Oct 2018 19:51:49 -0500 Subject: [PATCH 1/5] Update LoopKit rev --- Cartfile.resolved | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cartfile.resolved b/Cartfile.resolved index 1676b87ad7..937b196088 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,7 +1,7 @@ github "LoopKit/Amplitude-iOS" "2137d5fd44bf630ed33e1e72d7af6d8f8612f270" -github "LoopKit/CGMBLEKit" "97433c2c546b50268bbfe2e1a42cb377c6066e78" -github "LoopKit/G4ShareSpy" "47931f1ee70f1081db6eebf3a68bb8f4146e5fca" -github "LoopKit/LoopKit" "8c553f7bd03c2032b146e4a52d58d527a4285c15" -github "LoopKit/dexcom-share-client-swift" "f0125aed8960586a9473dca02d17b3e5969558ac" +github "LoopKit/CGMBLEKit" "bc7c6bb2aaea48e501ecdbb379cd3a533f5df42e" +github "LoopKit/G4ShareSpy" "67208c66abfbb3bff813f047b6c40612338a335b" +github "LoopKit/LoopKit" "1c23bb5fd54c0b0aaa81dbabcb35f2d4c9c2e8cf" +github "LoopKit/dexcom-share-client-swift" "27fd38c28dcb16093ddf660c2b6b84ae34733352" github "i-schuetz/SwiftCharts" "0.6.2" -github "ps2/rileylink_ios" "49a43189c1606875d27133589c6d6b92fbd3a0a1" +github "ps2/rileylink_ios" "72590877d063ef6fc2a5a564210ea679fa9bebf6" From 0511d05e25629bae0c8a9b0d26f096c575e21abc Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 1 Oct 2018 21:23:35 -0500 Subject: [PATCH 2/5] Update rileylink_ios rev --- Cartfile.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cartfile.resolved b/Cartfile.resolved index 937b196088..46a30a96dc 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -4,4 +4,4 @@ github "LoopKit/G4ShareSpy" "67208c66abfbb3bff813f047b6c40612338a335b" github "LoopKit/LoopKit" "1c23bb5fd54c0b0aaa81dbabcb35f2d4c9c2e8cf" github "LoopKit/dexcom-share-client-swift" "27fd38c28dcb16093ddf660c2b6b84ae34733352" github "i-schuetz/SwiftCharts" "0.6.2" -github "ps2/rileylink_ios" "72590877d063ef6fc2a5a564210ea679fa9bebf6" +github "ps2/rileylink_ios" "5df08f32f11ac2cb6097ba1411349f05b9171e72" From fbb9f02ef61d11b06a1fe6b6b0f0d0ec9d0861d5 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 4 Oct 2018 18:32:43 -0500 Subject: [PATCH 3/5] Refactor RC as a timeline (#820) * Refactor RC as a timeline To provide support for a safe, accurate IRC implementation, this patch provides retrospective correction discrepancies as a timeline, which can be extended for use in integration. * Bump LoopKit dev revision --- Common/Models/LoopSettings.swift | 6 +- Loop/Base.lproj/Localizable.strings | 4 +- Loop/Extensions/CollectionType+Loop.swift | 2 +- Loop/Managers/DoseMath.swift | 2 +- Loop/Managers/LoopDataManager.swift | 128 ++++++++---------- .../PredictionTableViewController.swift | 17 ++- Loop/de.lproj/Localizable.strings | 3 - Loop/es.lproj/Localizable.strings | 3 - Loop/fr.lproj/Localizable.strings | 3 - Loop/it.lproj/Localizable.strings | 3 - Loop/nb.lproj/Localizable.strings | 3 - Loop/nl.lproj/Localizable.strings | 3 - Loop/pl.lproj/Localizable.strings | 3 - Loop/ru.lproj/Localizable.strings | 3 - Loop/zh-Hans.lproj/Localizable.strings | 3 - LoopUI/Extensions/CollectionType.swift | 8 +- 16 files changed, 79 insertions(+), 115 deletions(-) diff --git a/Common/Models/LoopSettings.swift b/Common/Models/LoopSettings.swift index 3e932b4813..1dcb0cd83a 100644 --- a/Common/Models/LoopSettings.swift +++ b/Common/Models/LoopSettings.swift @@ -23,7 +23,11 @@ struct LoopSettings { var retrospectiveCorrectionEnabled = true - let retrospectiveCorrectionInterval = TimeInterval(minutes: 30) + /// The interval over which to aggregate changes in glucose for retrospective correction + let retrospectiveCorrectionGroupingInterval = TimeInterval(minutes: 30) + + /// The maximum duration over which to integrate retrospective correction changes + let retrospectiveCorrectionIntegrationInterval = TimeInterval(minutes: 30) /// The amount of time since a given date that data should be considered valid let recencyInterval = TimeInterval(minutes: 15) diff --git a/Loop/Base.lproj/Localizable.strings b/Loop/Base.lproj/Localizable.strings index 795ecbbb05..fdcaafce26 100644 --- a/Loop/Base.lproj/Localizable.strings +++ b/Loop/Base.lproj/Localizable.strings @@ -247,8 +247,8 @@ /* The title text for the issue report cell */ "Issue Report" = "Issue Report"; -/* Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose) */ -"Last comparison: %1$@ → %2$@ vs %3$@" = "Last comparison: %1$@ → %2$@ vs %3$@"; +/* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ +"prediction-description-retrospective-correction" = "Predicted: %1$@\nActual: %2$@ (%3$@)"; /* Glucose HUD accessibility hint */ "Launches CGM app" = "Launches CGM app"; diff --git a/Loop/Extensions/CollectionType+Loop.swift b/Loop/Extensions/CollectionType+Loop.swift index 2af61c8f4e..6833b7a84c 100644 --- a/Loop/Extensions/CollectionType+Loop.swift +++ b/Loop/Extensions/CollectionType+Loop.swift @@ -10,7 +10,7 @@ import Foundation import LoopKit -public extension Sequence where Iterator.Element: TimelineValue { +public extension Sequence where Element: TimelineValue { /// Returns the closest element index in the sorted sequence prior to the specified date /// /// - parameter date: The date to use in the search diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 1192d01cc5..3366170c4a 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -201,7 +201,7 @@ private func targetGlucoseValue(percentEffectDuration: Double, minValue: Double, } -extension Collection where Iterator.Element == GlucoseValue { +extension Collection where Element == GlucoseValue { /// For a collection of glucose prediction, determine the least amount of insulin delivered at /// `date` to correct the predicted glucose to the middle of `correctionRange` at the time of prediction. diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 350415712e..32a455df17 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -22,8 +22,6 @@ final class LoopDataManager { static let LoopUpdateContextKey = "com.loudnate.Loop.LoopDataManager.LoopUpdateContext" - fileprivate typealias GlucoseChange = (start: GlucoseValue, end: GlucoseValue) - let carbStore: CarbStore let doseStore: DoseStore @@ -108,7 +106,6 @@ final class LoopDataManager { self.logger.info("Received notification of glucose samples changing") self.glucoseMomentumEffect = nil - self.retrospectiveGlucoseChange = nil self.notify(forChange: .glucose) } @@ -136,7 +133,7 @@ final class LoopDataManager { predictedGlucose = nil // Carb data may be back-dated, so re-calculate the retrospective glucose. - retrospectivePredictedGlucose = nil + retrospectiveGlucoseDiscrepancies = nil } } private var insulinEffect: [GlucoseEffect]? { @@ -155,12 +152,12 @@ final class LoopDataManager { } } - /// The change in glucose over the reflection time interval (default is 30 min) - fileprivate var retrospectiveGlucoseChange: GlucoseChange? { + private var retrospectiveGlucoseDiscrepancies: [GlucoseEffect]? { didSet { - retrospectivePredictedGlucose = nil + retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies?.combinedSums(of: settings.retrospectiveCorrectionGroupingInterval * 1.01) } } + private var retrospectiveGlucoseDiscrepanciesSummed: [GlucoseChange]? fileprivate var predictedGlucose: [GlucoseValue]? { didSet { @@ -168,11 +165,7 @@ final class LoopDataManager { recommendedBolus = nil } } - fileprivate var retrospectivePredictedGlucose: [GlucoseValue]? { - didSet { - retrospectiveGlucoseEffect = [] - } - } + fileprivate var recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? fileprivate var recommendedBolus: (recommendation: BolusRecommendation, date: Date)? @@ -615,19 +608,11 @@ extension LoopDataManager { throw LoopError.missingDataError(.glucose) } - let retrospectiveStart = lastGlucoseDate.addingTimeInterval(-settings.retrospectiveCorrectionInterval) + let retrospectiveStart = lastGlucoseDate.addingTimeInterval(-settings.retrospectiveCorrectionIntegrationInterval) let earliestEffectDate = Date(timeIntervalSinceNow: .hours(-24)) let nextEffectDate = insulinCounteractionEffects.last?.endDate ?? earliestEffectDate - if retrospectiveGlucoseChange == nil { - updateGroup.enter() - glucoseStore.getGlucoseChange(start: retrospectiveStart) { (change) in - self.retrospectiveGlucoseChange = change - updateGroup.leave() - } - } - if glucoseMomentumEffect == nil { updateGroup.enter() glucoseStore.getRecentMomentumEffect { (effects) -> Void in @@ -638,7 +623,7 @@ extension LoopDataManager { if insulinEffect == nil { updateGroup.enter() - doseStore.getGlucoseEffects(start: min(retrospectiveStart, nextEffectDate)) { (result) -> Void in + doseStore.getGlucoseEffects(start: nextEffectDate) { (result) -> Void in switch result { case .failure(let error): self.logger.error(error) @@ -700,7 +685,7 @@ extension LoopDataManager { _ = updateGroup.wait(timeout: .distantFuture) - if retrospectivePredictedGlucose == nil { + if retrospectiveGlucoseDiscrepancies == nil { do { try updateRetrospectiveGlucoseEffect() } catch let error { @@ -794,7 +779,7 @@ extension LoopDataManager { var prediction = LoopMath.predictGlucose(startingAt: glucose, momentum: momentum, effects: effects) // Dosing requires prediction entries at least as long as the insulin model duration. - // If our prediciton is shorter than that, then extend it here. + // If our prediction is shorter than that, then extend it here. let finalDate = glucose.startDate.addingTimeInterval(model.effectDuration) if let last = prediction.last, last.startDate < finalDate { prediction.append(PredictedGlucoseValue(startDate: finalDate, quantity: last.quantity)) @@ -803,52 +788,39 @@ extension LoopDataManager { return prediction } - /** - Runs the glucose retrospective analysis using the latest effect data. - - *This method should only be called from the `dataAccessQueue`* - */ + /// Generates an effect based on how large the discrepancy is between the current glucose and its predicted value. + /// + /// - Parameter effectDuration: The length of time to extend the effect + /// - Throws: LoopError.missingDataError private func updateRetrospectiveGlucoseEffect(effectDuration: TimeInterval = TimeInterval(minutes: 60)) throws { dispatchPrecondition(condition: .onQueue(dataAccessQueue)) - - guard let carbEffect = self.carbEffect else { - self.retrospectivePredictedGlucose = nil + + guard let carbEffects = self.carbEffect else { + retrospectiveGlucoseDiscrepancies = nil + retrospectiveGlucoseEffect = [] throw LoopError.missingDataError(.carbEffect) } - guard let insulinEffect = self.insulinEffect else { - self.retrospectivePredictedGlucose = nil - throw LoopError.missingDataError(.insulinEffect) - } + retrospectiveGlucoseDiscrepancies = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: carbStore.delta) - guard let change = retrospectiveGlucoseChange else { - self.retrospectivePredictedGlucose = nil - return // Expected case for calibrations + // Our last change should be recent, otherwise clear the effects + guard let discrepancy = retrospectiveGlucoseDiscrepanciesSummed?.last, + Date().timeIntervalSince(discrepancy.endDate) <= settings.recencyInterval + else { + retrospectiveGlucoseEffect = [] + return } - // Run a retrospective prediction over the duration of the recorded glucose change, using the current carb and insulin effects - let startDate = change.start.startDate - let endDate = change.end.endDate - let retrospectivePrediction = LoopMath.predictGlucose(startingAt: change.start, effects: - carbEffect.filterDateRange(startDate, endDate), - insulinEffect.filterDateRange(startDate, endDate) - ) - - self.retrospectivePredictedGlucose = retrospectivePrediction - - guard let lastGlucose = retrospectivePrediction.last else { return } - let glucoseUnit = HKUnit.milligramsPerDeciliter - let velocityUnit = glucoseUnit.unitDivided(by: HKUnit.second()) - - let discrepancy = change.end.quantity.doubleValue(for: glucoseUnit) - lastGlucose.quantity.doubleValue(for: glucoseUnit) // mg/dL + guard let glucose = self.glucoseStore.latestGlucose else { + retrospectiveGlucoseEffect = [] + throw LoopError.missingDataError(.glucose) + } - // Determine the interval of discrepancy, requiring a minimum of the configured interval to avoid magnifying effects from short intervals - let discrepancyTime = max(change.end.endDate.timeIntervalSince(change.start.endDate), settings.retrospectiveCorrectionInterval) - let velocity = HKQuantity(unit: velocityUnit, doubleValue: discrepancy / discrepancyTime) - let type = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bloodGlucose)! - let glucose = HKQuantitySample(type: type, quantity: change.end.quantity, start: change.end.startDate, end: change.end.endDate) + let unit = HKUnit.milligramsPerDeciliter + let discrepancyTime = max(discrepancy.endDate.timeIntervalSince(discrepancy.startDate), settings.retrospectiveCorrectionGroupingInterval) + let velocity = HKQuantity(unit: unit.unitDivided(by: .second()), doubleValue: discrepancy.quantity.doubleValue(for: unit) / discrepancyTime) - self.retrospectiveGlucoseEffect = glucose.decayEffect(atRate: velocity, for: effectDuration) + retrospectiveGlucoseEffect = glucose.decayEffect(atRate: velocity, for: effectDuration) } /// Runs the glucose prediction on the latest effect data. @@ -1003,9 +975,9 @@ protocol LoopState { var recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? { get } var recommendedBolus: (recommendation: BolusRecommendation, date: Date)? { get } - - /// The retrospective prediction over a recent period of glucose samples - var retrospectivePredictedGlucose: [GlucoseValue]? { get } + + /// The difference in predicted vs actual glucose over a recent period + var retrospectiveGlucoseDiscrepancies: [GlucoseChange]? { get } /// Calculates a new prediction from the current data using the specified effect inputs /// @@ -1063,9 +1035,9 @@ extension LoopDataManager { return loopDataManager.recommendedBolus } - var retrospectivePredictedGlucose: [GlucoseValue]? { + var retrospectiveGlucoseDiscrepancies: [GlucoseChange]? { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) - return loopDataManager.retrospectivePredictedGlucose + return loopDataManager.retrospectiveGlucoseDiscrepanciesSummed } func predictGlucose(using inputs: PredictionInputEffect) throws -> [GlucoseValue] { @@ -1105,7 +1077,7 @@ extension LoopDataManager { func generateDiagnosticReport(_ completion: @escaping (_ report: String) -> Void) { getLoopState { (manager, state) in - var entries = [ + var entries: [String] = [ "## LoopDataManager", "settings: \(String(reflecting: manager.settings))", @@ -1117,16 +1089,16 @@ extension LoopDataManager { "]", "insulinEffect: [", - "* GlucoseEffect(start, end, md/dL", + "* GlucoseEffect(start, mg/dL)", (manager.insulinEffect ?? []).reduce(into: "", { (entries, entry) in - entries.append("* \(entry.startDate), \(entry.endDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") + entries.append("* \(entry.startDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") }), "]", "carbEffect: [", - "* GlucoseEffect(start, end, md/dL", + "* GlucoseEffect(start, mg/dL)", (manager.carbEffect ?? []).reduce(into: "", { (entries, entry) in - entries.append("* \(entry.startDate), \(entry.endDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") + entries.append("* \(entry.startDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") }), "]", @@ -1137,13 +1109,25 @@ extension LoopDataManager { }), "]", - "retrospectivePredictedGlucose: \(state.retrospectivePredictedGlucose ?? [])", + "retrospectiveGlucoseDiscrepancies: [", + "* GlucoseEffect(start, mg/dL)", + (manager.retrospectiveGlucoseDiscrepancies ?? []).reduce(into: "", { (entries, entry) in + entries.append("* \(entry.startDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") + }), + "]", + + "retrospectiveGlucoseDiscrepanciesSummed: [", + "* GlucoseChange(start, end, mg/dL)", + (manager.retrospectiveGlucoseDiscrepanciesSummed ?? []).reduce(into: "", { (entries, entry) in + entries.append("* \(entry.startDate), \(entry.endDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") + }), + "]", + "glucoseMomentumEffect: \(manager.glucoseMomentumEffect ?? [])", "retrospectiveGlucoseEffect: \(manager.retrospectiveGlucoseEffect)", "recommendedTempBasal: \(String(describing: state.recommendedTempBasal))", "recommendedBolus: \(String(describing: state.recommendedBolus))", "lastBolus: \(String(describing: manager.lastRequestedBolus))", - "retrospectiveGlucoseChange: \(String(describing: manager.retrospectiveGlucoseChange))", "lastLoopCompleted: \(String(describing: manager.lastLoopCompleted))", "lastTempBasal: \(String(describing: state.lastTempBasal))", "carbsOnBoard: \(String(describing: state.carbsOnBoard))", diff --git a/Loop/View Controllers/PredictionTableViewController.swift b/Loop/View Controllers/PredictionTableViewController.swift index fdb529f954..14e8375b56 100644 --- a/Loop/View Controllers/PredictionTableViewController.swift +++ b/Loop/View Controllers/PredictionTableViewController.swift @@ -67,7 +67,7 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas // MARK: - State - private var retrospectivePredictedGlucose: [GlucoseValue]? + private var retrospectiveGlucoseDiscrepancies: [GlucoseChange]? private var refreshContext = RefreshContext.all @@ -113,7 +113,7 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas _ = self.refreshContext.remove(.status) reloadGroup.enter() self.deviceManager.loopManager.getLoopState { (manager, state) in - self.retrospectivePredictedGlucose = state.retrospectivePredictedGlucose + self.retrospectiveGlucoseDiscrepancies = state.retrospectiveGlucoseDiscrepancies self.charts.setPredictedGlucoseValues(state.predictedGlucose ?? []) do { @@ -256,15 +256,18 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas var subtitleText = input.localizedDescription(forGlucoseUnit: charts.glucoseUnit) ?? "" if input == .retrospection, - let startGlucose = retrospectivePredictedGlucose?.first, - let endGlucose = retrospectivePredictedGlucose?.last, + let lastDiscrepancy = retrospectiveGlucoseDiscrepancies?.last, let currentGlucose = self.deviceManager.loopManager.glucoseStore.latestGlucose { - let formatter = NumberFormatter.glucoseFormatter(for: charts.glucoseUnit) - let values = [startGlucose, endGlucose, currentGlucose].map { formatter.string(from: $0.quantity.doubleValue(for: charts.glucoseUnit)) ?? "?" } + let formatter = QuantityFormatter() + formatter.setPreferredNumberFormatter(for: charts.glucoseUnit) + let predicted = HKQuantity(unit: charts.glucoseUnit, doubleValue: currentGlucose.quantity.doubleValue(for: charts.glucoseUnit) - lastDiscrepancy.quantity.doubleValue(for: charts.glucoseUnit)) + var values = [predicted, currentGlucose.quantity].map { formatter.string(from: $0, for: charts.glucoseUnit) ?? "?" } + formatter.numberFormatter.positivePrefix = formatter.numberFormatter.plusSign + values.append(formatter.string(from: lastDiscrepancy.quantity, for: charts.glucoseUnit) ?? "?" ) let retro = String( - format: NSLocalizedString("Last comparison: %1$@ → %2$@ vs %3$@", comment: "Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose)"), + format: NSLocalizedString("prediction-description-retrospective-correction", comment: "Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference)"), values[0], values[1], values[2] ) diff --git a/Loop/de.lproj/Localizable.strings b/Loop/de.lproj/Localizable.strings index b3a0814f9f..73db5816ed 100644 --- a/Loop/de.lproj/Localizable.strings +++ b/Loop/de.lproj/Localizable.strings @@ -247,9 +247,6 @@ /* The title text for the issue report cell */ "Issue Report" = "Problembericht"; -/* Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose) */ -"Last comparison: %1$@ → %2$@ vs %3$@" = "Letzter Vergleich: %1$@ → %2$@ vs %3$@"; - /* Glucose HUD accessibility hint */ "Launches CGM app" = "Startet die CGM-App"; diff --git a/Loop/es.lproj/Localizable.strings b/Loop/es.lproj/Localizable.strings index 1b3fdb51a6..313e2b5464 100644 --- a/Loop/es.lproj/Localizable.strings +++ b/Loop/es.lproj/Localizable.strings @@ -247,9 +247,6 @@ /* The title text for the issue report cell */ "Issue Report" = "Informe de Errores"; -/* Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose) */ -"Last comparison: %1$@ → %2$@ vs %3$@" = "Última comparación: %1$@ → %2$@ vs %3$@"; - /* Glucose HUD accessibility hint */ "Launches CGM app" = "Lanza app MCG"; diff --git a/Loop/fr.lproj/Localizable.strings b/Loop/fr.lproj/Localizable.strings index 56d14c322d..cf32d7c90c 100644 --- a/Loop/fr.lproj/Localizable.strings +++ b/Loop/fr.lproj/Localizable.strings @@ -247,9 +247,6 @@ /* The title text for the issue report cell */ "Issue Report" = "Editer rapport"; -/* Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose) */ -"Last comparison: %1$@ → %2$@ vs %3$@" = "Dernière comparaison: %1$@ → %2$@ vs %3$@"; - /* Glucose HUD accessibility hint */ "Launches CGM app" = "Lance Application CGM"; diff --git a/Loop/it.lproj/Localizable.strings b/Loop/it.lproj/Localizable.strings index 7075cf1edf..83e6ce0170 100644 --- a/Loop/it.lproj/Localizable.strings +++ b/Loop/it.lproj/Localizable.strings @@ -247,9 +247,6 @@ /* The title text for the issue report cell */ "Issue Report" = "Segnalazione"; -/* Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose) */ -"Last comparison: %1$@ → %2$@ vs %3$@" = "Ultimo confronto: %1$@ → %2$@ vs %3$@"; - /* Glucose HUD accessibility hint */ "Launches CGM app" = "Avvia CGM app"; diff --git a/Loop/nb.lproj/Localizable.strings b/Loop/nb.lproj/Localizable.strings index bb9d1069bc..0327aa145f 100644 --- a/Loop/nb.lproj/Localizable.strings +++ b/Loop/nb.lproj/Localizable.strings @@ -247,9 +247,6 @@ /* The title text for the issue report cell */ "Issue Report" = "Hendelsesrapport"; -/* Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose) */ -"Last comparison: %1$@ → %2$@ vs %3$@" = "Siste sammenligning: %1$@ → %2$@ vs %3$@"; - /* Glucose HUD accessibility hint */ "Launches CGM app" = "Starter CGM app"; diff --git a/Loop/nl.lproj/Localizable.strings b/Loop/nl.lproj/Localizable.strings index 8fd598f9aa..aa7fe74b94 100644 --- a/Loop/nl.lproj/Localizable.strings +++ b/Loop/nl.lproj/Localizable.strings @@ -247,9 +247,6 @@ /* The title text for the issue report cell */ "Issue Report" = "Incidenten rapportage"; -/* Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose) */ -"Last comparison: %1$@ → %2$@ vs %3$@" = "Laatste vergelijking: %1$@ → %2$@ vs %3$@"; - /* Glucose HUD accessibility hint */ "Launches CGM app" = "Start de CGM app op"; diff --git a/Loop/pl.lproj/Localizable.strings b/Loop/pl.lproj/Localizable.strings index 9a6383adea..b77dd05ba1 100644 --- a/Loop/pl.lproj/Localizable.strings +++ b/Loop/pl.lproj/Localizable.strings @@ -247,9 +247,6 @@ /* The title text for the issue report cell */ "Issue Report" = "Zgłaszanie błędów"; -/* Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose) */ -"Last comparison: %1$@ → %2$@ vs %3$@" = "Ostatnie porównanie: %1$@ → %2$@ vs %3$@"; - /* Glucose HUD accessibility hint */ "Launches CGM app" = "Uruchamia aplikację CGM"; diff --git a/Loop/ru.lproj/Localizable.strings b/Loop/ru.lproj/Localizable.strings index cc41fc09fb..c23a1eb427 100644 --- a/Loop/ru.lproj/Localizable.strings +++ b/Loop/ru.lproj/Localizable.strings @@ -247,9 +247,6 @@ /* The title text for the issue report cell */ "Issue Report" = "Сообщить об ошибе"; -/* Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose) */ -"Last comparison: %1$@ → %2$@ vs %3$@" = "Недавнее сравнение %1$@ → %2$@ и %3$@"; - /* Glucose HUD accessibility hint */ "Launches CGM app" = "Запускает приложение непрерывного мониторинга CGM"; diff --git a/Loop/zh-Hans.lproj/Localizable.strings b/Loop/zh-Hans.lproj/Localizable.strings index 9da1ff3b01..0e79f3540a 100644 --- a/Loop/zh-Hans.lproj/Localizable.strings +++ b/Loop/zh-Hans.lproj/Localizable.strings @@ -247,9 +247,6 @@ /* The title text for the issue report cell */ "Issue Report" = "反馈问题"; -/* Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose) */ -"Last comparison: %1$@ → %2$@ vs %3$@" = "的最后一次对比: %1$@ → %2$@ vs %3$@"; - /* Glucose HUD accessibility hint */ "Launches CGM app" = "启动CGM软件"; diff --git a/LoopUI/Extensions/CollectionType.swift b/LoopUI/Extensions/CollectionType.swift index cb0444941c..c6b80f181e 100644 --- a/LoopUI/Extensions/CollectionType.swift +++ b/LoopUI/Extensions/CollectionType.swift @@ -9,7 +9,7 @@ import Foundation -extension BidirectionalCollection where Index: Strideable, Iterator.Element: Comparable, Index.Stride == Int { +extension BidirectionalCollection where Index: Strideable, Element: Comparable, Index.Stride == Int { /** Returns the insertion index of a new value in a sorted collection @@ -20,7 +20,7 @@ extension BidirectionalCollection where Index: Strideable, Iterator.Element: Com - returns: The appropriate insertion index, between `startIndex` and `endIndex` */ - func findInsertionIndex(for value: Iterator.Element) -> Index { + func findInsertionIndex(for value: Element) -> Index { var low = startIndex var high = endIndex @@ -39,7 +39,7 @@ extension BidirectionalCollection where Index: Strideable, Iterator.Element: Com } -extension BidirectionalCollection where Index: Strideable, Iterator.Element: Strideable, Index.Stride == Int { +extension BidirectionalCollection where Index: Strideable, Element: Strideable, Index.Stride == Int { /** Returns the index of the closest element to a specified value in a sorted collection @@ -47,7 +47,7 @@ extension BidirectionalCollection where Index: Strideable, Iterator.Element: Str - returns: The index of the closest element, or nil if the collection is empty */ - func findClosestElementIndex(matching value: Iterator.Element) -> Index? { + func findClosestElementIndex(matching value: Element) -> Index? { let upperBound = findInsertionIndex(for: value) if upperBound == startIndex { From 8e1df7c083b5b0bad6c976c1f9f4eef509ee0b6e Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 4 Oct 2018 23:34:09 -0500 Subject: [PATCH 4/5] Watch merge (#824) * Put a glucose chart on the watch. This incorporates a ton of fine work by Eric Jensen, but I've squashed it down into a single commit for ease of management. The watch now has two discrete pages, one which is the standard actions based interface, the second is an information interface which shows the IOB, COB, basal rate and a glucose chart. Glucose data is read from HealthKit on the watch. Sometimes this lags behind so we also make requests to backfill glucose data as necessary. We're also minimizing the amount of data sent over BLE as much as possible since BLE is slow and less reliable. Fix filename mismatches in comments Prototype of a SpriteKit-based glucose chart Tweak the scene size for 42mm watches Add the rest of the chart components Set high watermark higher for mg/dl Clean up pause/unpause semantics and set the max hours to 4 The crown now adjusts the height of the graph Fix override range rendering to match the Loop app ui Small optimizations Clean up the rendering code to pause between data changes Remove debug code Highlight the upper BG when it changes Make updateGlucoseChart async to avoid hitting CPU limits More stability fixes & remove graph corner radius Force updateNode() to run on the main queue to avoid contention Improve watch size detection code Switch to the more efficient SpriteNode Only cache 4 hours of glucose history Remove pause/unpause code- it doesn't save significant CPU Try unpausing the WKInterfaceSKScene to get the animation to stop freezing Revert "Remove pause/unpause code- it doesn't save significant CPU" This reverts commit deeddb21e066f760a9dac23ac6c0b3807b097676. Move the IOB/COB below the graph Whitespace and comment cleanup Animate moving points on the chart Only animate the predicted path when adjusting the y-axis Clean up sprite animation code Adjust scene size on 38mm watch Only show overrides that haven't yet expired Fix node expiration code Break the more complex updateSprite into two more manageable pieces: getSprite() and SKSpriteKitNode.move() Clean up variable names and comments around how we expire inactive nodes for readability Add some comments to aid readability Change visible hours with force touch menu Revert changes in tools versions in storyboard Add icons for duration menu Rearrange menu actions Fix asset locations for graph menu icons Make mmol/L graph limits be integers Force minimum height for dated range rects Make target ranges more visible Only show future part of override ranges Revert "Only show future part of override ranges" This reverts commit 0b25d2e2ec75c498a461f606b57a871d4e054bdd. Expire overrides once they move into past Set min range height via variable Rearrange image assets for graph menu Clarify minimum height on dated range Don't send expired overrides in watch context Rearrange testing of override end date Use Int16 instead of UInt16 to handle negative numbers in predicted glucose Change the appearance of the chart to be platter style to better fit with Watch aesthetics Remove duplicate files * chart tweaks adds auto-scaling, handles the SpriteKit lifecycle, and simplifies the sync story for target ranges --- Common/Extensions/Double.swift | 28 ++ .../Extensions/GlucoseRangeSchedule.swift | 18 +- Common/Extensions/HKUnit.swift | 6 +- Common/Extensions/PersistenceController.swift | 10 + Common/Extensions/SampleValue.swift | 38 ++ Common/Models/BolusSuggestionUserInfo.swift | 16 +- .../GlucoseBackfillRequestUserInfo.swift | 40 ++ ...GlucoseRangeScheduleOverrideUserInfo.swift | 75 ---- Common/Models/GlucoseThreshold.swift | 9 +- Common/Models/LoopSettings.swift | 2 +- Common/Models/LoopSettingsUserInfo.swift | 39 ++ Common/Models/WatchContext.swift | 56 +-- Common/Models/WatchHistoricalGlucose.swift | 51 +++ Common/Models/WatchPredictedGlucose.swift | 52 +++ .../Base.lproj/MainInterface.storyboard | 4 +- Loop.xcodeproj/project.pbxproj | 134 +++++-- Loop/Managers/DiagnosticLogger.swift | 2 +- Loop/Managers/WatchDataManager.swift | 160 ++++---- Loop/Models/WatchContext+LoopKit.swift | 3 +- LoopUI/Extensions/ChartPoint.swift | 2 +- .../ComplicationController.swift | 8 +- .../Controllers/ActionHUDController.swift | 157 ++++++++ .../AddCarbsInterfaceController.swift | 41 +- .../BolusInterfaceController.swift | 25 +- .../Controllers/ChartHUDController.swift | 197 +++++++++ .../Controllers/ContextUpdatable.swift | 14 - .../Controllers/HUDInterfaceController.swift | 103 +++++ .../StatusInterfaceController.swift | 232 ----------- WatchApp Extension/ExtensionDelegate.swift | 141 ++++--- WatchApp Extension/Extensions/Date.swift | 20 + .../Extensions/GlucoseStore.swift | 15 + .../Extensions/NSUserDefaults+WatchApp.swift | 25 ++ .../Extensions/NSUserDefaults.swift | 25 -- WatchApp Extension/Extensions/UIColor.swift | 34 +- WatchApp Extension/Extensions/WCSession.swift | 69 +++- .../Managers/LoopDataManager.swift | 114 ++++++ .../Scenes/GlucoseChartScene.swift | 377 ++++++++++++++++++ .../Scenes/GlucoseChartValueHashable.swift | 87 ++++ .../Graph menu icons/1-hour-graph-38mm.png | Bin 0 -> 2658 bytes .../Graph menu icons/1-hour-graph-42mm.png | Bin 0 -> 2687 bytes .../1-hour-graph-38mm.png | Bin 0 -> 1052 bytes .../1-hour-graph-42mm.png | Bin 0 -> 1570 bytes .../1-hour-graph.imageset/Contents.json | 20 + .../Graph menu icons/2-hour-graph-38mm.png | Bin 0 -> 2969 bytes .../Graph menu icons/2-hour-graph-42mm.png | Bin 0 -> 3097 bytes .../2-hour-graph-38mm.png | Bin 0 -> 1430 bytes .../2-hour-graph-42mm.png | Bin 0 -> 1941 bytes .../2-hour-graph.imageset/Contents.json | 20 + .../Graph menu icons/3-hour-graph-38mm.png | Bin 0 -> 3043 bytes .../Graph menu icons/3-hour-graph-42mm.png | Bin 0 -> 3157 bytes .../3-hour-graph-38mm.png | Bin 0 -> 1655 bytes .../3-hour-graph-42mm.png | Bin 0 -> 2270 bytes .../3-hour-graph.imageset/Contents.json | 20 + .../Graph menu icons/Contents.json | 6 + WatchApp/Base.lproj/Interface.storyboard | 112 +++++- 55 files changed, 2000 insertions(+), 607 deletions(-) create mode 100644 Common/Extensions/Double.swift rename {Loop => Common}/Extensions/GlucoseRangeSchedule.swift (71%) create mode 100644 Common/Extensions/SampleValue.swift create mode 100644 Common/Models/GlucoseBackfillRequestUserInfo.swift delete mode 100644 Common/Models/GlucoseRangeScheduleOverrideUserInfo.swift create mode 100644 Common/Models/LoopSettingsUserInfo.swift create mode 100644 Common/Models/WatchHistoricalGlucose.swift create mode 100644 Common/Models/WatchPredictedGlucose.swift create mode 100644 WatchApp Extension/Controllers/ActionHUDController.swift create mode 100644 WatchApp Extension/Controllers/ChartHUDController.swift delete mode 100644 WatchApp Extension/Controllers/ContextUpdatable.swift create mode 100644 WatchApp Extension/Controllers/HUDInterfaceController.swift delete mode 100644 WatchApp Extension/Controllers/StatusInterfaceController.swift create mode 100644 WatchApp Extension/Extensions/Date.swift create mode 100644 WatchApp Extension/Extensions/GlucoseStore.swift create mode 100644 WatchApp Extension/Extensions/NSUserDefaults+WatchApp.swift delete mode 100644 WatchApp Extension/Extensions/NSUserDefaults.swift create mode 100644 WatchApp Extension/Managers/LoopDataManager.swift create mode 100644 WatchApp Extension/Scenes/GlucoseChartScene.swift create mode 100644 WatchApp Extension/Scenes/GlucoseChartValueHashable.swift create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/1-hour-graph-38mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/1-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/2-hour-graph-38mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/2-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/Contents.json create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/3-hour-graph-38mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/3-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/Contents.json diff --git a/Common/Extensions/Double.swift b/Common/Extensions/Double.swift new file mode 100644 index 0000000000..2247409097 --- /dev/null +++ b/Common/Extensions/Double.swift @@ -0,0 +1,28 @@ +// +// Double.swift +// Loop +// +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation + + +extension Double { + func floored(to increment: Double) -> Double { + if increment == 0 { + return self + } + + return floor(self / increment) * increment + } + + func ceiled(to increment: Double) -> Double { + if increment == 0 { + return self + } + + return ceil(self / increment) * increment + } +} + diff --git a/Loop/Extensions/GlucoseRangeSchedule.swift b/Common/Extensions/GlucoseRangeSchedule.swift similarity index 71% rename from Loop/Extensions/GlucoseRangeSchedule.swift rename to Common/Extensions/GlucoseRangeSchedule.swift index 9e19e5bf61..4cd1712a52 100644 --- a/Loop/Extensions/GlucoseRangeSchedule.swift +++ b/Common/Extensions/GlucoseRangeSchedule.swift @@ -23,12 +23,26 @@ extension GlucoseRangeSchedule { return override.isActive() } - var activeOverrideContext: GlucoseRangeSchedule.Override.Context? { + var activeOverride: GlucoseRangeSchedule.Override? { guard let override = override, override.isActive() else { return nil } - return override.context + return override + } + + var activeOverrideContext: GlucoseRangeSchedule.Override.Context? { + return activeOverride?.context + } + + var activeOverrideQuantityRange: Range? { + guard let activeOverride = activeOverride else { + return nil + } + + let lowerBound = HKQuantity(unit: unit, doubleValue: activeOverride.value.minValue) + let upperBound = HKQuantity(unit: unit, doubleValue: activeOverride.value.maxValue) + return lowerBound.. PersistenceController { + guard let directoryURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else { + fatalError("Could not access the document directory of the current process") + } + + let isReadOnly = Bundle.main.bundleURL.pathExtension == "appex" + + return self.init(directoryURL: directoryURL.appendingPathComponent("com.loopkit.LoopKit"), isReadOnly: isReadOnly) + } } diff --git a/Common/Extensions/SampleValue.swift b/Common/Extensions/SampleValue.swift new file mode 100644 index 0000000000..d554b7cd1d --- /dev/null +++ b/Common/Extensions/SampleValue.swift @@ -0,0 +1,38 @@ +// +// SampleValue.swift +// Loop +// +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import HealthKit +import LoopKit + + +extension Collection where Element == SampleValue { + /// O(n) + var quantityRange: Range? { + var lowest: HKQuantity? + var highest: HKQuantity? + + for sample in self { + if let l = lowest { + lowest = Swift.min(l, sample.quantity) + } else { + lowest = sample.quantity + } + + if let h = highest { + highest = Swift.max(h, sample.quantity) + } else { + highest = sample.quantity + } + } + + guard let l = lowest, let h = highest else { + return nil + } + + return l.. Bool { - return lhs.value == rhs.value - } -} diff --git a/Common/Models/LoopSettings.swift b/Common/Models/LoopSettings.swift index 1dcb0cd83a..ae1c5a33b1 100644 --- a/Common/Models/LoopSettings.swift +++ b/Common/Models/LoopSettings.swift @@ -8,7 +8,7 @@ import LoopKit -struct LoopSettings { +struct LoopSettings: Equatable { var dosingEnabled = false let dynamicCarbAbsorptionEnabled = true diff --git a/Common/Models/LoopSettingsUserInfo.swift b/Common/Models/LoopSettingsUserInfo.swift new file mode 100644 index 0000000000..06efd35087 --- /dev/null +++ b/Common/Models/LoopSettingsUserInfo.swift @@ -0,0 +1,39 @@ +// +// LoopSettingsUserInfo.swift +// Loop +// +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + + +struct LoopSettingsUserInfo { + let settings: LoopSettings +} + + +extension LoopSettingsUserInfo: RawRepresentable { + typealias RawValue = [String: Any] + + static let name = "LoopSettingsUserInfo" + static let version = 1 + + init?(rawValue: RawValue) { + guard rawValue["v"] as? Int == LoopSettingsUserInfo.version, + rawValue["name"] as? String == LoopSettingsUserInfo.name, + let settingsRaw = rawValue["s"] as? LoopSettings.RawValue, + let settings = LoopSettings(rawValue: settingsRaw) + else { + return nil + } + + self.settings = settings + } + + var rawValue: RawValue { + return [ + "v": LoopSettingsUserInfo.version, + "name": LoopSettingsUserInfo.name, + "s": settings.rawValue + ] + } +} diff --git a/Common/Models/WatchContext.swift b/Common/Models/WatchContext.swift index 8181fd3344..d25e5c7e55 100644 --- a/Common/Models/WatchContext.swift +++ b/Common/Models/WatchContext.swift @@ -10,21 +10,22 @@ import Foundation import HealthKit import LoopKit + final class WatchContext: NSObject, RawRepresentable { typealias RawValue = [String: Any] private let version = 4 var preferredGlucoseUnit: HKUnit? - var maxBolus: Double? var glucose: HKQuantity? var glucoseTrendRawValue: Int? - var eventualGlucose: HKQuantity? var glucoseDate: Date? - var glucoseRangeScheduleOverride: GlucoseRangeScheduleOverrideUserInfo? - var configuredOverrideContexts: [GlucoseRangeScheduleOverrideUserInfo.Context] = [] + var predictedGlucose: WatchPredictedGlucose? + var eventualGlucose: HKQuantity? { + return predictedGlucose?.values.last?.quantity + } var loopLastRunDate: Date? var lastNetTempBasalDose: Double? @@ -34,11 +35,11 @@ final class WatchContext: NSObject, RawRepresentable { var bolusSuggestion: BolusSuggestionUserInfo? { guard let recommended = recommendedBolusDose else { return nil } - return BolusSuggestionUserInfo(recommendedBolus: recommended, maxBolus: maxBolus) + return BolusSuggestionUserInfo(recommendedBolus: recommended) } - var COB: Double? - var IOB: Double? + var cob: Double? + var iob: Double? var reservoir: Double? var reservoirPercentage: Double? var batteryPercentage: Double? @@ -57,32 +58,16 @@ final class WatchContext: NSObject, RawRepresentable { } if let unitString = rawValue["gu"] as? String { - let unit = HKUnit(from: unitString) - preferredGlucoseUnit = unit + preferredGlucoseUnit = HKUnit(from: unitString) } - + let unit = preferredGlucoseUnit ?? .milligramsPerDeciliter if let glucoseValue = rawValue["gv"] as? Double { - glucose = HKQuantity(unit: preferredGlucoseUnit ?? .milligramsPerDeciliter, doubleValue: glucoseValue) - } - - if let glucoseValue = rawValue["egv"] as? Double { - eventualGlucose = HKQuantity(unit: preferredGlucoseUnit ?? .milligramsPerDeciliter, doubleValue: glucoseValue) + glucose = HKQuantity(unit: unit, doubleValue: glucoseValue) } glucoseTrendRawValue = rawValue["gt"] as? Int glucoseDate = rawValue["gd"] as? Date - - if let overrideUserInfoRawValue = rawValue["grsoc"] as? GlucoseRangeScheduleOverrideUserInfo.RawValue, - let overrideUserInfo = GlucoseRangeScheduleOverrideUserInfo(rawValue: overrideUserInfoRawValue) - { - glucoseRangeScheduleOverride = overrideUserInfo - } - - if let configuredOverrideContextsRawValues = rawValue["coc"] as? [GlucoseRangeScheduleOverrideUserInfo.Context.RawValue] { - configuredOverrideContexts = configuredOverrideContextsRawValues.compactMap(GlucoseRangeScheduleOverrideUserInfo.Context.init(rawValue:)) - } - - IOB = rawValue["iob"] as? Double + iob = rawValue["iob"] as? Double reservoir = rawValue["r"] as? Double reservoirPercentage = rawValue["rp"] as? Double batteryPercentage = rawValue["bp"] as? Double @@ -91,10 +76,13 @@ final class WatchContext: NSObject, RawRepresentable { lastNetTempBasalDose = rawValue["ba"] as? Double lastNetTempBasalDate = rawValue["bad"] as? Date recommendedBolusDose = rawValue["rbo"] as? Double - COB = rawValue["cob"] as? Double - maxBolus = rawValue["mb"] as? Double + cob = rawValue["cob"] as? Double cgmManagerState = rawValue["cgmManagerState"] as? CGMManager.RawStateValue + + if let rawValue = rawValue["pg"] as? WatchPredictedGlucose.RawValue { + predictedGlucose = WatchPredictedGlucose(rawValue: rawValue) + } } var rawValue: RawValue { @@ -108,24 +96,22 @@ final class WatchContext: NSObject, RawRepresentable { raw["cgmManagerState"] = cgmManagerState - raw["cob"] = COB + raw["cob"] = cob let unit = preferredGlucoseUnit ?? .milligramsPerDeciliter - raw["egv"] = eventualGlucose?.doubleValue(for: unit) raw["gu"] = preferredGlucoseUnit?.unitString raw["gv"] = glucose?.doubleValue(for: unit) raw["gt"] = glucoseTrendRawValue raw["gd"] = glucoseDate - raw["grsoc"] = glucoseRangeScheduleOverride?.rawValue - raw["coc"] = configuredOverrideContexts.map { $0.rawValue } - raw["iob"] = IOB + raw["iob"] = iob raw["ld"] = loopLastRunDate - raw["mb"] = maxBolus raw["r"] = reservoir raw["rbo"] = recommendedBolusDose raw["rp"] = reservoirPercentage + raw["pg"] = predictedGlucose?.rawValue + return raw } } diff --git a/Common/Models/WatchHistoricalGlucose.swift b/Common/Models/WatchHistoricalGlucose.swift new file mode 100644 index 0000000000..cfba470355 --- /dev/null +++ b/Common/Models/WatchHistoricalGlucose.swift @@ -0,0 +1,51 @@ +// +// WatchHistoricalGlucose.swift +// Loop +// +// Created by Bharat Mediratta on 6/22/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation +import HealthKit +import LoopKit + + +struct WatchHistoricalGlucose { + let samples: [NewGlucoseSample] + + init(with samples: [StoredGlucoseSample]) { + self.samples = samples.map { + NewGlucoseSample(date: $0.startDate, quantity: $0.quantity, isDisplayOnly: false, syncIdentifier: $0.syncIdentifier, syncVersion: 0) + } + } +} + + +extension WatchHistoricalGlucose: RawRepresentable { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + return [ + "d": samples.map { $0.date }, + "v": samples.map { Int16($0.quantity.doubleValue(for: .milligramsPerDeciliter)) }, + "id": samples.map { $0.syncIdentifier } + ] + } + + init?(rawValue: RawValue) { + guard + let dates = rawValue["d"] as? [Date], + let values = rawValue["v"] as? [Int16], + let syncIdentifiers = rawValue["id"] as? [String], + dates.count == values.count, + dates.count == syncIdentifiers.count + else { + return nil + } + + self.samples = (0.. 1 else { + return nil + } + self.values = values + } +} + + +extension WatchPredictedGlucose: RawRepresentable { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + + return [ + "v": values.map { Int16($0.quantity.doubleValue(for: .milligramsPerDeciliter)) }, + "d": values[0].startDate, + "i": values[1].startDate.timeIntervalSince(values[0].startDate) + ] + } + + init?(rawValue: RawValue) { + guard + let values = rawValue["v"] as? [Int16], + let firstDate = rawValue["d"] as? Date, + let interval = rawValue["i"] as? TimeInterval + else { + return nil + } + + self.values = values.enumerated().map { tuple in + PredictedGlucoseValue(startDate: firstDate + Double(tuple.0) * interval, + quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(tuple.1))) + } + } +} diff --git a/Loop Status Extension/Base.lproj/MainInterface.storyboard b/Loop Status Extension/Base.lproj/MainInterface.storyboard index 154bdf9b27..5b5eb74e56 100644 --- a/Loop Status Extension/Base.lproj/MainInterface.storyboard +++ b/Loop Status Extension/Base.lproj/MainInterface.storyboard @@ -1,11 +1,11 @@ - + - + diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 00892ae375..81db1cb41f 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -41,7 +41,7 @@ 431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */; }; 431E73481FF95A900069B5F7 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431E73471FF95A900069B5F7 /* PersistenceController.swift */; }; 4326BA641F3A44D9007CCAD4 /* ChartLineModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4326BA631F3A44D9007CCAD4 /* ChartLineModel.swift */; }; - 4328E01A1CFBE1DA00E199AA /* StatusInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0151CFBE1DA00E199AA /* StatusInterfaceController.swift */; }; + 4328E01A1CFBE1DA00E199AA /* ActionHUDController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0151CFBE1DA00E199AA /* ActionHUDController.swift */; }; 4328E01B1CFBE1DA00E199AA /* BolusInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0161CFBE1DA00E199AA /* BolusInterfaceController.swift */; }; 4328E01E1CFBE25F00E199AA /* AddCarbsInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E01D1CFBE25F00E199AA /* AddCarbsInterfaceController.swift */; }; 4328E0261CFBE2C500E199AA /* IdentifiableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0201CFBE2C500E199AA /* IdentifiableClass.swift */; }; @@ -51,7 +51,7 @@ 4328E02F1CFBF81800E199AA /* WKInterfaceImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E02E1CFBF81800E199AA /* WKInterfaceImage.swift */; }; 4328E0331CFC091100E199AA /* WatchContext+LoopKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0311CFC068900E199AA /* WatchContext+LoopKit.swift */; }; 4328E0351CFC0AE100E199AA /* WatchDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0341CFC0AE100E199AA /* WatchDataManager.swift */; }; - 432CF87520D8AC950066B889 /* NSUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0231CFBE2C500E199AA /* NSUserDefaults.swift */; }; + 432CF87520D8AC950066B889 /* NSUserDefaults+WatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0231CFBE2C500E199AA /* NSUserDefaults+WatchApp.swift */; }; 432E73CB1D24B3D6009AD15D /* RemoteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432E73CA1D24B3D6009AD15D /* RemoteDataManager.swift */; }; 433EA4C41D9F71C800CD78FB /* CommandResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */; }; 4341F4EB1EDB92AC001C936B /* LogglyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4341F4EA1EDB92AC001C936B /* LogglyService.swift */; }; @@ -86,6 +86,22 @@ 436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0DA41D236A2A00104B24 /* LoopError.swift */; }; 436D9BF81F6F4EA100CFA75F /* recommended_temp_start_low_end_just_above_range.json in Resources */ = {isa = PBXBuildFile; fileRef = 436D9BF71F6F4EA100CFA75F /* recommended_temp_start_low_end_just_above_range.json */; }; 43709AEA20DF3F8200F941B3 /* MinimedKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43709AE920DF3F8200F941B3 /* MinimedKitUI.framework */; }; + 4372E484213A63FB0068E043 /* ChartHUDController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFEDFBE20E5CF22000BFC58 /* ChartHUDController.swift */; }; + 4372E487213C86240068E043 /* SampleValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4372E486213C86240068E043 /* SampleValue.swift */; }; + 4372E488213C862B0068E043 /* SampleValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4372E486213C86240068E043 /* SampleValue.swift */; }; + 4372E48B213CB5F00068E043 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4372E48A213CB5F00068E043 /* Double.swift */; }; + 4372E48C213CB6750068E043 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4372E48A213CB5F00068E043 /* Double.swift */; }; + 4372E48D213CF8A70068E043 /* GlucoseThreshold.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430B298D2041F56500BA9F93 /* GlucoseThreshold.swift */; }; + 4372E48E213CF8AD0068E043 /* LoopSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430B298C2041F56500BA9F93 /* LoopSettings.swift */; }; + 4372E490213CFCE70068E043 /* LoopSettingsUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4372E48F213CFCE70068E043 /* LoopSettingsUserInfo.swift */; }; + 4372E491213D05F90068E043 /* LoopSettingsUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4372E48F213CFCE70068E043 /* LoopSettingsUserInfo.swift */; }; + 4372E492213D956C0068E043 /* GlucoseRangeSchedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C513181E864C4E001547C7 /* GlucoseRangeSchedule.swift */; }; + 4372E496213DCDD30068E043 /* GlucoseChartValueHashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4372E495213DCDD30068E043 /* GlucoseChartValueHashable.swift */; }; + 4372E497213F79F90068E043 /* NSUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430B29892041F54A00BA9F93 /* NSUserDefaults.swift */; }; + 4372E498213F7A550068E043 /* InsulinModelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435CB6281F37B01300C320C7 /* InsulinModelSettings.swift */; }; + 4372E499213F7A6D0068E043 /* ExponentialInsulinModelPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435CB6241F37ABFC00C320C7 /* ExponentialInsulinModelPreset.swift */; }; + 4372E49A213F7A830068E043 /* WalshInsulinModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435CB6261F37AE5600C320C7 /* WalshInsulinModel.swift */; }; + 4372E49B213F7B340068E043 /* NSBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430DA58D1D4AEC230097D1CA /* NSBundle.swift */; }; 4374B5EF209D84BF00D17AA8 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4374B5EE209D84BE00D17AA8 /* OSLog.swift */; }; 4374B5F0209D857E00D17AA8 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4374B5EE209D84BE00D17AA8 /* OSLog.swift */; }; 4374B5F2209D897600D17AA8 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4374B5F1209D897600D17AA8 /* Locked.swift */; }; @@ -104,8 +120,6 @@ 437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEE31CDE5C0A003C8C80 /* UIImage.swift */; }; 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */; }; 438172D91F4E9E37003C3328 /* NewPumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438172D81F4E9E37003C3328 /* NewPumpEvent.swift */; }; - 43846AD51D8FA67800799272 /* Base.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 43846AD41D8FA67800799272 /* Base.lproj */; }; - 43846ADB1D91057000799272 /* ContextUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43846ADA1D91057000799272 /* ContextUpdatable.swift */; }; 438849EA1D297CB6003B3F23 /* NightscoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438849E91D297CB6003B3F23 /* NightscoutService.swift */; }; 438849EC1D29EC34003B3F23 /* AmplitudeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438849EB1D29EC34003B3F23 /* AmplitudeService.swift */; }; 438849EE1D2A1EBB003B3F23 /* MLabService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438849ED1D2A1EBB003B3F23 /* MLabService.swift */; }; @@ -199,6 +213,10 @@ 4F08DE8F1E7BB871006741EA /* CollectionType+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F08DE8E1E7BB871006741EA /* CollectionType+Loop.swift */; }; 4F08DE9B1E7BC4ED006741EA /* SwiftCharts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4346D1EF1C781BEA00ABAFE3 /* SwiftCharts.framework */; }; 4F08DE9D1E81D0E9006741EA /* StatusChartsManager+LoopKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430C1ABC1E5568A80067F1AE /* StatusChartsManager+LoopKit.swift */; }; + 4F11D3C020DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F11D3BF20DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift */; }; + 4F11D3C220DD80B3006E072C /* WatchHistoricalGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F11D3C120DD80B3006E072C /* WatchHistoricalGlucose.swift */; }; + 4F11D3C320DD84DB006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F11D3BF20DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift */; }; + 4F11D3C420DD881A006E072C /* WatchHistoricalGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F11D3C120DD80B3006E072C /* WatchHistoricalGlucose.swift */; }; 4F20AE621E6B879C00D07A06 /* ReservoirVolumeHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEC71CD84CBB003C8C80 /* ReservoirVolumeHUDView.swift */; }; 4F20AE631E6B87B100D07A06 /* ChartContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4313EDDF1D8A6BF90060FA79 /* ChartContainerView.swift */; }; 4F2C15741E0209F500E160D4 /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; @@ -219,6 +237,7 @@ 4F70C2101DE8FAC5006380B7 /* StatusExtensionDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F70C20F1DE8FAC5006380B7 /* StatusExtensionDataManager.swift */; }; 4F70C2121DE900EA006380B7 /* StatusExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F70C2111DE900EA006380B7 /* StatusExtensionContext.swift */; }; 4F70C2131DE90339006380B7 /* StatusExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F70C2111DE900EA006380B7 /* StatusExtensionContext.swift */; }; + 4F73F5FC20E2E7FA00E8D82C /* GlucoseStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F73F5FB20E2E7FA00E8D82C /* GlucoseStore.swift */; }; 4F7528941DFE1E9500C322D6 /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 4F7528951DFE1E9B00C322D6 /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 4F75289A1DFE1F6000C322D6 /* BasalRateHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEBF1CD6FCD8003C8C80 /* BasalRateHUDView.swift */; }; @@ -231,6 +250,11 @@ 4F7528A21DFE200B00C322D6 /* LevelMaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43FBEDD71D73843700B21F22 /* LevelMaskView.swift */; }; 4F7528A51DFE208C00C322D6 /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; 4F7528AA1DFE215100C322D6 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D5E1DF2459000A04910 /* HKUnit.swift */; }; + 4F75F00220FCFE8C00B5570E /* GlucoseChartScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F75F00120FCFE8C00B5570E /* GlucoseChartScene.swift */; }; + 4F7E8AC520E2AB9600AEA65E /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7E8AC420E2AB9600AEA65E /* Date.swift */; }; + 4F7E8AC720E2AC0300AEA65E /* WatchPredictedGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7E8AC620E2AC0300AEA65E /* WatchPredictedGlucose.swift */; }; + 4F7E8ACB20E2ACB500AEA65E /* WatchPredictedGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7E8AC620E2AC0300AEA65E /* WatchPredictedGlucose.swift */; }; + 4F82655020E69F9A0031A8F5 /* HUDInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F82654F20E69F9A0031A8F5 /* HUDInterfaceController.swift */; }; 4FAC02541E22F6B20087A773 /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; 4FB76FB01E8C3E8000B39636 /* SwiftCharts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4346D1EF1C781BEA00ABAFE3 /* SwiftCharts.framework */; }; 4FB76FB31E8C3EE400B39636 /* ChartAxisValueDoubleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F08DE7C1E7BB6E5006741EA /* ChartAxisValueDoubleLog.swift */; }; @@ -246,13 +270,14 @@ 4FB76FCE1E8C835D00B39636 /* ChartColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB76FCD1E8C835D00B39636 /* ChartColorPalette.swift */; }; 4FC8C8011DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC8C8001DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift */; }; 4FC8C8021DEB943800A1452E /* NSUserDefaults+StatusExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC8C8001DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift */; }; + 4FDDD23720DC51DF00D04B16 /* LoopDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */; }; + 4FF0F75E20E1E5D100FC6291 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431E73471FF95A900069B5F7 /* PersistenceController.swift */; }; 4FF4D0F81E1725B000846527 /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434F54561D287FDB002A9274 /* NibLoadable.swift */; }; 4FF4D0F91E17268800846527 /* IdentifiableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434FF1E91CF26C29000DB779 /* IdentifiableClass.swift */; }; 4FF4D1001E18374700846527 /* WatchContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF4D0FF1E18374700846527 /* WatchContext.swift */; }; 4FF4D1011E18375000846527 /* WatchContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF4D0FF1E18374700846527 /* WatchContext.swift */; }; 7D23667D21250C7E0028B67D /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D23667C21250C7E0028B67D /* LocalizedString.swift */; }; 7D2366E621250E0A0028B67D /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D2366E421250E0A0028B67D /* InfoPlist.strings */; }; - 7D2366E821250E7B0028B67D /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D2366E421250E0A0028B67D /* InfoPlist.strings */; }; 7D7076351FE06EDE004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076371FE06EDE004AC8EA /* Localizable.strings */; }; 7D70763A1FE06EDF004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D70763C1FE06EDF004AC8EA /* InfoPlist.strings */; }; 7D70763F1FE06EDF004AC8EA /* ckcomplication.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076411FE06EDF004AC8EA /* ckcomplication.strings */; }; @@ -264,8 +289,6 @@ 7D70765E1FE06EE3004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076601FE06EE3004AC8EA /* Localizable.strings */; }; 7D7076631FE06EE4004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076651FE06EE4004AC8EA /* Localizable.strings */; }; 7D7076681FE0702F004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D70766A1FE0702F004AC8EA /* InfoPlist.strings */; }; - 894B91CD1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894B91CC1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift */; }; - 894B91CE1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894B91CC1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift */; }; 894F71E21FFEC4D8007D365C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 894F71E11FFEC4D8007D365C /* Assets.xcassets */; }; C10428971D17BAD400DD539A /* NightscoutUploadKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */; }; C10B28461EA9BA5E006EA1FC /* far_future_high_bg_forecast.json in Resources */ = {isa = PBXBuildFile; fileRef = C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */; }; @@ -436,12 +459,12 @@ 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleMaskView.swift; sourceTree = ""; }; 431E73471FF95A900069B5F7 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; 4326BA631F3A44D9007CCAD4 /* ChartLineModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartLineModel.swift; sourceTree = ""; }; - 4328E0151CFBE1DA00E199AA /* StatusInterfaceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusInterfaceController.swift; sourceTree = ""; }; + 4328E0151CFBE1DA00E199AA /* ActionHUDController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionHUDController.swift; sourceTree = ""; }; 4328E0161CFBE1DA00E199AA /* BolusInterfaceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusInterfaceController.swift; sourceTree = ""; }; 4328E01D1CFBE25F00E199AA /* AddCarbsInterfaceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddCarbsInterfaceController.swift; sourceTree = ""; }; 4328E0201CFBE2C500E199AA /* IdentifiableClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentifiableClass.swift; sourceTree = ""; }; 4328E0221CFBE2C500E199AA /* CLKComplicationTemplate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLKComplicationTemplate.swift; sourceTree = ""; }; - 4328E0231CFBE2C500E199AA /* NSUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaults.swift; sourceTree = ""; }; + 4328E0231CFBE2C500E199AA /* NSUserDefaults+WatchApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSUserDefaults+WatchApp.swift"; sourceTree = ""; }; 4328E0241CFBE2C500E199AA /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; 4328E0251CFBE2C500E199AA /* WKAlertAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKAlertAction.swift; sourceTree = ""; }; 4328E02E1CFBF81800E199AA /* WKInterfaceImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKInterfaceImage.swift; sourceTree = ""; }; @@ -477,6 +500,10 @@ 436A0DA41D236A2A00104B24 /* LoopError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopError.swift; sourceTree = ""; }; 436D9BF71F6F4EA100CFA75F /* recommended_temp_start_low_end_just_above_range.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommended_temp_start_low_end_just_above_range.json; sourceTree = ""; }; 43709AE920DF3F8200F941B3 /* MinimedKitUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MinimedKitUI.framework; path = Carthage/Build/iOS/MinimedKitUI.framework; sourceTree = ""; }; + 4372E486213C86240068E043 /* SampleValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleValue.swift; sourceTree = ""; }; + 4372E48A213CB5F00068E043 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; + 4372E48F213CFCE70068E043 /* LoopSettingsUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopSettingsUserInfo.swift; sourceTree = ""; }; + 4372E495213DCDD30068E043 /* GlucoseChartValueHashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseChartValueHashable.swift; sourceTree = ""; }; 4374B5EE209D84BE00D17AA8 /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; 4374B5F1209D897600D17AA8 /* Locked.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = ""; }; 4374B5F3209D89A900D17AA8 /* TextFieldTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewCell.swift; sourceTree = ""; }; @@ -502,8 +529,6 @@ 437D9BA11D7B5203007245E8 /* Loop.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Loop.xcconfig; sourceTree = ""; }; 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionTableViewController.swift; sourceTree = ""; }; 438172D81F4E9E37003C3328 /* NewPumpEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPumpEvent.swift; sourceTree = ""; }; - 43846AD41D8FA67800799272 /* Base.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base.lproj; sourceTree = ""; }; - 43846ADA1D91057000799272 /* ContextUpdatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextUpdatable.swift; sourceTree = ""; }; 438849E91D297CB6003B3F23 /* NightscoutService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutService.swift; sourceTree = ""; }; 438849EB1D29EC34003B3F23 /* AmplitudeService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmplitudeService.swift; sourceTree = ""; }; 438849ED1D2A1EBB003B3F23 /* MLabService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MLabService.swift; sourceTree = ""; }; @@ -601,6 +626,8 @@ 4F08DE831E7BB70B006741EA /* ChartPointsScatterDownTrianglesLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartPointsScatterDownTrianglesLayer.swift; sourceTree = ""; }; 4F08DE841E7BB70B006741EA /* ChartPointsTouchHighlightLayerViewCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartPointsTouchHighlightLayerViewCache.swift; sourceTree = ""; }; 4F08DE8E1E7BB871006741EA /* CollectionType+Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CollectionType+Loop.swift"; sourceTree = ""; }; + 4F11D3BF20DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseBackfillRequestUserInfo.swift; sourceTree = ""; }; + 4F11D3C120DD80B3006E072C /* WatchHistoricalGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHistoricalGlucose.swift; sourceTree = ""; }; 4F2C15801E0495B200E160D4 /* WatchContext+WatchApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WatchContext+WatchApp.swift"; sourceTree = ""; }; 4F2C15921E09BF2C00E160D4 /* HUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HUDView.swift; sourceTree = ""; }; 4F2C15941E09BF3C00E160D4 /* HUDView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HUDView.xib; sourceTree = ""; }; @@ -616,13 +643,20 @@ 4F70C1FD1DE8E662006380B7 /* Loop Status Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Loop Status Extension.entitlements"; sourceTree = ""; }; 4F70C20F1DE8FAC5006380B7 /* StatusExtensionDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusExtensionDataManager.swift; sourceTree = ""; }; 4F70C2111DE900EA006380B7 /* StatusExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusExtensionContext.swift; sourceTree = ""; }; + 4F73F5FB20E2E7FA00E8D82C /* GlucoseStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStore.swift; sourceTree = ""; }; 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LoopUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F75288D1DFE1DC600C322D6 /* LoopUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoopUI.h; sourceTree = ""; }; 4F75288E1DFE1DC600C322D6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4F75F00120FCFE8C00B5570E /* GlucoseChartScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseChartScene.swift; sourceTree = ""; }; + 4F7E8AC420E2AB9600AEA65E /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; + 4F7E8AC620E2AC0300AEA65E /* WatchPredictedGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchPredictedGlucose.swift; sourceTree = ""; }; + 4F82654F20E69F9A0031A8F5 /* HUDInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDInterfaceController.swift; sourceTree = ""; }; 4FB76FC51E8C57B100B39636 /* StatusChartsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusChartsManager.swift; sourceTree = ""; }; 4FB76FCD1E8C835D00B39636 /* ChartColorPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartColorPalette.swift; sourceTree = ""; }; 4FC8C8001DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSUserDefaults+StatusExtension.swift"; sourceTree = ""; }; + 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopDataManager.swift; sourceTree = ""; }; 4FF4D0FF1E18374700846527 /* WatchContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchContext.swift; sourceTree = ""; }; + 4FFEDFBE20E5CF22000BFC58 /* ChartHUDController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartHUDController.swift; sourceTree = ""; }; 7D199D92212A067600241026 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/LaunchScreen.strings; sourceTree = ""; }; 7D199D93212A067600241026 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Main.strings; sourceTree = ""; }; 7D199D94212A067600241026 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/MainInterface.strings; sourceTree = ""; }; @@ -789,7 +823,6 @@ 7DD382771F8DBFC60071272B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Main.strings; sourceTree = ""; }; 7DD382781F8DBFC60071272B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainInterface.strings; sourceTree = ""; }; 7DD382791F8DBFC60071272B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Interface.strings; sourceTree = ""; }; - 894B91CC1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseRangeScheduleOverrideUserInfo.swift; sourceTree = ""; }; 894F71E11FFEC4D8007D365C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NightscoutUploadKit.framework; path = Carthage/Build/iOS/NightscoutUploadKit.framework; sourceTree = SOURCE_ROOT; }; C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = far_future_high_bg_forecast.json; sourceTree = ""; }; @@ -888,11 +921,12 @@ 4328E0121CFBE1B700E199AA /* Controllers */ = { isa = PBXGroup; children = ( + 4328E0151CFBE1DA00E199AA /* ActionHUDController.swift */, 4328E01D1CFBE25F00E199AA /* AddCarbsInterfaceController.swift */, 4328E0161CFBE1DA00E199AA /* BolusInterfaceController.swift */, - 43846ADA1D91057000799272 /* ContextUpdatable.swift */, + 4FFEDFBE20E5CF22000BFC58 /* ChartHUDController.swift */, + 4F82654F20E69F9A0031A8F5 /* HUDInterfaceController.swift */, 43A943891B926B7B0051FA24 /* NotificationController.swift */, - 4328E0151CFBE1DA00E199AA /* StatusInterfaceController.swift */, ); path = Controllers; sourceTree = ""; @@ -902,9 +936,11 @@ children = ( 4344629120A7C19800C4BE6F /* ButtonGroup.swift */, 4328E0221CFBE2C500E199AA /* CLKComplicationTemplate.swift */, + 4F7E8AC420E2AB9600AEA65E /* Date.swift */, + 4F73F5FB20E2E7FA00E8D82C /* GlucoseStore.swift */, 4328E0201CFBE2C500E199AA /* IdentifiableClass.swift */, 43785E952120E4010057DED1 /* INRelevantShortcutStore+Loop.swift */, - 4328E0231CFBE2C500E199AA /* NSUserDefaults.swift */, + 4328E0231CFBE2C500E199AA /* NSUserDefaults+WatchApp.swift */, 4328E0241CFBE2C500E199AA /* UIColor.swift */, 4F2C15801E0495B200E160D4 /* WatchContext+WatchApp.swift */, 43CB2B2A1D924D450079823D /* WCSession.swift */, @@ -1024,13 +1060,14 @@ 7D7076411FE06EDF004AC8EA /* ckcomplication.strings */, 7D70763C1FE06EDF004AC8EA /* InfoPlist.strings */, 43D533BB1CFD1DD7009E3085 /* WatchApp Extension.entitlements */, - 43846AD41D8FA67800799272 /* Base.lproj */, 43A943911B926B7B0051FA24 /* Info.plist */, 43A9438D1B926B7B0051FA24 /* ComplicationController.swift */, 43A943871B926B7B0051FA24 /* ExtensionDelegate.swift */, 43A9438F1B926B7B0051FA24 /* Assets.xcassets */, 4328E0121CFBE1B700E199AA /* Controllers */, 4328E01F1CFBE2B100E199AA /* Extensions */, + 4FE3475F20D5D7FA00A86D03 /* Managers */, + 4F75F0052100146B00B5570E /* Scenes */, 43A943831B926B7B0051FA24 /* Supporting Files */, ); path = "WatchApp Extension"; @@ -1087,7 +1124,6 @@ 4389916A1E91B689000EEF90 /* ChartSettings+Loop.swift */, 4F08DE8E1E7BB871006741EA /* CollectionType+Loop.swift */, 43CE7CDD1CA8B63E003CC1B0 /* Data.swift */, - 43C513181E864C4E001547C7 /* GlucoseRangeSchedule.swift */, C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */, 438172D81F4E9E37003C3328 /* NewPumpEvent.swift */, 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */, @@ -1240,6 +1276,15 @@ path = Models; sourceTree = ""; }; + 4F75F0052100146B00B5570E /* Scenes */ = { + isa = PBXGroup; + children = ( + 4F75F00120FCFE8C00B5570E /* GlucoseChartScene.swift */, + 4372E495213DCDD30068E043 /* GlucoseChartValueHashable.swift */, + ); + path = Scenes; + sourceTree = ""; + }; 4FB76FC31E8C575900B39636 /* Managers */ = { isa = PBXGroup; children = ( @@ -1258,6 +1303,14 @@ path = Extensions; sourceTree = ""; }; + 4FE3475F20D5D7FA00A86D03 /* Managers */ = { + isa = PBXGroup; + children = ( + 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */, + ); + path = Managers; + sourceTree = ""; + }; 4FF4D0FA1E1834BD00846527 /* Common */ = { isa = PBXGroup; children = ( @@ -1274,12 +1327,15 @@ 43673E2E1F37BDA10058AC7C /* Insulin */, 435400301C9F744E00D5819C /* BolusSuggestionUserInfo.swift */, 43DE92581C5479E4001FFDE1 /* CarbEntryUserInfo.swift */, - 894B91CC1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift */, + 4F11D3BF20DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift */, 430B298D2041F56500BA9F93 /* GlucoseThreshold.swift */, 430B298C2041F56500BA9F93 /* LoopSettings.swift */, + 4372E48F213CFCE70068E043 /* LoopSettingsUserInfo.swift */, 435400331C9F878D00D5819C /* SetBolusUserInfo.swift */, 4F70C2111DE900EA006380B7 /* StatusExtensionContext.swift */, 4FF4D0FF1E18374700846527 /* WatchContext.swift */, + 4F11D3C120DD80B3006E072C /* WatchHistoricalGlucose.swift */, + 4F7E8AC620E2AC0300AEA65E /* WatchPredictedGlucose.swift */, ); path = Models; sourceTree = ""; @@ -1287,7 +1343,9 @@ 4FF4D0FC1E1834CC00846527 /* Extensions */ = { isa = PBXGroup; children = ( + 4372E48A213CB5F00068E043 /* Double.swift */, 4F526D5E1DF2459000A04910 /* HKUnit.swift */, + 43C513181E864C4E001547C7 /* GlucoseRangeSchedule.swift */, 434FF1E91CF26C29000DB779 /* IdentifiableClass.swift */, 43785E922120A01B0057DED1 /* NewCarbEntryIntent+Loop.swift */, 434F54561D287FDB002A9274 /* NibLoadable.swift */, @@ -1299,6 +1357,7 @@ 43BFF0B31E45C1BE00FF19A9 /* NumberFormatter.swift */, 4374B5EE209D84BE00D17AA8 /* OSLog.swift */, 431E73471FF95A900069B5F7 /* PersistenceController.swift */, + 4372E486213C86240068E043 /* SampleValue.swift */, 43BFF0B11E45C18400FF19A9 /* UIColor.swift */, 43BFF0C31E4659E700FF19A9 /* UIColor+HIG.swift */, 4344628D20A7ADD100C4BE6F /* UserDefaults+CGM.swift */, @@ -1500,7 +1559,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 0940; ORGANIZATIONNAME = "LoopKit Authors"; TargetAttributes = { 432CF87720D8B8380066B889 = { @@ -1646,7 +1705,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 43846AD51D8FA67800799272 /* Base.lproj in Resources */, 7D70765E1FE06EE3004AC8EA /* Localizable.strings in Resources */, 7D70763A1FE06EDF004AC8EA /* InfoPlist.strings in Resources */, 7D70763F1FE06EDF004AC8EA /* ckcomplication.strings in Resources */, @@ -1684,7 +1742,6 @@ buildActionMask = 2147483647; files = ( 7D2366E621250E0A0028B67D /* InfoPlist.strings in Resources */, - 7D2366E821250E7B0028B67D /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1810,11 +1867,13 @@ 43C513191E864C4E001547C7 /* GlucoseRangeSchedule.swift in Sources */, 43A51E1F1EB6D62A000736CC /* CarbAbsorptionViewController.swift in Sources */, 43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */, + 4372E48B213CB5F00068E043 /* Double.swift in Sources */, 430B29932041F5B300BA9F93 /* UserDefaults+Loop.swift in Sources */, 4341F4EB1EDB92AC001C936B /* LogglyService.swift in Sources */, 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */, 43BFF0CB1E466C0900FF19A9 /* StateColorPalette.swift in Sources */, 439A7942211F631C0041B75F /* RootNavigationController.swift in Sources */, + 4F11D3C020DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */, 43F5C2DB1B92A5E1003EB13D /* SettingsTableViewController.swift in Sources */, 434FF1EA1CF26C29000DB779 /* IdentifiableClass.swift in Sources */, 43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */, @@ -1831,6 +1890,7 @@ 430B298F2041F56500BA9F93 /* GlucoseThreshold.swift in Sources */, C17824A01E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift in Sources */, 435CB6251F37ABFC00C320C7 /* ExponentialInsulinModelPreset.swift in Sources */, + 4372E487213C86240068E043 /* SampleValue.swift in Sources */, 4346D1E71C77F5FE00ABAFE3 /* ChartTableViewCell.swift in Sources */, 437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */, 43DBF0591C93F73800B3C386 /* CarbEntryTableViewController.swift in Sources */, @@ -1864,7 +1924,6 @@ 435400341C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */, 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */, 4344628F20A7ADD500C4BE6F /* UserDefaults+CGM.swift in Sources */, - 894B91CD1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift in Sources */, 43F41C371D3BF32400C11ED6 /* UIAlertController.swift in Sources */, 433EA4C41D9F71C800CD78FB /* CommandResponseViewController.swift in Sources */, 43D2E8231F00425400AE5CBF /* BolusViewController+LoopDataManager.swift in Sources */, @@ -1875,9 +1934,12 @@ 4328E0331CFC091100E199AA /* WatchContext+LoopKit.swift in Sources */, 4F526D611DF8D9A900A04910 /* NetBasal.swift in Sources */, 43C3B6EC20B650A80026CAFA /* SettingsImageTableViewCell.swift in Sources */, + 4F7E8ACB20E2ACB500AEA65E /* WatchPredictedGlucose.swift in Sources */, 436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */, + 4F11D3C220DD80B3006E072C /* WatchHistoricalGlucose.swift in Sources */, 435CB6231F37967800C320C7 /* InsulinModelSettingsViewController.swift in Sources */, 431E73481FF95A900069B5F7 /* PersistenceController.swift in Sources */, + 4372E490213CFCE70068E043 /* LoopSettingsUserInfo.swift in Sources */, 43F78D261C8FC000002152D1 /* DoseMath.swift in Sources */, 438D42F91D7C88BC003244B0 /* PredictionInputEffect.swift in Sources */, 4F70C2101DE8FAC5006380B7 /* StatusExtensionDataManager.swift in Sources */, @@ -1903,33 +1965,53 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4372E488213C862B0068E043 /* SampleValue.swift in Sources */, 4F2C15741E0209F500E160D4 /* NSTimeInterval.swift in Sources */, 4FF4D1011E18375000846527 /* WatchContext.swift in Sources */, 435400311C9F744E00D5819C /* BolusSuggestionUserInfo.swift in Sources */, + 4372E49A213F7A830068E043 /* WalshInsulinModel.swift in Sources */, 43A9438A1B926B7B0051FA24 /* NotificationController.swift in Sources */, 439A7945211FE23A0041B75F /* NSUserActivity.swift in Sources */, 43A943881B926B7B0051FA24 /* ExtensionDelegate.swift in Sources */, + 4372E498213F7A550068E043 /* InsulinModelSettings.swift in Sources */, + 4F75F00220FCFE8C00B5570E /* GlucoseChartScene.swift in Sources */, 4328E02F1CFBF81800E199AA /* WKInterfaceImage.swift in Sources */, 4F2C15811E0495B200E160D4 /* WatchContext+WatchApp.swift in Sources */, + 4372E496213DCDD30068E043 /* GlucoseChartValueHashable.swift in Sources */, 4344629820A8B2D700C4BE6F /* OSLog.swift in Sources */, 4328E02A1CFBE2C500E199AA /* UIColor.swift in Sources */, + 4372E484213A63FB0068E043 /* ChartHUDController.swift in Sources */, + 4FDDD23720DC51DF00D04B16 /* LoopDataManager.swift in Sources */, 4328E01B1CFBE1DA00E199AA /* BolusInterfaceController.swift in Sources */, + 4F82655020E69F9A0031A8F5 /* HUDInterfaceController.swift in Sources */, + 4372E492213D956C0068E043 /* GlucoseRangeSchedule.swift in Sources */, 4328E02B1CFBE2C500E199AA /* WKAlertAction.swift in Sources */, + 4F7E8AC720E2AC0300AEA65E /* WatchPredictedGlucose.swift in Sources */, 4344628E20A7ADD100C4BE6F /* UserDefaults+CGM.swift in Sources */, - 894B91CE1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift in Sources */, + 4372E499213F7A6D0068E043 /* ExponentialInsulinModelPreset.swift in Sources */, + 4F7E8AC520E2AB9600AEA65E /* Date.swift in Sources */, + 4F11D3C420DD881A006E072C /* WatchHistoricalGlucose.swift in Sources */, 4328E0281CFBE2C500E199AA /* CLKComplicationTemplate.swift in Sources */, 4328E01E1CFBE25F00E199AA /* AddCarbsInterfaceController.swift in Sources */, - 43846ADB1D91057000799272 /* ContextUpdatable.swift in Sources */, 4328E0261CFBE2C500E199AA /* IdentifiableClass.swift in Sources */, - 432CF87520D8AC950066B889 /* NSUserDefaults.swift in Sources */, + 4F73F5FC20E2E7FA00E8D82C /* GlucoseStore.swift in Sources */, + 432CF87520D8AC950066B889 /* NSUserDefaults+WatchApp.swift in Sources */, 43027F0F1DFE0EC900C51989 /* HKUnit.swift in Sources */, 4344629220A7C19800C4BE6F /* ButtonGroup.swift in Sources */, + 4372E48C213CB6750068E043 /* Double.swift in Sources */, 43785E972120E4500057DED1 /* INRelevantShortcutStore+Loop.swift in Sources */, + 4372E48E213CF8AD0068E043 /* LoopSettings.swift in Sources */, + 4372E49B213F7B340068E043 /* NSBundle.swift in Sources */, 43CB2B2B1D924D450079823D /* WCSession.swift in Sources */, + 4372E497213F79F90068E043 /* NSUserDefaults.swift in Sources */, + 4372E491213D05F90068E043 /* LoopSettingsUserInfo.swift in Sources */, 43DE925A1C5479E4001FFDE1 /* CarbEntryUserInfo.swift in Sources */, + 4FF0F75E20E1E5D100FC6291 /* PersistenceController.swift in Sources */, 43BFF0B51E45C1E700FF19A9 /* NumberFormatter.swift in Sources */, 43A9438E1B926B7B0051FA24 /* ComplicationController.swift in Sources */, - 4328E01A1CFBE1DA00E199AA /* StatusInterfaceController.swift in Sources */, + 4328E01A1CFBE1DA00E199AA /* ActionHUDController.swift in Sources */, + 4F11D3C320DD84DB006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */, + 4372E48D213CF8A70068E043 /* GlucoseThreshold.swift in Sources */, 435400351C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Loop/Managers/DiagnosticLogger.swift b/Loop/Managers/DiagnosticLogger.swift index a02a85b5cd..9959765e66 100644 --- a/Loop/Managers/DiagnosticLogger.swift +++ b/Loop/Managers/DiagnosticLogger.swift @@ -108,7 +108,7 @@ final class CategoryLogger { } func `default`(_ message: String) { - systemLog.info("%{public}@", message) + systemLog.default("%{public}@", message) remoteLog(.default, message: message) } diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index f59f05fa28..e7c055e7dd 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -11,8 +11,7 @@ import UIKit import WatchConnectivity import LoopKit - -final class WatchDataManager: NSObject, WCSessionDelegate { +final class WatchDataManager: NSObject { unowned let deviceManager: DeviceDataManager @@ -38,8 +37,7 @@ final class WatchDataManager: NSObject, WCSessionDelegate { } }() - private var lastActiveOverrideContext: GlucoseRangeSchedule.Override.Context? - private var lastConfiguredOverrideContexts: [GlucoseRangeSchedule.Override.Context] = [] + private var lastSentSettings: LoopSettings? @objc private func updateWatch(_ notification: Notification) { guard @@ -51,19 +49,13 @@ final class WatchDataManager: NSObject, WCSessionDelegate { } switch updateContext { + case .glucose: + break case .tempBasal: break case .preferences: - let activeOverrideContext = deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.activeOverrideContext - let configuredOverrideContexts = deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.configuredOverrideContexts ?? [] - defer { - lastActiveOverrideContext = activeOverrideContext - lastConfiguredOverrideContexts = configuredOverrideContexts - } - - guard activeOverrideContext != lastActiveOverrideContext || configuredOverrideContexts != lastConfiguredOverrideContexts else { - return - } + sendSettingsIfNeeded() + return default: return } @@ -85,6 +77,35 @@ final class WatchDataManager: NSObject, WCSessionDelegate { private let minTrendDrift: Double = 20 private lazy var minTrendUnit = HKUnit.milligramsPerDeciliter + private func sendSettingsIfNeeded() { + let settings = deviceManager.loopManager.settings + + guard let session = watchSession, session.isPaired, session.isWatchAppInstalled else { + return + } + + guard case .activated = session.activationState else { + session.activate() + return + } + + guard settings != lastSentSettings else { + log.default("Skipping settings transfer due to no changes") + return + } + + lastSentSettings = settings + + log.default("Transferring LoopSettingsUserInfo") + session.transferUserInfo(LoopSettingsUserInfo(settings: settings).rawValue) + + createWatchContext { (context) in + if let context = context { + self.sendWatchContext(context) + } + } + } + private func sendWatchContext(_ context: WatchContext) { if let session = watchSession, session.isPaired && session.isWatchAppInstalled { let complicationShouldUpdate: Bool @@ -123,34 +144,45 @@ final class WatchDataManager: NSObject, WCSessionDelegate { let reservoir = loopManager.doseStore.lastReservoirValue loopManager.getLoopState { (manager, state) in - let eventualGlucose = state.predictedGlucose?.last - let context = WatchContext(glucose: glucose, eventualGlucose: eventualGlucose, glucoseUnit: manager.glucoseStore.preferredUnit) + let updateGroup = DispatchGroup() + let context = WatchContext(glucose: glucose, glucoseUnit: manager.glucoseStore.preferredUnit) context.reservoir = reservoir?.unitVolume - context.loopLastRunDate = manager.lastLoopCompleted context.recommendedBolusDose = state.recommendedBolus?.recommendation.amount - context.maxBolus = manager.settings.maximumBolus + context.cob = state.carbsOnBoard?.quantity.doubleValue(for: HKUnit.gram()) + context.glucoseTrendRawValue = self.deviceManager.cgmManager?.sensorState?.trendType?.rawValue context.cgmManagerState = self.deviceManager.cgmManager?.rawValue - if let glucoseTargetRangeSchedule = manager.settings.glucoseTargetRangeSchedule { - if let override = glucoseTargetRangeSchedule.override { - context.glucoseRangeScheduleOverride = GlucoseRangeScheduleOverrideUserInfo( - context: override.context.correspondingUserInfoContext, - startDate: override.start, - endDate: override.end - ) + if let trend = self.deviceManager.cgmManager?.sensorState?.trendType { + context.glucoseTrendRawValue = trend.rawValue + } + + updateGroup.enter() + manager.doseStore.insulinOnBoard(at: Date()) { (result) in + switch result { + case .success(let iobValue): + context.iob = iobValue.value + case .failure: + context.iob = nil } + updateGroup.leave() + } - let configuredOverrideContexts = self.deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.configuredOverrideContexts ?? [] - let configuredUserInfoOverrideContexts = configuredOverrideContexts.map { $0.correspondingUserInfoContext } - context.configuredOverrideContexts = configuredUserInfoOverrideContexts + // Only set this value in the Watch context if there is a temp basal running that hasn't ended yet + let date = state.lastTempBasal?.startDate ?? Date() + if let scheduledBasal = manager.basalRateSchedule?.between(start: date, end: date).first, + let lastTempBasal = state.lastTempBasal, + lastTempBasal.endDate > Date() { + context.lastNetTempBasalDose = lastTempBasal.unitsPerHour - scheduledBasal.value } - if let trend = self.deviceManager.cgmManager?.sensorState?.trendType { - context.glucoseTrendRawValue = trend.rawValue + // Drop the first element in predictedGlucose because it is the current glucose + if let predictedGlucose = state.predictedGlucose?.dropFirst(), predictedGlucose.count > 0 { + context.predictedGlucose = WatchPredictedGlucose(values: Array(predictedGlucose)) } + _ = updateGroup.wait(timeout: .distantFuture) completion(context) } } @@ -178,9 +210,10 @@ final class WatchDataManager: NSObject, WCSessionDelegate { completionHandler?(nil) } } +} - // MARK: WCSessionDelegate +extension WatchDataManager: WCSessionDelegate { func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { switch message["name"] as? String { case CarbEntryUserInfo.name?: @@ -197,24 +230,24 @@ final class WatchDataManager: NSObject, WCSessionDelegate { } replyHandler([:]) - case GlucoseRangeScheduleOverrideUserInfo.name?: - if let overrideUserInfo = GlucoseRangeScheduleOverrideUserInfo(rawValue: message) { - let overrideContext = overrideUserInfo.context.correspondingOverrideContext - - // update the recorded last active override context prior to enabling the actual override - // to prevent the Watch context being unnecessarily sent in response to the override being enabled - let previousActiveOverrideContext = lastActiveOverrideContext - lastActiveOverrideContext = overrideContext - let overrideSuccess = deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.setOverride(overrideContext, from: overrideUserInfo.startDate, until: overrideUserInfo.effectiveEndDate) - - if overrideSuccess == false { - lastActiveOverrideContext = previousActiveOverrideContext + case LoopSettingsUserInfo.name?: + if let watchSettings = LoopSettingsUserInfo(rawValue: message)?.settings { + // So far we only support watch changes of target range overrides + var settings = deviceManager.loopManager.settings + settings.glucoseTargetRangeSchedule = watchSettings.glucoseTargetRangeSchedule + + // Prevent re-sending these updated settings back to the watch + lastSentSettings = settings + deviceManager.loopManager.settings = settings + } + replyHandler([:]) + case GlucoseBackfillRequestUserInfo.name?: + if let userInfo = GlucoseBackfillRequestUserInfo(rawValue: message), + let manager = deviceManager.loopManager { + manager.glucoseStore.getCachedGlucoseSamples(start: userInfo.startDate.addingTimeInterval(1)) { (values) in + replyHandler(WatchHistoricalGlucose(with: values).rawValue) } - - replyHandler([:]) } else { - lastActiveOverrideContext = nil - deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.clearOverride() replyHandler([:]) } default: @@ -231,6 +264,8 @@ final class WatchDataManager: NSObject, WCSessionDelegate { case .activated: if let error = error { log.error(error) + } else { + sendSettingsIfNeeded() } case .inactive, .notActivated: break @@ -240,6 +275,15 @@ final class WatchDataManager: NSObject, WCSessionDelegate { func session(_ session: WCSession, didFinish userInfoTransfer: WCSessionUserInfoTransfer, error: Error?) { if let error = error { log.error(error) + + // This might be useless, as userInfoTransfer.userInfo seems to be nil when error is non-nil. + switch userInfoTransfer.userInfo["name"] as? String { + case LoopSettingsUserInfo.name?, .none: + lastSentSettings = nil + sendSettingsIfNeeded() + default: + break + } } } @@ -252,26 +296,8 @@ final class WatchDataManager: NSObject, WCSessionDelegate { watchSession?.delegate = self watchSession?.activate() } -} -fileprivate extension GlucoseRangeSchedule.Override.Context { - var correspondingUserInfoContext: GlucoseRangeScheduleOverrideUserInfo.Context { - switch self { - case .preMeal: - return .preMeal - case .workout: - return .workout - } - } -} - -fileprivate extension GlucoseRangeScheduleOverrideUserInfo.Context { - var correspondingOverrideContext: GlucoseRangeSchedule.Override.Context { - switch self { - case .preMeal: - return .preMeal - case .workout: - return .workout - } + func sessionReachabilityDidChange(_ session: WCSession) { + sendSettingsIfNeeded() } } diff --git a/Loop/Models/WatchContext+LoopKit.swift b/Loop/Models/WatchContext+LoopKit.swift index 24a67c6854..745479ef86 100644 --- a/Loop/Models/WatchContext+LoopKit.swift +++ b/Loop/Models/WatchContext+LoopKit.swift @@ -11,12 +11,11 @@ import HealthKit import LoopKit extension WatchContext { - convenience init(glucose: GlucoseValue?, eventualGlucose: GlucoseValue?, glucoseUnit: HKUnit?) { + convenience init(glucose: GlucoseValue?, glucoseUnit: HKUnit?) { self.init() self.glucose = glucose?.quantity self.glucoseDate = glucose?.startDate - self.eventualGlucose = eventualGlucose?.quantity self.preferredGlucoseUnit = glucoseUnit } } diff --git a/LoopUI/Extensions/ChartPoint.swift b/LoopUI/Extensions/ChartPoint.swift index 24230d2694..032f934726 100644 --- a/LoopUI/Extensions/ChartPoint.swift +++ b/LoopUI/Extensions/ChartPoint.swift @@ -59,7 +59,7 @@ extension ChartPoint { static func pointsForGlucoseRangeScheduleOverride(_ override: GlucoseRangeSchedule.Override, unit: HKUnit, xAxisValues: [ChartAxisValue], extendEndDateToChart: Bool = false) -> [ChartPoint] { let range = override.value.rangeWithMinimumIncremement(unit.chartableIncrement) let startDate = Date() - let endDate = override.end ?? .distantFuture + let endDate = override.end guard endDate.timeIntervalSince(startDate) > 0, let lastXAxisValue = xAxisValues.last as? ChartAxisValueDate diff --git a/WatchApp Extension/ComplicationController.swift b/WatchApp Extension/ComplicationController.swift index 4b149cabb1..e889eeb229 100644 --- a/WatchApp Extension/ComplicationController.swift +++ b/WatchApp Extension/ComplicationController.swift @@ -19,7 +19,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { } func getTimelineStartDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { - if let date = ExtensionDelegate.shared().lastContext?.glucoseDate { + if let date = ExtensionDelegate.shared().loopManager.activeContext?.glucoseDate { handler(date) } else { handler(nil) @@ -27,7 +27,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { } func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { - if let date = ExtensionDelegate.shared().lastContext?.glucoseDate { + if let date = ExtensionDelegate.shared().loopManager.activeContext?.glucoseDate { handler(date) } else { handler(nil) @@ -45,7 +45,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: (@escaping (CLKComplicationTimelineEntry?) -> Void)) { let entry: CLKComplicationTimelineEntry? - if let context = ExtensionDelegate.shared().lastContext, + if let context = ExtensionDelegate.shared().loopManager.activeContext, let glucoseDate = context.glucoseDate, glucoseDate.timeIntervalSinceNow.minutes >= -15, let template = CLKComplicationTemplate.templateForFamily(complication.family, from: context) @@ -68,7 +68,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { // Call the handler with the timeline entries after to the given date let entries: [CLKComplicationTimelineEntry]? - if let context = ExtensionDelegate.shared().lastContext, + if let context = ExtensionDelegate.shared().loopManager.activeContext, let glucoseDate = context.glucoseDate, glucoseDate.timeIntervalSince(date) > 0, let template = CLKComplicationTemplate.templateForFamily(complication.family, from: context) diff --git a/WatchApp Extension/Controllers/ActionHUDController.swift b/WatchApp Extension/Controllers/ActionHUDController.swift new file mode 100644 index 0000000000..e5bfd49e68 --- /dev/null +++ b/WatchApp Extension/Controllers/ActionHUDController.swift @@ -0,0 +1,157 @@ +// +// ActionHUDController.swift +// Loop +// +// Created by Nathan Racklyeft on 5/29/16. +// Copyright © 2016 Nathan Racklyeft. All rights reserved. +// + +import WatchKit +import WatchConnectivity +import LoopKit + + +final class ActionHUDController: HUDInterfaceController { + @IBOutlet var preMealButton: WKInterfaceButton! + @IBOutlet var preMealButtonImage: WKInterfaceImage! + @IBOutlet var preMealButtonBackground: WKInterfaceGroup! + @IBOutlet var workoutButton: WKInterfaceButton! + @IBOutlet var workoutButtonImage: WKInterfaceImage! + @IBOutlet var workoutButtonBackground: WKInterfaceGroup! + + private lazy var preMealButtonGroup = ButtonGroup(button: preMealButton, image: preMealButtonImage, background: preMealButtonBackground, onBackgroundColor: .carbsColor, offBackgroundColor: .darkCarbsColor) + + private lazy var workoutButtonGroup = ButtonGroup(button: workoutButton, image: workoutButtonImage, background: workoutButtonBackground, onBackgroundColor: .workoutColor, offBackgroundColor: .darkWorkoutColor) + + override func willActivate() { + super.willActivate() + + let userActivity = NSUserActivity.forViewLoopStatus() + if #available(watchOSApplicationExtension 5.0, *) { + update(userActivity) + } else { + updateUserActivity(userActivity.activityType, userInfo: userActivity.userInfo, webpageURL: nil) + } + } + override func update() { + super.update() + + let schedule = loopManager?.settings.glucoseTargetRangeSchedule + let activeOverrideContext: GlucoseRangeSchedule.Override.Context? + if let glucoseRangeScheduleOverride = schedule?.override, glucoseRangeScheduleOverride.isActive() + { + activeOverrideContext = glucoseRangeScheduleOverride.context + } else { + activeOverrideContext = nil + } + updateForOverrideContext(activeOverrideContext) + + for overrideContext in GlucoseRangeSchedule.Override.Context.all { + let contextButtonGroup = buttonGroup(for: overrideContext) + if schedule == nil || !(schedule!.configuredOverrideContexts.contains(overrideContext)) { + contextButtonGroup.state = .disabled + } else if contextButtonGroup.state == .disabled { + contextButtonGroup.state = .off + } + } + } + + private func updateForOverrideContext(_ context: GlucoseRangeSchedule.Override.Context?) { + switch context { + case .preMeal?: + preMealButtonGroup.state = .on + workoutButtonGroup.turnOff() + case .workout?: + preMealButtonGroup.turnOff() + workoutButtonGroup.state = .on + case nil: + preMealButtonGroup.turnOff() + workoutButtonGroup.turnOff() + } + } + + private func buttonGroup(for overrideContext: GlucoseRangeSchedule.Override.Context) -> ButtonGroup { + switch overrideContext { + case .preMeal: + return preMealButtonGroup + case .workout: + return workoutButtonGroup + } + } + + // MARK: - Menu Items + + @IBAction func togglePreMealMode() { + guard var glucoseTargetRangeSchedule = loopManager?.settings.glucoseTargetRangeSchedule else { + return + } + if preMealButtonGroup.state == .on { + glucoseTargetRangeSchedule.clearOverride() + } else { + guard glucoseTargetRangeSchedule.setOverride(.preMeal, until: Date(timeIntervalSinceNow: .hours(1))) else { + return + } + } + + sendGlucoseRangeSchedule(glucoseTargetRangeSchedule) + } + + @IBAction func toggleWorkoutMode() { + guard var glucoseTargetRangeSchedule = loopManager?.settings.glucoseTargetRangeSchedule else { + return + } + if workoutButtonGroup.state == .on { + glucoseTargetRangeSchedule.clearOverride() + } else { + guard glucoseTargetRangeSchedule.setOverride(.workout, until: .distantFuture) else { + return + } + } + + sendGlucoseRangeSchedule(glucoseTargetRangeSchedule) + } + + private var pendingMessageResponses = 0 + + private func sendGlucoseRangeSchedule(_ schedule: GlucoseRangeSchedule) { + updateForOverrideContext(schedule.override?.context) + pendingMessageResponses += 1 + do { + var settings = LoopSettings() + settings.glucoseTargetRangeSchedule = schedule + let userInfo = LoopSettingsUserInfo(settings: settings) + + try WCSession.default.sendSettingsUpdateMessage(userInfo, completionHandler: { (error) in + DispatchQueue.main.async { + self.pendingMessageResponses -= 1 + if let error = error { + if self.pendingMessageResponses == 0 { + ExtensionDelegate.shared().present(error) + self.updateForOverrideContext(self.loopManager?.settings.glucoseTargetRangeSchedule?.override?.context) + } + } else { + if self.pendingMessageResponses == 0 { + self.loopManager?.settings.glucoseTargetRangeSchedule = schedule + } + } + } + }) + } catch { + pendingMessageResponses -= 1 + if pendingMessageResponses == 0 { + updateForOverrideContext(self.loopManager?.settings.glucoseTargetRangeSchedule?.override?.context) + } + presentAlert( + withTitle: NSLocalizedString("Send Failed", comment: "The title of the alert controller displayed after a glucose range override send attempt fails"), + message: NSLocalizedString("Make sure your iPhone is nearby and try again", comment: "The recovery message displayed after a glucose range override send attempt fails"), + preferredStyle: .alert, + actions: [WKAlertAction.dismissAction()] + ) + } + } +} + + +extension GlucoseRangeSchedule.Override.Context { + static let all: [GlucoseRangeSchedule.Override.Context] = [.preMeal, .workout] +} diff --git a/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift b/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift index 1425509e40..43e66d96e2 100644 --- a/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift +++ b/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift @@ -17,16 +17,19 @@ final class AddCarbsInterfaceController: WKInterfaceController, IdentifiableClas private var carbValue: Int = 15 { didSet { - if carbValue < 0 { - carbValue = 0 - } else if carbValue > 100 { - carbValue = 100 + if carbValue < minimumCarbValue { + carbValue = minimumCarbValue + } else if carbValue > maximumCarbValue { + carbValue = maximumCarbValue } valueLabel.setText(String(carbValue)) } } + private let minimumCarbValue = 0 + private let maximumCarbValue = 100 + private let maximumDatePastInterval = TimeInterval(hours: 8) private let maximumDateFutureInterval = TimeInterval(hours: 4) @@ -138,6 +141,8 @@ final class AddCarbsInterfaceController: WKInterfaceController, IdentifiableClas case .date: date -= dateIncrement } + + WKInterfaceDevice.current().play(.directionDown) } @IBAction func increment() { @@ -147,6 +152,8 @@ final class AddCarbsInterfaceController: WKInterfaceController, IdentifiableClas case .date: date += dateIncrement } + + WKInterfaceDevice.current().play(.directionUp) } @IBAction func toggleInputMode() { @@ -173,11 +180,13 @@ final class AddCarbsInterfaceController: WKInterfaceController, IdentifiableClas try WCSession.default.sendCarbEntryMessage(entry, replyHandler: { (suggestion) in DispatchQueue.main.async { + WKInterfaceDevice.current().play(.success) WKExtension.shared().rootInterfaceController?.presentController(withName: BolusInterfaceController.className, context: suggestion) } }, errorHandler: { (error) in DispatchQueue.main.async { + WKInterfaceDevice.current().play(.failure) ExtensionDelegate.shared().present(error) } } @@ -206,13 +215,31 @@ extension AddCarbsInterfaceController: WKCrownDelegate { func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) { accumulatedRotation += rotationalDelta let remainder = accumulatedRotation.truncatingRemainder(dividingBy: rotationsPerIncrement) - let delta = (accumulatedRotation - remainder) / rotationsPerIncrement + var delta = Int((accumulatedRotation - remainder) / rotationsPerIncrement) switch inputMode { case .value: - carbValue += Int(delta) + let oldValue = carbValue + carbValue += delta + + // If we didn't change, adjust the delta to prevent the haptic + if oldValue == carbValue { + delta = 0 + } case .date: - date += TimeInterval(minutes: delta) + let oldValue = date + date += TimeInterval(minutes: Double(delta)) + + // If we didn't change, adjust the delta to prevent the haptic + if oldValue == date { + delta = 0 + } + } + + if delta > 0 { + WKInterfaceDevice.current().play(.click) + } else if delta < 0 { + WKInterfaceDevice.current().play(.click) } accumulatedRotation = remainder diff --git a/WatchApp Extension/Controllers/BolusInterfaceController.swift b/WatchApp Extension/Controllers/BolusInterfaceController.swift index ff2bf99613..e3015a67ca 100644 --- a/WatchApp Extension/Controllers/BolusInterfaceController.swift +++ b/WatchApp Extension/Controllers/BolusInterfaceController.swift @@ -85,7 +85,7 @@ final class BolusInterfaceController: WKInterfaceController, IdentifiableClass { var pickerValue = 0 if let context = context as? BolusSuggestionUserInfo { - let recommendedBolus = context.recommendedBolus + let recommendedBolus = context.recommendedBolus ?? 0 if let maxBolus = context.maxBolus { maxBolusValue = maxBolus @@ -95,7 +95,7 @@ final class BolusInterfaceController: WKInterfaceController, IdentifiableClass { pickerValue = pickerValueFromBolusValue(recommendedBolus * 0.75) - if let valueString = formatter.string(from: recommendedBolus) { + if let recommendedBolus = context.recommendedBolus, let valueString = formatter.string(from: recommendedBolus) { recommendedValueLabel.setText(String(format: NSLocalizedString("Rec: %@ U", comment: "The label and value showing the recommended bolus"), valueString).localizedUppercase) } } @@ -122,10 +122,14 @@ final class BolusInterfaceController: WKInterfaceController, IdentifiableClass { @IBAction func decrement() { pickerValue -= 10 + + WKInterfaceDevice.current().play(.directionDown) } @IBAction func increment() { pickerValue += 10 + + WKInterfaceDevice.current().play(.directionUp) } @IBAction func deliver() { @@ -166,7 +170,22 @@ extension BolusInterfaceController: WKCrownDelegate { accumulatedRotation += rotationalDelta let remainder = accumulatedRotation.truncatingRemainder(dividingBy: rotationsPerValue) - pickerValue += Int((accumulatedRotation - remainder) / rotationsPerValue) + var delta = Int((accumulatedRotation - remainder) / rotationsPerValue) + + let oldValue = pickerValue + pickerValue += delta + + // If we didn't change, adjust the delta to prevent the haptic + if oldValue == pickerValue { + delta = 0 + } + + if delta > 0 { + WKInterfaceDevice.current().play(.click) + } else if delta < 0 { + WKInterfaceDevice.current().play(.click) + } + accumulatedRotation = remainder } } diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift new file mode 100644 index 0000000000..2545739dc7 --- /dev/null +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -0,0 +1,197 @@ +// +// ChartHUDController.swift +// Loop +// +// Created by Bharat Mediratta on 6/26/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import WatchKit +import WatchConnectivity +import CGMBLEKit +import LoopKit +import SpriteKit +import os.log + +final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { + @IBOutlet weak var basalLabel: WKInterfaceLabel! + @IBOutlet weak var iobLabel: WKInterfaceLabel! + @IBOutlet weak var cobLabel: WKInterfaceLabel! + @IBOutlet weak var glucoseScene: WKInterfaceSKScene! + @IBAction func setChartWindow1Hour() { + scene.visibleDuration = .hours(2) + } + @IBAction func setChartWindow2Hours() { + scene.visibleDuration = .hours(4) + } + @IBAction func setChartWindow3Hours() { + scene.visibleDuration = .hours(6) + } + private let scene = GlucoseChartScene() + private var timer: Timer? { + didSet { + oldValue?.invalidate() + } + } + private let log = OSLog(category: "ChartHUDController") + + override init() { + super.init() + + loopManager = ExtensionDelegate.shared().loopManager + NotificationCenter.default.addObserver(forName: .GlucoseSamplesDidChange, object: loopManager?.glucoseStore, queue: nil) { [weak self] (note) in + self?.log.default("Received GlucoseSamplesDidChange notification: %{public}@. Updating chart", String(describing: note.userInfo ?? [:])) + + DispatchQueue.main.async { + self?.updateGlucoseChart() + } + } + + glucoseScene.presentScene(scene) + } + + override func awake(withContext context: Any?) { + super.awake(withContext: context) + + if UserDefaults.standard.startOnChartPage { + log.default("Switching to startOnChartPage") + becomeCurrentPage() + + // For some reason, .didAppear() does not get called when we do this. It gets called *twice* the next + // time this view appears. Force it by hand now, until we figure out the root cause. + // + // TODO: possibly because I'm not calling super.awake()? investigate that. + DispatchQueue.main.async { + self.didAppear() + } + } + } + + override func didAppear() { + super.didAppear() + + log.default("didAppear") + + // Force an update when our pixels need to move + let pixelsWide = scene.size.width * WKInterfaceDevice.current().screenScale + let pixelInterval = scene.visibleDuration / TimeInterval(pixelsWide) + + timer = Timer.scheduledTimer(withTimeInterval: pixelInterval, repeats: true) { [weak self] _ in + self?.scene.setNeedsUpdate() + } + } + + override func willDisappear() { + super.willDisappear() + + log.default("willDisappear") + + timer = nil + } + + override func willActivate() { + super.willActivate() + + if glucoseScene.isPaused { + log.default("willActivate() unpausing") + glucoseScene.isPaused = false + } else { + log.default("willActivate() unpausing") + } + + loopManager?.requestGlucoseBackfillIfNecessary() + } + + override func didDeactivate() { + super.didDeactivate() + + log.default("didDeactivate() pausing") + glucoseScene.isPaused = true + } + + override func update() { + super.update() + + guard let activeContext = loopManager?.activeContext else { + return + } + + let insulinFormatter: NumberFormatter = { + let numberFormatter = NumberFormatter() + + numberFormatter.numberStyle = .decimal + numberFormatter.minimumFractionDigits = 1 + numberFormatter.maximumFractionDigits = 1 + + return numberFormatter + }() + + iobLabel.setHidden(true) + if let activeInsulin = activeContext.iob, let valueStr = insulinFormatter.string(from: activeInsulin) { + iobLabel.setText(String(format: NSLocalizedString( + "IOB %1$@ U", + comment: "The subtitle format describing units of active insulin. (1: localized insulin value description)" + ), + valueStr + )) + iobLabel.setHidden(false) + } + + cobLabel.setHidden(true) + if let carbsOnBoard = activeContext.cob { + let carbFormatter = NumberFormatter() + carbFormatter.numberStyle = .decimal + carbFormatter.maximumFractionDigits = 0 + let valueStr = carbFormatter.string(from: carbsOnBoard) + + cobLabel.setText(String(format: NSLocalizedString( + "COB %1$@ g", + comment: "The subtitle format describing grams of active carbs. (1: localized carb value description)" + ), + valueStr! + )) + cobLabel.setHidden(false) + } + + basalLabel.setHidden(true) + if let tempBasal = activeContext.lastNetTempBasalDose { + let basalFormatter = NumberFormatter() + basalFormatter.numberStyle = .decimal + basalFormatter.minimumFractionDigits = 1 + basalFormatter.maximumFractionDigits = 3 + basalFormatter.positivePrefix = basalFormatter.plusSign + let valueStr = basalFormatter.string(from: tempBasal) + + let basalLabelText = String(format: NSLocalizedString( + "%1$@ U/hr", + comment: "The subtitle format describing the current temp basal rate. (1: localized basal rate description)"), + valueStr!) + basalLabel.setText(basalLabelText) + basalLabel.setHidden(false) + } + + if glucoseScene.isPaused { + log.default("update() unpausing") + glucoseScene.isPaused = false + } + + updateGlucoseChart() + } + + func updateGlucoseChart() { + guard let activeContext = loopManager?.activeContext else { + return + } + + scene.predictedGlucose = activeContext.predictedGlucose?.values + scene.correctionRange = loopManager?.settings.glucoseTargetRangeSchedule + scene.unit = activeContext.preferredGlucoseUnit + + loopManager?.glucoseStore.getCachedGlucoseSamples(start: .earliestGlucoseCutoff) { (samples) in + DispatchQueue.main.async { + self.scene.historicalGlucose = samples + self.scene.setNeedsUpdate() + } + } + } +} diff --git a/WatchApp Extension/Controllers/ContextUpdatable.swift b/WatchApp Extension/Controllers/ContextUpdatable.swift deleted file mode 100644 index 00cc2100d7..0000000000 --- a/WatchApp Extension/Controllers/ContextUpdatable.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// ContextUpdatable.swift -// Loop -// -// Created by Nate Racklyeft on 9/19/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import Foundation - - -protocol ContextUpdatable { - func update(with context: WatchContext?) -} diff --git a/WatchApp Extension/Controllers/HUDInterfaceController.swift b/WatchApp Extension/Controllers/HUDInterfaceController.swift new file mode 100644 index 0000000000..55eb8299d7 --- /dev/null +++ b/WatchApp Extension/Controllers/HUDInterfaceController.swift @@ -0,0 +1,103 @@ +// +// HUDInterfaceController.swift +// WatchApp Extension +// +// Created by Bharat Mediratta on 6/29/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import WatchKit + +class HUDInterfaceController: WKInterfaceController { + private var activeContextObserver: NSObjectProtocol? + + @IBOutlet weak var loopHUDImage: WKInterfaceImage! + @IBOutlet weak var loopTimer: WKInterfaceTimer! + @IBOutlet weak var glucoseLabel: WKInterfaceLabel! + @IBOutlet weak var eventualGlucoseLabel: WKInterfaceLabel! + + weak var loopManager: LoopDataManager? + + override init() { + loopManager = ExtensionDelegate.shared().loopManager + } + + override func willActivate() { + super.willActivate() + + update() + + if activeContextObserver == nil { + activeContextObserver = NotificationCenter.default.addObserver(forName: LoopDataManager.didUpdateContextNotification, object: loopManager, queue: nil) { [weak self] _ in + DispatchQueue.main.async { + self?.update() + } + } + } + } + + override func didDeactivate() { + super.didDeactivate() + + if let observer = activeContextObserver { + NotificationCenter.default.removeObserver(observer) + } + activeContextObserver = nil + } + + func update() { + guard let activeContext = loopManager?.activeContext, let date = activeContext.loopLastRunDate else { + loopHUDImage.setLoopImage(.unknown) + loopTimer.setHidden(true) + return + } + + loopTimer.setDate(date) + loopTimer.setHidden(false) + loopTimer.start() + + glucoseLabel.setHidden(true) + eventualGlucoseLabel.setHidden(true) + if let glucose = activeContext.glucose, let unit = activeContext.preferredGlucoseUnit { + let formatter = NumberFormatter.glucoseFormatter(for: unit) + + if let glucoseValue = formatter.string(from: glucose.doubleValue(for: unit)) { + let trend = activeContext.glucoseTrend?.symbol ?? "" + glucoseLabel.setText(glucoseValue + trend) + glucoseLabel.setHidden(false) + } + + if let eventualGlucose = activeContext.eventualGlucose { + let glucoseValue = formatter.string(from: eventualGlucose.doubleValue(for: unit)) + eventualGlucoseLabel.setText(glucoseValue) + eventualGlucoseLabel.setHidden(false) + } + } + + loopHUDImage.setLoopImage({ + switch date.timeIntervalSinceNow { + case let t where t > .minutes(-6): + return .fresh + case let t where t > .minutes(-20): + return .aging + default: + return .stale + } + }()) + } + + @IBAction func addCarbs() { + presentController(withName: AddCarbsInterfaceController.className, context: nil) + } + + @IBAction func setBolus() { + var context = loopManager?.activeContext?.bolusSuggestion ?? BolusSuggestionUserInfo(recommendedBolus: nil) + + if context.maxBolus == nil { + context.maxBolus = loopManager?.settings.maximumBolus + } + + presentController(withName: BolusInterfaceController.className, context: context) + } + +} diff --git a/WatchApp Extension/Controllers/StatusInterfaceController.swift b/WatchApp Extension/Controllers/StatusInterfaceController.swift deleted file mode 100644 index 9e56ce5ded..0000000000 --- a/WatchApp Extension/Controllers/StatusInterfaceController.swift +++ /dev/null @@ -1,232 +0,0 @@ -// -// StatusInterfaceController.swift -// Loop -// -// Created by Nathan Racklyeft on 5/29/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import WatchKit -import WatchConnectivity -import LoopKit - - -final class StatusInterfaceController: WKInterfaceController, ContextUpdatable { - - @IBOutlet weak var loopHUDImage: WKInterfaceImage! - @IBOutlet weak var loopTimer: WKInterfaceTimer! - @IBOutlet weak var glucoseLabel: WKInterfaceLabel! - @IBOutlet weak var eventualGlucoseLabel: WKInterfaceLabel! - @IBOutlet weak var statusLabel: WKInterfaceLabel! - - @IBOutlet var preMealButton: WKInterfaceButton! - @IBOutlet var preMealButtonImage: WKInterfaceImage! - @IBOutlet var preMealButtonBackground: WKInterfaceGroup! - - @IBOutlet var workoutButton: WKInterfaceButton! - @IBOutlet var workoutButtonImage: WKInterfaceImage! - @IBOutlet var workoutButtonBackground: WKInterfaceGroup! - - private lazy var preMealButtonGroup = ButtonGroup(button: preMealButton, image: preMealButtonImage, background: preMealButtonBackground, onBackgroundColor: .carbsColor, offBackgroundColor: .darkCarbsColor) - - private lazy var workoutButtonGroup = ButtonGroup(button: workoutButton, image: workoutButtonImage, background: workoutButtonBackground, onBackgroundColor: .workoutColor, offBackgroundColor: .darkWorkoutColor) - - private var lastOverrideContext: GlucoseRangeScheduleOverrideUserInfo.Context? - - private var lastContext: WatchContext? - - override func didAppear() { - super.didAppear() - } - - override func willActivate() { - super.willActivate() - - updateLoopHUD() - - let userActivity = NSUserActivity.forViewLoopStatus() - if #available(watchOSApplicationExtension 5.0, *) { - update(userActivity) - } else { - updateUserActivity(userActivity.activityType, userInfo: userActivity.userInfo, webpageURL: nil) - } - } - - private func updateLoopHUD() { - guard let date = lastContext?.loopLastRunDate else { - loopHUDImage.setLoopImage(.unknown) - return - } - - let loopImage: LoopImage - - switch date.timeIntervalSinceNow { - case let t where t > .minutes(-6): - loopImage = .fresh - case let t where t > .minutes(-20): - loopImage = .aging - default: - loopImage = .stale - } - - loopHUDImage.setLoopImage(loopImage) - } - - func update(with context: WatchContext?) { - lastContext = context - - if let date = context?.loopLastRunDate { - loopTimer.setDate(date) - loopTimer.setHidden(false) - loopTimer.start() - - updateLoopHUD() - } else { - loopTimer.setHidden(true) - loopHUDImage.setLoopImage(.unknown) - } - - if let glucose = context?.glucose, let unit = context?.preferredGlucoseUnit { - let formatter = NumberFormatter.glucoseFormatter(for: unit) - - if let glucoseValue = formatter.string(from: glucose.doubleValue(for: unit)) { - let trend = context?.glucoseTrend?.symbol ?? "" - glucoseLabel.setText(glucoseValue + trend) - glucoseLabel.setHidden(false) - } else { - glucoseLabel.setHidden(true) - } - - if let eventualGlucose = context?.eventualGlucose { - let glucoseValue = formatter.string(from: eventualGlucose.doubleValue(for: unit)) - eventualGlucoseLabel.setText(glucoseValue) - eventualGlucoseLabel.setHidden(false) - } else { - eventualGlucoseLabel.setHidden(true) - } - } else { - glucoseLabel.setHidden(true) - eventualGlucoseLabel.setHidden(true) - } - - let overrideContext: GlucoseRangeScheduleOverrideUserInfo.Context? - if let glucoseRangeScheduleOverride = context?.glucoseRangeScheduleOverride, glucoseRangeScheduleOverride.dateInterval.contains(Date()) - { - overrideContext = glucoseRangeScheduleOverride.context - } else { - overrideContext = nil - } - updateForOverrideContext(overrideContext) - lastOverrideContext = overrideContext - - if let configuredOverrideContexts = context?.configuredOverrideContexts { - for overrideContext in GlucoseRangeScheduleOverrideUserInfo.Context.allContexts { - let contextButtonGroup = buttonGroup(for: overrideContext) - if !configuredOverrideContexts.contains(overrideContext) { - contextButtonGroup.state = .disabled - } else if contextButtonGroup.state == .disabled { - contextButtonGroup.state = .off - } - } - } - - // TODO: Other elements - statusLabel.setHidden(true) - } - - private func updateForOverrideContext(_ context: GlucoseRangeScheduleOverrideUserInfo.Context?) { - switch context { - case .preMeal?: - preMealButtonGroup.state = .on - workoutButtonGroup.turnOff() - case .workout?: - preMealButtonGroup.turnOff() - workoutButtonGroup.state = .on - case nil: - preMealButtonGroup.turnOff() - workoutButtonGroup.turnOff() - } - } - - private func buttonGroup(for overrideContext: GlucoseRangeScheduleOverrideUserInfo.Context) -> ButtonGroup { - switch overrideContext { - case .preMeal: - return preMealButtonGroup - case .workout: - return workoutButtonGroup - } - } - - // MARK: - Menu Items - - @IBAction func addCarbs() { - presentController(withName: AddCarbsInterfaceController.className, context: nil) - } - - @IBAction func setBolus() { - presentController(withName: BolusInterfaceController.className, context: lastContext?.bolusSuggestion) - } - - @IBAction func togglePreMealMode() { - let userInfo: GlucoseRangeScheduleOverrideUserInfo? - if preMealButtonGroup.state == .on { - userInfo = nil - } else { - userInfo = GlucoseRangeScheduleOverrideUserInfo(context: .preMeal, startDate: Date(), endDate: Date(timeIntervalSinceNow: .hours(1))) - } - - updateForOverrideContext(userInfo?.context) - sendGlucoseRangeOverride(userInfo: userInfo) - } - - @IBAction func toggleWorkoutMode() { - let userInfo: GlucoseRangeScheduleOverrideUserInfo? - if workoutButtonGroup.state == .on { - userInfo = nil - } else { - userInfo = GlucoseRangeScheduleOverrideUserInfo(context: .workout, startDate: Date(), endDate: nil) - } - - updateForOverrideContext(userInfo?.context) - sendGlucoseRangeOverride(userInfo: userInfo) - } - - private var pendingMessageResponses = 0 - - private func sendGlucoseRangeOverride(userInfo: GlucoseRangeScheduleOverrideUserInfo?) { - pendingMessageResponses += 1 - do { - try WCSession.default.sendGlucoseRangeScheduleOverrideMessage(userInfo, - replyHandler: { _ in - DispatchQueue.main.async { - self.pendingMessageResponses -= 1 - if self.pendingMessageResponses == 0 { - self.updateForOverrideContext(userInfo?.context) - } - self.lastOverrideContext = userInfo?.context - } - }, - errorHandler: { error in - DispatchQueue.main.async { - self.pendingMessageResponses -= 1 - if self.pendingMessageResponses == 0 { - self.updateForOverrideContext(self.lastOverrideContext) - } - ExtensionDelegate.shared().present(error) - } - } - ) - } catch { - pendingMessageResponses -= 1 - if pendingMessageResponses == 0 { - updateForOverrideContext(lastOverrideContext) - } - presentAlert( - withTitle: NSLocalizedString("Send Failed", comment: "The title of the alert controller displayed after a glucose range override send attempt fails"), - message: NSLocalizedString("Make sure your iPhone is nearby and try again", comment: "The recovery message displayed after a glucose range override send attempt fails"), - preferredStyle: .alert, - actions: [WKAlertAction.dismissAction()] - ) - } - } -} diff --git a/WatchApp Extension/ExtensionDelegate.swift b/WatchApp Extension/ExtensionDelegate.swift index 006ff237cd..52ea89d225 100644 --- a/WatchApp Extension/ExtensionDelegate.swift +++ b/WatchApp Extension/ExtensionDelegate.swift @@ -16,9 +16,13 @@ import UserNotifications final class ExtensionDelegate: NSObject, WKExtensionDelegate { + private(set) lazy var loopManager = LoopDataManager() private let log = OSLog(category: "ExtensionDelegate") + private var observers: [NSKeyValueObservation] = [] + private var notifications: [NSObjectProtocol] = [] + static func shared() -> ExtensionDelegate { return WKExtension.shared().extensionDelegate } @@ -32,15 +36,34 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate { // It seems, according to [this sample code](https://developer.apple.com/library/prerelease/content/samplecode/QuickSwitch/Listings/QuickSwitch_WatchKit_Extension_ExtensionDelegate_swift.html#//apple_ref/doc/uid/TP40016647-QuickSwitch_WatchKit_Extension_ExtensionDelegate_swift-DontLinkElementID_8) // that WCSession activation and delegation and WKWatchConnectivityRefreshBackgroundTask don't have any determinism, // and that KVO is the "recommended" way to deal with it. - session.addObserver(self, forKeyPath: #keyPath(WCSession.activationState), options: [], context: nil) - session.addObserver(self, forKeyPath: #keyPath(WCSession.hasContentPending), options: [], context: nil) + observers.append(session.observe(\WCSession.activationState) { [weak self] (session, change) in + self?.log.default("WCSession.applicationState did change to %d", session.activationState.rawValue) + + DispatchQueue.main.async { + self?.completePendingConnectivityTasksIfNeeded() + } + }) + observers.append(session.observe(\WCSession.hasContentPending) { [weak self] (session, change) in + self?.log.default("WCSession.hasContentPending did change to %d", session.hasContentPending) + + DispatchQueue.main.async { + self?.loopManager.sendDidUpdateContextNotificationIfNecessary() + self?.completePendingConnectivityTasksIfNeeded() + } + }) + + notifications.append(NotificationCenter.default.addObserver(forName: LoopDataManager.didUpdateContextNotification, object: loopManager, queue: nil) { [weak self] (_) in + DispatchQueue.main.async { + self?.loopManagerDidUpdateContext() + } + }) session.activate() } - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - DispatchQueue.main.async { - self.completePendingConnectivityTasksIfNeeded() + deinit { + for notification in notifications { + NotificationCenter.default.removeObserver(notification) } } @@ -57,7 +80,14 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate { } } + func applicationWillResignActive() { + UserDefaults.standard.startOnChartPage = (WKExtension.shared().visibleInterfaceController as? ChartHUDController) != nil + } + + // Presumably the main thread? func handle(_ backgroundTasks: Set) { + loopManager.requestGlucoseBackfillIfNecessary() + for task in backgroundTasks { switch task { case is WKApplicationRefreshBackgroundTask: @@ -97,6 +127,7 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate { private func completePendingConnectivityTasksIfNeeded() { if WCSession.default.activationState == .activated && !WCSession.default.hasContentPending { pendingConnectivityTasks.forEach { (task) in + self.log.default("Completing WKWatchConnectivityRefreshBackgroundTask %{public}@", String(describing: task)) if #available(watchOSApplicationExtension 4.0, *) { task.setTaskCompletedWithSnapshot(false) } else { @@ -111,7 +142,7 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate { if #available(watchOSApplicationExtension 5.0, *) { switch userActivity.activityType { case NSUserActivity.newCarbEntryActivityType, NSUserActivity.didAddCarbEntryOnWatchActivityType: - if let statusController = WKExtension.shared().visibleInterfaceController as? StatusInterfaceController { + if let statusController = WKExtension.shared().visibleInterfaceController as? HUDInterfaceController { statusController.addCarbs() } default: @@ -120,60 +151,45 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate { } } - // Main queue only - private(set) var lastContext: WatchContext? { - didSet { - WKExtension.shared().rootUpdatableInterfaceController?.update(with: lastContext) + private func updateContext(_ data: [String: Any]) { + guard let context = WatchContext(rawValue: data) else { + log.error("Could not decode WatchContext: %{public}@", data) + return + } - if WKExtension.shared().applicationState != .active { - WKExtension.shared().scheduleSnapshotRefresh(withPreferredDate: Date(), userInfo: nil) { (error) in - if let error = error { - self.log.error("scheduleSnapshotRefresh error: %{public}@", String(describing: error)) - } - } - } + if context.preferredGlucoseUnit == nil { + let type = HKQuantityType.quantityType(forIdentifier: .bloodGlucose)! + loopManager.healthStore.preferredUnits(for: [type]) { (units, error) in + context.preferredGlucoseUnit = units[type] - // Update complication data if needed - let server = CLKComplicationServer.sharedInstance() - for complication in server.activeComplications ?? [] { - // In watchOS 2, we forced a timeline reload every 8 hours because attempting to extend it indefinitely seemed to lead to the complication "freezing". - if UserDefaults.standard.complicationDataLastRefreshed.timeIntervalSinceNow < TimeInterval(hours: -8) { - UserDefaults.standard.complicationDataLastRefreshed = Date() - log.default("Reloading complication timeline") - server.reloadTimeline(for: complication) - } else { - log.default("Extending complication timeline") - // TODO: Switch this back to extendTimeline if things are working correctly. - // Time Travel appears to be disabled by default in watchOS 3 anyway - server.reloadTimeline(for: complication) + DispatchQueue.main.async { + self.loopManager.updateContext(context) } } + } else { + DispatchQueue.main.async { + self.loopManager.updateContext(context) + } } } - private lazy var healthStore = HKHealthStore() + private func loopManagerDidUpdateContext() { + dispatchPrecondition(condition: .onQueue(.main)) - fileprivate func updateContext(_ data: [String: Any]) { - if let context = WatchContext(rawValue: data as WatchContext.RawValue) { - if context.preferredGlucoseUnit == nil { - let type = HKQuantityType.quantityType(forIdentifier: .bloodGlucose)! - healthStore.preferredUnits(for: [type]) { (units, error) in - context.preferredGlucoseUnit = units[type] - - DispatchQueue.main.async { - if self.lastContext == nil || context.shouldReplace(self.lastContext!) { - self.lastContext = context - } - } - } - } else { - DispatchQueue.main.async { - if self.lastContext == nil || context.shouldReplace(self.lastContext!) { - self.lastContext = context - } + if WKExtension.shared().applicationState != .active { + WKExtension.shared().scheduleSnapshotRefresh(withPreferredDate: Date(), userInfo: nil) { (error) in + if let error = error { + self.log.error("scheduleSnapshotRefresh error: %{public}@", String(describing: error)) } } } + + // Update complication data if needed + let server = CLKComplicationServer.sharedInstance() + for complication in server.activeComplications ?? [] { + log.default("Reloading complication timeline") + server.reloadTimeline(for: complication) + } } } @@ -190,11 +206,26 @@ extension ExtensionDelegate: WCSessionDelegate { updateContext(applicationContext) } + // This method is called on a background thread of your app func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) { - log.default("didReceiveUserInfo") - // WatchContext is the only userInfo type without a "name" key. This isn't a great heuristic. - if !(userInfo["name"] is String) { + let name = userInfo["name"] as? String ?? "WatchContext" + + log.default("didReceiveUserInfo: %{public}@", name) + + switch name { + case LoopSettingsUserInfo.name: + if let settings = LoopSettingsUserInfo(rawValue: userInfo)?.settings { + DispatchQueue.main.async { + self.loopManager.settings = settings + } + } else { + log.error("Could not decode LoopSettingsUserInfo: %{public}@", userInfo) + } + case "WatchContext": + // WatchContext is the only userInfo type without a "name" key. This isn't a great heuristic. updateContext(userInfo) + default: + break } } } @@ -213,6 +244,8 @@ extension ExtensionDelegate { /// /// - parameter error: The error whose contents to display func present(_ error: Error) { + dispatchPrecondition(condition: .onQueue(.main)) + WKExtension.shared().rootInterfaceController?.presentAlert(withTitle: error.localizedDescription, message: (error as NSError).localizedRecoverySuggestion ?? (error as NSError).localizedFailureReason, preferredStyle: .alert, actions: [WKAlertAction.dismissAction()]) } } @@ -222,8 +255,4 @@ fileprivate extension WKExtension { var extensionDelegate: ExtensionDelegate! { return delegate as? ExtensionDelegate } - - var rootUpdatableInterfaceController: ContextUpdatable? { - return rootInterfaceController as? ContextUpdatable - } } diff --git a/WatchApp Extension/Extensions/Date.swift b/WatchApp Extension/Extensions/Date.swift new file mode 100644 index 0000000000..9af6427d7c --- /dev/null +++ b/WatchApp Extension/Extensions/Date.swift @@ -0,0 +1,20 @@ +// +// Date.swift +// WatchApp Extension +// +// Created by Bharat Mediratta on 6/26/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation + + +extension Date { + static var earliestGlucoseCutoff: Date { + return Date(timeIntervalSinceNow: .hours(-3)) + } + + static var staleGlucoseCutoff: Date { + return Date(timeIntervalSinceNow: .minutes(-5)) + } +} diff --git a/WatchApp Extension/Extensions/GlucoseStore.swift b/WatchApp Extension/Extensions/GlucoseStore.swift new file mode 100644 index 0000000000..f866595ea0 --- /dev/null +++ b/WatchApp Extension/Extensions/GlucoseStore.swift @@ -0,0 +1,15 @@ +// +// GlucoseStore.swift +// WatchApp Extension +// +// Created by Bharat Mediratta on 6/26/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit +import WatchConnectivity + +extension GlucoseStore { + +} diff --git a/WatchApp Extension/Extensions/NSUserDefaults+WatchApp.swift b/WatchApp Extension/Extensions/NSUserDefaults+WatchApp.swift new file mode 100644 index 0000000000..012d29b1f2 --- /dev/null +++ b/WatchApp Extension/Extensions/NSUserDefaults+WatchApp.swift @@ -0,0 +1,25 @@ +// +// NSUserDefaults.swift +// Naterade +// +// Created by Nathan Racklyeft on 3/29/16. +// Copyright © 2016 Nathan Racklyeft. All rights reserved. +// + +import Foundation + + +extension UserDefaults { + private enum Key: String { + case StartOnChartPage = "com.loudnate.Naterade.StartOnChartPage" + } + + var startOnChartPage: Bool { + get { + return object(forKey: Key.StartOnChartPage.rawValue) as? Bool ?? false + } + set { + set(newValue, forKey: Key.StartOnChartPage.rawValue) + } + } +} diff --git a/WatchApp Extension/Extensions/NSUserDefaults.swift b/WatchApp Extension/Extensions/NSUserDefaults.swift deleted file mode 100644 index b45ba55feb..0000000000 --- a/WatchApp Extension/Extensions/NSUserDefaults.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// NSUserDefaults.swift -// Naterade -// -// Created by Nathan Racklyeft on 3/29/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import Foundation - - -extension UserDefaults { - private enum Key: String { - case ComplicationDataLastRefreshed = "com.loudnate.Naterade.ComplicationDataLastRefreshed" - } - - var complicationDataLastRefreshed: Date { - get { - return object(forKey: Key.ComplicationDataLastRefreshed.rawValue) as? Date ?? Date.distantPast - } - set { - set(newValue, forKey: Key.ComplicationDataLastRefreshed.rawValue) - } - } -} diff --git a/WatchApp Extension/Extensions/UIColor.swift b/WatchApp Extension/Extensions/UIColor.swift index 4f9d036a63..02328e78ec 100644 --- a/WatchApp Extension/Extensions/UIColor.swift +++ b/WatchApp Extension/Extensions/UIColor.swift @@ -10,21 +10,32 @@ import UIKit extension UIColor { - @nonobjc static let tintColor = UIColor(red: 76 / 255, green: 217 / 255, blue: 100 / 255, alpha: 1) + static let tintColor = UIColor(red: 76 / 255, green: 217 / 255, blue: 100 / 255, alpha: 1) - @nonobjc static let carbsColor = UIColor(red: 99 / 255, green: 218 / 255, blue: 56 / 255, alpha: 1) + static let carbsColor = UIColor(red: 99 / 255, green: 218 / 255, blue: 56 / 255, alpha: 1) // Equivalent to carbsColor with alpha 0.14 on a black background - @nonobjc static let darkCarbsColor = UIColor(red: 0.07, green: 0.12, blue: 0.04, alpha: 1) + static let darkCarbsColor = UIColor(red: 0.07, green: 0.12, blue: 0.04, alpha: 1) - @nonobjc static let workoutColor = UIColor(red: 79 / 255, green: 173 / 255, blue: 248 / 255, alpha: 1) + static let glucose = UIColor(red: 79 / 255, green: 173 / 255, blue: 248 / 255, alpha: 1) + + // Equivalent to glucoseColor with alpha 0.14 on a black background + static let darkGlucose = UIColor(red: 0.02, green: 0.10, blue: 0.14, alpha: 1) + + static let workoutColor = glucose // Equivalent to workoutColor with alpha 0.14 on a black background - @nonobjc static let darkWorkoutColor = UIColor(red: 0.02, green: 0.10, blue: 0.14, alpha: 1) + static let darkWorkoutColor = darkGlucose + + static let disabledButtonColor = UIColor.gray + + static let darkDisabledButtonColor = UIColor.disabledButtonColor.withAlphaComponent(0.14) + + static let chartLabel = HIGWhiteColor() - @nonobjc static let disabledButtonColor = UIColor.gray + static let chartNowLine = HIGWhiteColor().withAlphaComponent(0.2) - @nonobjc static let darkDisabledButtonColor = UIColor.disabledButtonColor.withAlphaComponent(0.14) + static let chartPlatter = HIGWhiteColorDark() // MARK: - HIG colors // See: https://developer.apple.com/watch/human-interface-guidelines/visual-design/#color @@ -68,4 +79,13 @@ extension UIColor { private static func HIGGreenColorDark() -> UIColor { return HIGGreenColor().withAlphaComponent(0.14) } + + private static func HIGWhiteColor() -> UIColor { + return UIColor(red: 242 / 255, green: 244 / 255, blue: 1, alpha: 1) + } + + private static func HIGWhiteColorDark() -> UIColor { + // Equivalent to HIGWhiteColor().withAlphaComponent(0.14) on black + return UIColor(red: 33 / 255, green: 34 / 255, blue: 35 / 255, alpha: 1) + } } diff --git a/WatchApp Extension/Extensions/WCSession.swift b/WatchApp Extension/Extensions/WCSession.swift index 11c9114131..b135fede49 100644 --- a/WatchApp Extension/Extensions/WCSession.swift +++ b/WatchApp Extension/Extensions/WCSession.swift @@ -7,22 +7,31 @@ // import WatchConnectivity +import os.log enum MessageError: Error { - case activationError - case decodingError - case reachabilityError + case activation + case decoding + case reachability + case send(Error) } +enum WCSessionMessageResult { + case success(T) + case failure(MessageError) +} + +private let log = OSLog(category: "WCSession Extension") extension WCSession { func sendCarbEntryMessage(_ carbEntry: CarbEntryUserInfo, replyHandler: @escaping (BolusSuggestionUserInfo) -> Void, errorHandler: @escaping (Error) -> Void) throws { guard activationState == .activated else { - throw MessageError.activationError + throw MessageError.activation } guard isReachable else { + log.default("sendCarbEntryMessage: Phone is unreachable, sending as userInfo") transferUserInfo(carbEntry.rawValue) return } @@ -30,7 +39,7 @@ extension WCSession { sendMessage(carbEntry.rawValue, replyHandler: { reply in guard let suggestion = BolusSuggestionUserInfo(rawValue: reply as BolusSuggestionUserInfo.RawValue) else { - errorHandler(MessageError.decodingError) + errorHandler(MessageError.decoding) return } @@ -42,11 +51,11 @@ extension WCSession { func sendBolusMessage(_ userInfo: SetBolusUserInfo, errorHandler: @escaping (Error) -> Void) throws { guard activationState == .activated else { - throw MessageError.activationError + throw MessageError.activation } guard isReachable else { - throw MessageError.reachabilityError + throw MessageError.reachability } sendMessage(userInfo.rawValue, @@ -55,18 +64,52 @@ extension WCSession { ) } - func sendGlucoseRangeScheduleOverrideMessage(_ userInfo: GlucoseRangeScheduleOverrideUserInfo?, replyHandler: @escaping ([String: Any]) -> Void, errorHandler: @escaping (Error) -> Void) throws { + func sendSettingsUpdateMessage(_ userInfo: LoopSettingsUserInfo, completionHandler: @escaping (Error?) -> Void) throws { guard activationState == .activated else { - throw MessageError.activationError + throw MessageError.activation } guard isReachable else { - throw MessageError.reachabilityError + throw MessageError.reachability } - sendMessage(userInfo?.rawValue ?? GlucoseRangeScheduleOverrideUserInfo.clearOverride, - replyHandler: replyHandler, - errorHandler: errorHandler + sendMessage(userInfo.rawValue, replyHandler: { (reply) in + completionHandler(nil) + }, errorHandler: { (error) in + completionHandler(error) + }) + } + + func sendGlucoseBackfillRequestMessage(_ userInfo: GlucoseBackfillRequestUserInfo, completionHandler: @escaping (WCSessionMessageResult) -> Void) { + log.default("sendGlucoseBackfillRequestMessage: since %{public}@", String(describing: userInfo.startDate)) + + // Backfill is optional so we ignore any errors + guard activationState == .activated else { + log.error("sendGlucoseBackfillRequestMessage failed: not activated") + completionHandler(.failure(.activation)) + return + } + + guard isReachable else { + log.error("sendGlucoseBackfillRequestMessage failed: not reachable") + completionHandler(.failure(.reachability)) + return + } + + sendMessage(userInfo.rawValue, + replyHandler: { reply in + if let context = WatchHistoricalGlucose(rawValue: reply as WatchHistoricalGlucose.RawValue) { + log.default("sendGlucoseBackfillRequestMessage succeeded with %d samples", context.samples.count) + completionHandler(.success(context)) + } else { + log.error("sendGlucoseBackfillRequestMessage failed: could not decode reply %{public}@", reply) + completionHandler(.failure(.decoding)) + } + }, + errorHandler: { error in + log.error("sendGlucoseBackfillRequestMessage error: %{public}@", String(describing: error)) + completionHandler(.failure(.send(error))) + } ) } } diff --git a/WatchApp Extension/Managers/LoopDataManager.swift b/WatchApp Extension/Managers/LoopDataManager.swift new file mode 100644 index 0000000000..b8ac90d1f5 --- /dev/null +++ b/WatchApp Extension/Managers/LoopDataManager.swift @@ -0,0 +1,114 @@ +// +// LoopDataManager.swift +// WatchApp Extension +// +// Created by Bharat Mediratta on 6/21/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation +import HealthKit +import LoopKit +import WatchConnectivity +import os.log + +class LoopDataManager { + let glucoseStore: GlucoseStore + + var healthStore: HKHealthStore { + return glucoseStore.healthStore + } + + // Main queue only + var settings = LoopSettings() { + didSet { + UserDefaults.standard.loopSettings = settings + needsDidUpdateContextNotification = true + sendDidUpdateContextNotificationIfNecessary() + } + } + + private let log = OSLog(category: "LoopDataManager") + + // Main queue only + private(set) var activeContext: WatchContext? { + didSet { + needsDidUpdateContextNotification = true + sendDidUpdateContextNotificationIfNecessary() + } + } + + private var needsDidUpdateContextNotification: Bool = false + + /// The last attempt to backfill glucose. We use a date because the message timeout is longer + /// than our desired retry interval, so we allow multiple messages in-flight + /// Main queue only + private var lastGlucoseBackfill = Date.distantPast + + init(settings: LoopSettings = UserDefaults.standard.loopSettings ?? LoopSettings()) { + self.settings = settings + + glucoseStore = GlucoseStore( + healthStore: HKHealthStore(), + cacheStore: PersistenceController.controllerInLocalDirectory(), + cacheLength: .hours(4) + ) + } +} + +extension LoopDataManager { + static let didUpdateContextNotification = Notification.Name(rawValue: "com.loopkit.notification.ContextUpdated") +} + +extension LoopDataManager { + func updateContext(_ context: WatchContext) { + dispatchPrecondition(condition: .onQueue(.main)) + + if activeContext == nil || context.shouldReplace(activeContext!) { + activeContext = context + } + } + + func sendDidUpdateContextNotificationIfNecessary() { + dispatchPrecondition(condition: .onQueue(.main)) + + if needsDidUpdateContextNotification && !WCSession.default.hasContentPending { + needsDidUpdateContextNotification = false + NotificationCenter.default.post(name: LoopDataManager.didUpdateContextNotification, object: self) + } + } + + @discardableResult + func requestGlucoseBackfillIfNecessary() -> Bool { + dispatchPrecondition(condition: .onQueue(.main)) + + guard lastGlucoseBackfill < .staleGlucoseCutoff else { + log.default("Skipping glucose backfill request because our latest attempt was %{public}@", String(describing: lastGlucoseBackfill)) + return false + } + + let latestDate = glucoseStore.latestGlucose?.startDate ?? .earliestGlucoseCutoff + guard latestDate < .staleGlucoseCutoff else { + self.log.default("Skipping glucose backfill request because our latest sample date is %{public}@", String(describing: latestDate)) + return false + } + + lastGlucoseBackfill = Date() + let userInfo = GlucoseBackfillRequestUserInfo(startDate: latestDate) + WCSession.default.sendGlucoseBackfillRequestMessage(userInfo) { (result) in + switch result { + case .success(let context): + self.glucoseStore.addGlucose(context.samples) { _ in } + case .failure: + // Already logged + // Reset our last date to immediately retry + DispatchQueue.main.async { + self.lastGlucoseBackfill = .staleGlucoseCutoff + } + break + } + } + + return true + } +} diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift new file mode 100644 index 0000000000..26264c64d7 --- /dev/null +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -0,0 +1,377 @@ +// +// GlucoseChartScene.swift +// WatchApp Extension +// +// Created by Bharat Mediratta on 7/16/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation +import SpriteKit +import HealthKit +import LoopKit +import WatchKit +import os.log + +private enum NodePlane: Int { + case lines = 0 + case ranges + case overrideRanges + case values + case labels + + var zPosition: CGFloat { + return CGFloat(rawValue) + } +} + +private extension SKLabelNode { + static func basic(at position: CGPoint) -> SKLabelNode { + let basic = SKLabelNode(text: "--") + basic.fontSize = 12 + basic.fontName = "HelveticaNeue" + basic.fontColor = .chartLabel + basic.alpha = 0.8 + basic.verticalAlignmentMode = .top + basic.horizontalAlignmentMode = .left + basic.position = position + basic.zPosition = NodePlane.labels.zPosition + return basic + } +} + +private extension SKSpriteNode { + func move(to rect: CGRect, animated: Bool) { + if parent == nil || animated == false { + size = rect.size + position = rect.origin + } else { + run(.group([ + .move(to: rect.origin, duration: 0.25), + .resize(toWidth: rect.size.width, duration: 0.25), + .resize(toHeight: rect.size.height, duration: 0.25) + ])) + } + } +} + +private struct Scaler { + let dates: DateInterval + let glucoseMin: Double + let xScale: CGFloat + let yScale: CGFloat + + func point(_ x: Date, _ y: Double) -> CGPoint { + return CGPoint(x: CGFloat(x.timeIntervalSince(dates.start)) * xScale, y: CGFloat(y - glucoseMin) * yScale) + } + + // By default enforce a minimum height so that the range is visible + func rect(for range: GlucoseChartValueHashable, unit: HKUnit, minHeight: CGFloat = 2) -> CGRect { + let minY: Double + let maxY: Double + + if unit != .milligramsPerDeciliter { + minY = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: range.min).doubleValue(for: unit) + maxY = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: range.max).doubleValue(for: unit) + } else { + minY = range.min + maxY = range.max + } + + let a = point(max(dates.start, range.start), minY) + let b = point(min(dates.end, range.end), maxY) + let size = CGSize(width: b.x - a.x, height: max(b.y - a.y, minHeight)) + return CGRect(origin: CGPoint(x: a.x + size.width / 2, y: a.y + size.height / 2), size: size) + } +} + +private extension HKUnit { + var axisIncrement: Double { + return chartableIncrement * 25 + } + + var highWatermark: Double { + if self == .milligramsPerDeciliter { + return 150 + } else { + return 8 + } + } + + var lowWatermark: Double { + if self == .milligramsPerDeciliter { + return 50.0 + } else { + return 3.0 + } + } +} + +class GlucoseChartScene: SKScene { + let log = OSLog(category: "GlucoseChartScene") + + var unit: HKUnit? + var correctionRange: GlucoseRangeSchedule? + var historicalGlucose: [SampleValue]? { + didSet { + historicalGlucoseRange = historicalGlucose?.quantityRange + } + } + private(set) var historicalGlucoseRange: Range? + var predictedGlucose: [SampleValue]? { + didSet { + predictedGlucoseRange = predictedGlucose?.quantityRange + + if let firstNewValue = predictedGlucose?.first { + if oldValue?.first == nil || oldValue?.first!.startDate != firstNewValue.startDate { + shouldAnimatePredictionPath = true + } + } + } + } + private(set) var predictedGlucoseRange: Range? + + private func chartableGlucoseRange(from start: Date, to end: Date) -> Range { + let unit = self.unit ?? .milligramsPerDeciliter + + // Defaults + var min = unit.lowWatermark + var max = unit.highWatermark + + for correction in correctionRange?.quantityBetween(start: start, end: end) ?? [] { + min = Swift.min(min, correction.value.lowerBound.doubleValue(for: unit)) + max = Swift.max(max, correction.value.upperBound.doubleValue(for: unit)) + } + + if let override = correctionRange?.activeOverrideQuantityRange { + min = Swift.min(min, override.lowerBound.doubleValue(for: unit)) + max = Swift.max(max, override.upperBound.doubleValue(for: unit)) + } + + if let historicalGlucoseRange = historicalGlucoseRange { + min = Swift.min(min, historicalGlucoseRange.lowerBound.doubleValue(for: unit)) + max = Swift.max(max, historicalGlucoseRange.upperBound.doubleValue(for: unit)) + } + + if let predictedGlucoseRange = predictedGlucoseRange { + min = Swift.min(min, predictedGlucoseRange.lowerBound.doubleValue(for: unit)) + max = Swift.max(max, predictedGlucoseRange.upperBound.doubleValue(for: unit)) + } + + min = min.floored(to: unit.axisIncrement) + max = max.ceiled(to: unit.axisIncrement) + + let lowerBound = HKQuantity(unit: unit, doubleValue: min) + let upperBound = HKQuantity(unit: unit, doubleValue: max) + + return lowerBound.. (sprite: SKSpriteNode, created: Bool) { + var created = false + if nodes[hashValue] == nil { + nodes[hashValue] = SKSpriteNode(color: .clear, size: CGSize(width: 0, height: 0)) + addChild(nodes[hashValue]!) + created = true + } + return (sprite: nodes[hashValue]!, created: created) + } + + func setNeedsUpdate() { + dispatchPrecondition(condition: .onQueue(.main)) + needsUpdate = true + + if isPaused { + log.default("updateNodes(animated:) unpausing") + isPaused = false + } + } + + private func performUpdate(animated: Bool) { + guard let unit = unit else { + return + } + + let window = visibleDuration / 2 + let start = Date(timeIntervalSinceNow: -window) + let end = start.addingTimeInterval(visibleDuration) + let yRange = chartableGlucoseRange(from: start, to: end) + let scaler = Scaler( + dates: DateInterval(start: start, end: end), + glucoseMin: yRange.lowerBound.doubleValue(for: unit), + xScale: size.width / CGFloat(window * 2), + yScale: size.height / CGFloat(yRange.upperBound.doubleValue(for: unit) - yRange.lowerBound.doubleValue(for: unit)) + ) + + let numberFormatter = NumberFormatter.glucoseFormatter(for: unit) + minBGLabel.text = numberFormatter.string(from: yRange.lowerBound.doubleValue(for: unit)) + maxBGLabel.text = numberFormatter.string(from: yRange.upperBound.doubleValue(for: unit)) + hoursLabel.text = dateFormatter.string(from: visibleDuration) + + // Keep track of the nodes we started this pass with so we can expire obsolete nodes at the end + var inactiveNodes = nodes + + let activeOverride = correctionRange?.activeOverride + + correctionRange?.quantityBetween(start: start, end: end).forEach({ (range) in + let (sprite, created) = getSprite(forHash: range.chartHashValue) + sprite.color = UIColor.glucose.withAlphaComponent(activeOverride != nil ? 0.2 : 0.3) + sprite.zPosition = NodePlane.ranges.zPosition + sprite.move(to: scaler.rect(for: range, unit: unit), animated: !created) + inactiveNodes.removeValue(forKey: range.chartHashValue) + }) + + // Make temporary overrides visually match what we do in the Loop app. This means that we have + // one darker box which represents the duration of the override, but we have a second lighter box which + // extends to the end of the visible window. + if let range = activeOverride { + let (sprite1, created) = getSprite(forHash: range.chartHashValue) + sprite1.color = UIColor.glucose.withAlphaComponent(0.4) + sprite1.zPosition = NodePlane.overrideRanges.zPosition + sprite1.move(to: scaler.rect(for: range, unit: unit), animated: !created) + inactiveNodes.removeValue(forKey: range.chartHashValue) + + if range.end < end { + let extendedRange = GlucoseRangeSchedule.Override(context: range.context, start: range.start, end: end, value: range.value) + let (sprite2, created) = getSprite(forHash: extendedRange.chartHashValue) + sprite2.color = UIColor.glucose.withAlphaComponent(0.25) + sprite2.zPosition = NodePlane.overrideRanges.zPosition + sprite2.move(to: scaler.rect(for: extendedRange, unit: unit), animated: !created) + inactiveNodes.removeValue(forKey: extendedRange.chartHashValue) + } + } + + historicalGlucose?.filter { scaler.dates.contains($0.startDate) }.forEach { + let origin = scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) + let size = CGSize(width: 2, height: 2) + let (sprite, created) = getSprite(forHash: $0.chartHashValue) + sprite.color = .glucose + sprite.zPosition = NodePlane.values.zPosition + sprite.move(to: CGRect(origin: origin, size: size), animated: !created) + inactiveNodes.removeValue(forKey: $0.chartHashValue) + } + + predictedPathNode?.removeFromParent() + if let predictedGlucose = predictedGlucose, predictedGlucose.count > 2 { + let predictedPath = CGMutablePath() + predictedPath.addLines(between: predictedGlucose.map { + scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) + }) + + predictedPathNode = SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3])) + predictedPathNode?.zPosition = NodePlane.values.zPosition + addChild(predictedPathNode!) + + if shouldAnimatePredictionPath { + shouldAnimatePredictionPath = false + // SKShapeNode paths cannot be easily animated. Make it vanish, then fade in at the new location. + predictedPathNode!.alpha = 0 + predictedPathNode!.run(.sequence([ + .wait(forDuration: 0.25), + .fadeIn(withDuration: 0.75) + ]), + withKey: "move" + ) + } + } + + // Any inactive nodes can be safely removed + inactiveNodes.forEach { hash, node in + node.removeFromParent() + nodes.removeValue(forKey: hash) + } + } +} diff --git a/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift b/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift new file mode 100644 index 0000000000..869a1a3d93 --- /dev/null +++ b/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift @@ -0,0 +1,87 @@ +// +// GlucoseChartValueHashable.swift +// WatchApp Extension +// +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import LoopKit +import HealthKit + + +protocol GlucoseChartValueHashable { + var start: Date { get } + var end: Date { get } + var min: Double { get } + var max: Double { get } + + var chartHashValue: Int { get } +} + +extension GlucoseChartValueHashable { + var chartHashValue: Int { + var hashValue = start.timeIntervalSinceReferenceDate.hashValue + hashValue ^= end.timeIntervalSince(start).hashValue + hashValue ^= min.hashValue + if min != max { + hashValue ^= max.hashValue + } + return hashValue + } +} + + +extension SampleValue { + var start: Date { + return startDate + } + + var end: Date { + return endDate + } + + var min: Double { + return quantity.doubleValue(for: .milligramsPerDeciliter) + } + + var max: Double { + return quantity.doubleValue(for: .milligramsPerDeciliter) + } + + var chartHashValue: Int { + var hashValue = start.timeIntervalSinceReferenceDate.hashValue + hashValue ^= end.timeIntervalSince(start).hashValue + hashValue ^= min.hashValue + return hashValue + } +} + + +extension AbsoluteScheduleValue: GlucoseChartValueHashable where T == Range { + var start: Date { + return startDate + } + + var end: Date { + return endDate + } + + var min: Double { + return value.lowerBound.doubleValue(for: .milligramsPerDeciliter) + } + + var max: Double { + return value.upperBound.doubleValue(for: .milligramsPerDeciliter) + } +} + + +extension GlucoseRangeSchedule.Override: GlucoseChartValueHashable { + var min: Double { + return value.minValue + } + + var max: Double { + return value.maxValue + } +} diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.png new file mode 100644 index 0000000000000000000000000000000000000000..3db2e8a0a2da1c313a8c7a113a315df3c5fd4149 GIT binary patch literal 2658 zcmZuzc{~*A8Xh}?EF)WF8i~OyIii@cXJ3bF$tmk#FpHUyLCJn(-%@rJDwRD;$a2QKq?`Wlwb@5j1owo zy3k+*iu89UfB4bEQryTm5*0@zfRFsTxDx%TXb9ve(4XT+o_Nxqfe4fztQh2u~~-=7VRclP&;H#l9rI$|zwtaVQwl70znNn=ha#)few?}RoQ3Y8c? zdo*1HpnNu1--pL6S_exiI*_3X>ZlrcViDw*Gq^E9gIL zze*hlr#6ZYKdI(FQQ}})?i0;++-+dkNu_X*b3DG(Q=aN0p3rJq`$Z$QY;#rPG<%v=u#9%)g1CRy=T)y(>rhiE;Ao*doOyJ0{#@m z-gJqrfR*dfEesy|6#sgy=I(;-*4gv{QdeJXd|CyCg*1% z5#|22EgV&JPQPt1L^2UnU%KcY^F<^)$fnZ-4IQ#sOmLs}8zMnU~ShR1pnzgVNg!XHC^>H zy7FSoukcalR?P4k#Vz1a;b-jQUH@xHtFM0MggzfnhaOXCnVG_Y7mNEX+f@Fn6LN{G zCZWIMM>`_t=^W{Sb);E!_FfOy(}udr@RL7z3KsJ4u+#lPvn+)T^DK_o zUfUQ8tx8i=Hz4@5hqBs_OuE+1u4=QH(9^axFkjCBbc_Tpw_ImSEOd%7Ugs2gnxwPf)l(C0=$YtH(<1YD8llYQ~~yNVe>vq{3kF zB6Tq-12MJMq%9yL6cJX`;!a?`c~_4k46Z^5Bca^fi|&u_6E>kB9d3O6c^z#@1=W@U z`ZGf}d!C*46ygZ;Ub=es+0HlX<2G}0V%;k5t_un#SBSOT9&v7Vtnd#K7K=Y4V~tS; z>UK6m3LEX}zmzRmpjDq=*}!*7vYnb7P(>? zRT!mae|<0Rfia)d;oDiAg`Q!0bk6ePwqZ!=_RDH%{&yAaQHHM?ywjS3{LC-eO!Y!f zt#B95y{q-fy2%qw5|22EUF3LOrrSO;M}mII-^NLi+u9}*J^Q)Zno#kW z%Io#L7o)!DQAI4Ktq=WPj<(4(ESB?W>6eNE^bUk zK1ug`F@w{q!joSP{p-OsVT)}8*ZKO%`43SyHrA^X&t=t1K1Uw{e_aSH!s=ha9Yk~m zSvyY5Vd*T#t#UPtKM1COmLZE+Uc&foZH<)AHzDxnZUkF#YPA=VB~E*g{g@}C#0^OO z*`_T0=Lt`nj-iIW#@9QG-sE^$O^wqeTO&8M>K}1gNj5A>T*+@(@}cl7=c!134DP%T zn=gM1y)Wgz{h8wMG!d5kB)KiNcZe$yxPS1xyu;b?w|$ zYyib)^_JwVxu(7 zVhQj#$H}4E=NfYPJwYUu2qTOviREbSZ?&#NSuot0!t|ekb4rDmdY?SX2$%sycyIWv z4&U>T&sjc^wa1EBxfASIU9K5DKH}E0<7xUzcvMnnMQg;$iA+$I4C@b=tY}`d4dIVQ78^!~^xaF!DcRHCQ zC6mAuon$VQK@Y3J(fS`~%NHx6ZJdJo->rcarC(>%%%5b3uhDs(3;F!YMo4n-?qP_DgZg@W7=9^oX`%@~KoQG*n zzh8Vfw&Gl~)8L|j?e*&4&3W2HtX_MYu7pkN);82j$>VY|#T{^?mi*4&-hA^s&%07E zd+L^xW1*2(Q`>gLMOfw%i)>}ln4HO(*clTXU1Tu`p#%?~Wy3vA4PKSa$5SPjpD(Q3 zn6yX5O^JrdT!B62%alzVe00L#X$ZGb!auj)znb&cQ!7rH+n~C H=kWgk7Duwp literal 0 HcmV?d00001 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.png new file mode 100644 index 0000000000000000000000000000000000000000..63f7579ca6ca6d0617ce634afbe5061e204c8a16 GIT binary patch literal 2687 zcma)8XH*l&7EVN@gGyb5C4{mfl28JnlhBJn%<>c|fk3xGYsc zDPci_h)O8ZyMU;mf`mxR0(n8*-PeBXoO9>S-1%<#?wmPiQtWIk1b8KR0RVu2m8B_~ zt?dql`xtvpJF!vARzM=!!WdB3FSWwn@B~?2CISF_!iNF`z`PwFDS{Rn?*D z>MCr63NZ{v!je^RM7gg){)l7hNpvUp29bR6IM88S>=k?n$p8#KO!VjYR%c+)pNVk9 zZ>-q-pk!P;V3R`Al&$`rRTk~z;!jBWm`1q`QN3H?@6kMDNk!GjCl&+Vsg^s=~%x*O@J+MSP?!J7HU5&gpqS$=mC;>-AGYjfvZgjp>#_QjK^*bVaw{yRf2hgqD-+%m_y!Mh>!_T1UHj zxq4#Iu*K*@6#t@RB$y{17>ePrXt-&tv3SL<_p^F?SSC$^TALpGVIRPw_qGt8|G0$# z2*qy^D1`S^6Wp6J_;J`t+=C5-FR3~uL{rlAGvF%5zIeI2mVxGxKP`ozMb2t2Lz)dE zoIGn-7hLTG{=wtpdfNY5w~5fZ(05Uw^mq<#kgd^73~#6H%XC3Sa-Vhn-SD3>&4&}Z zIG;SPB3sI6?r2~PMdnrKclr^ ziqsEHyQ5+F;hW_lrvwpO?ml^ssV>j1^>*J=8DNg)c`YBE2iB-+75dl*I-2=}vRESt z6}L=pHEU&0?+$H(qc&xh0#fc9MO0YOCbRe>eyk#lA|=JeFxonEHb0Dwoe<oqaGl?R~=>aNUNqk(JbM?(w4Maai!<6X|Yy&D}S$*L-o);=x)x>Wu= zht6<+m6y2p%0CS#k*5337DxS1(CHoR*n+Gi)cZdeQY(CwoW%@W_jz^e;Xd&P-zPV8 z_a(!(d#+EioIlMos{GD z)!XFa>@q{yDVPq$ z+;h2kp!*cp$xqVz9m?6+!J&%Sl7neix90O1Vfwa>-kspKi=I{CY8)N~R`JQ03&@MZ zLSl8pq?9}DTV$Pz{k)B0FMgyI;<16x)Thi$1>)#5mFnuT z+|^PlME|3S4?SCaD^TZkg-zH|-v&%Gt62&} zo1M7NLrve5J;~_2)MUOeE3a(4M=a%Fig&W;JDCOD%pKpI;ZthNLS=f|rQB^l1J~Fkr&%U$WrnagGN=E4B~MP6wZVBYSuyHJI2PVnfEZ75+cs%o072=|UDTHhL-*nPjkI*#HSUop;&Uc$e)M_awcF;N#=eC>Nbz%}N= zK(qFWj5{&lXGLe~Wm3e$#qFrjf%Vd)>t#-r_q(zqKF@77>%E(E$t@0@t0ZrsPMjZ# zT-j>|!l4xu4$-H2)1oVcaWf&e)TSU$wM@&$Z`T)F9jStn1@4T_M`CD6j9zbQ8T>q5 z;SH{fR&2Bn`)&}ae%)UAQ?UP=Q65wW-l|rxnGQUXSw9qM5-Wy^b(Vu+-KX|4C!9}2 z_r*4n_S+jm8ak96g1GoaqD3ABn*I)Vg6y019`t`4so8mRQ-{KkIyY2hWXIsESK zb2MtUAb;Bn#q=@j2gZu zC>|;mJ91KFsC0$w97!s~^2I_Oc8;X8HdooyzPT4EF9JF(1XrAHtcJ#?+ieQ3)G zcqYuf^1dyjtzfYipkvrafzuwHD zB`jVN(-n*vSglK5+qj|m0P4!u89G`9zRi95$o1I2rxMQ3cV;n;Lh@sZHV%JAyd9=gzq|bK|q~=lOq1`&L%0oi@Mv`P{v-w+=ttpqjf)wyd#1(16M90EYyp1fy~S zOAnAM1Q7soRUiUPZeSH)d9VsF7p5VwdG_pU@;3E93(vp%zm@HDcku1<%X1RfRlQ8l z-+$Kht9oi!_sq&w32PEzdOd*F{hj^nO;hjL__{p;v;AJayL#}`BP|21Ui^OT;I?Ikxy=!ly^yDtnww>%V&&t~+AU)9xG;wOMBE z3|qUS7KRaVF$bAuyk~8z%gH{@UjS7CVRMA6mGU zFX$lDM%sMq)BvOTkZS@rt3J!eqP*ZKpdZ;f>mwESkiWzB{r=|Jb%uOcV?e{+Xt z^VD@W|8CxXADH|vcq_oD(S#mZXY1ctNm(#|J0F51Wd N1fH&bF6*2Ung9aC$x8qL literal 0 HcmV?d00001 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/1-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/1-hour-graph-42mm.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f39f99050edebaae4533cafaf559ae81ea56e5 GIT binary patch literal 1570 zcmbW1X)qfI6vt!RI;w8#iinnKDI4uX(%NEFEM_aHb=8@YT7oQskf6)douJj2qHM8j zsiHA-NhoO;RaDZ}5u`yYsUxl>iEDLdI@8^0zwC$q`_FrC-kUe?!!O$v>7Z~_=O_RG zP;hcYTsd(1N6AVb#P_2_^8=In#nC4g08m``D5>bdU+)|gwQ=^|ICt!ITmmWv4X_Qt zUPJ3UMWRB{SJ0@C#OQwXMF8MPqZ7jRC;Y_XR2LTL0eHG9k1*9gX&`^XeN?*nxNF|I zs-8gCEl8G+NvkDB!!O%v8GuQDc9mHuI@HsfK|O1!Dl6x9cdIBAPcKh(Wz7w3i5+X9 zt!nn8!YO(}rys0_9VQjImf>-OZ@ZzNq->R?hfu1N8A<;d)Xv zt?8EIqG7gZm(0uKbIzrtwukKOT{P{=d?$~&yy2{5P3D5l62}8kVgpmK3whJ!p~VP% zs`1R>{aEkmSZ5M{^pdQ&e&)lQ^x=8h9H)t>8USx@jjEu+M{6}HN^HZ;hQ(EeGJ*Ty z;L<*N@?>tR2E~S&36>4mSkT)$uNd4DqfJrjnYY6*S85b^ec5#jyd}ZEe!{S{2k++v zOWesT6%jD=&3h{-b;=fd-sYwf$jkik5JA#yDw>NryI)@La>pv!E!lqn$sGBu)EXql z%tDi00xj~iL%aJrRCccT?MedtkZeeVU2v69)2n!S$gievZGIqWuRd&X#VDY?Gy~Eh zxWCRaj2EEk0#5=};kaV5#HgpTks!LQj=3WeO{ei(bU)}88}&eR6?O}&>ic__# zH)t2qd4mmD@0-lqT>b`m|kRh%E=@$ZRv~wYTf@E^Mp$0f*a>LW40jH$+7D z0`=7zbWgDuTeE{@tj0s}XUc6(gN&sG%_8CXdA|6yEkVM8dasQwmo;*H&_e}7dY}-c zNT#rYtSrVT?~*`AW?mXjdkVLq8{)^fzO=fIeE;jSn{}((_M5^wc9k!cWDL|p;bW_K z6)&<0@$r1yiS0h`*_G0vx}%fwex3DXKe{ATJuHUMKIcm;u`c)2b(6&9+Dy@40sF7AvO9cl z(8>+^qmm#&wRmyS`$Wqm(+N(O8U<^jmp`h@WH&HcR-aXQOahQ)uy2z=J>a24;w^uZ zv%^^C>+c74XPCJQIZ#bwRsrUWLaHLmr;@jrmBwFawav4uu8)8+aL=-0GYG$D$Jbta zyfgfO9ibIRF&dgis3ljCwLBb1^pk-T{Q;WA4k$akYG+{E(!#4od6-w(Vy}B+W*F8Q zBAHCCA1v?ecevpe_JsPpFbD=dU7<*d&2$oM=am1TCubQpJ^(L(PFK(|x-JdYgPl_7rgH1hFp1;ECM<-vLhcNW`N{L1}*jdA|vY literal 0 HcmV?d00001 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json new file mode 100644 index 0000000000..e835ece7e8 --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "watch", + "filename" : "1-hour-graph-38mm.png", + "screen-width" : "<=145", + "scale" : "2x" + }, + { + "idiom" : "watch", + "filename" : "1-hour-graph-42mm.png", + "screen-width" : ">145", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.png new file mode 100644 index 0000000000000000000000000000000000000000..6f60342c722470b31762a4ea2d6a01da940e2121 GIT binary patch literal 2969 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 MM-!n@tZp0dUz9&!xc~qF literal 0 HcmV?d00001 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.png new file mode 100644 index 0000000000000000000000000000000000000000..a09880e901a1aab3fe182595664e2380514a3de4 GIT binary patch literal 3097 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 literal 0 HcmV?d00001 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/2-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/2-hour-graph-38mm.png new file mode 100644 index 0000000000000000000000000000000000000000..f4ff6ee7302c9d30094afaab813feaa34845af87 GIT binary patch literal 1430 zcmeAS@N?(olHy`uVBq!ia0vp^Js`}%1|-)VaI^zboCO|{#S9GG!XV7ZFl&wkP*AeO zHKHUqKdq!Zu_%?nF(p4KRlzeiF+DXXH8G{K@MNkD0|TqFr;B4q#jQ7U?K@;bB@W!5 zxBO$*1a2`iMa!M1Oe}+!TyQX((<|hW6U1>aH;GBJ$amXWHkOC0RyWO!o?)+asH-=D zX}Z(pW0y{eGPCK}7}lF^_-Xe}x?<*@o3rn}dHbhwzkJP}vv=N{G5UP_eeLIGZC1P);iW`u-*;bzyj75i#8AATs`-+T4xp}yFOlFyWu z@2fhVYj!Q)D|F(Sm=y+B;MPLTznnPDdBe$?-_ItQa0_{_w7+}tMnvqp;wehvZYq|F zXEQtbRsT)9Asg{_W!swH4!72;TuYjkt`-zHOX&NPr>2vSrkeYm^RJC-eXUuQjc@=w$H1N*u(~%HSRWv_jtqc=VyIqxN6;`9f_w){yGc!e|dXv zS8>?;KRO%b6JMSFs<$Pf&-Ykl}xQ-{% zZtCQHLX%u4KFz-LW+T%L_oX{EZpS&y_;r!%iq3z}KN?OY+p9L8EID;=^1Wl6Ei2{S znNG6_J^z&Fu;iuZW&V_z!5qQ+lfr*q&h6W-az{GZ=V@8OO~@G4|_9kc+PjY#6v{2QD-KiFwpJwix`YfVNr|SAI zrz{15#AOl6F9d3hzB;`BW9F@MT3NR!_f)g-+oNCv3La&wc&-+=M)htVl1#9zXpFlfO>9E#rEX=hDowXCL+EinpIAa{YF>>#x%S z5!dy;KCeV4Ru-LD6l`&O<29Y7B56%C)9)Gee>V0Hb33Kj?YG}%aypm#p%9(V5>+#+ z55%oj@Q#$!Y}fg9dD7++L5hiPXKc<*K3f0R38eaS>cp??E6-o}FztVGPGfzKTt~yQq>ltzFDZ8iDUfUD3`Y|ta#tY%S(%*V!ty_D2^Dmx6 zuC#myvAsFhSLFeK4y^q6guxKe9 yW;n#4z=ME5E&>u1R^UNW4KZj+Lpl==!@aL_uNe71KMpKY89ZJ6T-G@yGywqQ)_e^B literal 0 HcmV?d00001 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/2-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/2-hour-graph-42mm.png new file mode 100644 index 0000000000000000000000000000000000000000..c43ed36fab4efc7196b5bac01fb19d147e930b30 GIT binary patch literal 1941 zcmb7_X*e6`7RO^cYN{1WEmOOoLT$MhgW4-%4N|mJXhKj$TKh6digt!-RK{M6ky@%% zI?-AhOYK|HSgNs3kxDDpkdi1@@4a*H%sl-vAI>??^LyX-|DJEZR2Mi*NKjf3000Qt z*;*qGC-rlEbM&yAW~>Dtj$_wsJtF`BVCU!J%RU2?IfSPop%)|1V*?^%P~jMWr9T#p zQM3z11!52wlz-gyK8y(fAONznwseb~WX+i05qFn;v0=mqg}n6=oEMdOyc)vaE-H51 zZ(btjbRkhl#T#1oR@cKoHnoT!Hugie^1iy>P1A#M7}^T+ru$newcS=EqNS%Lu@My* zvzm_N3@;TlIY#fV1aNqGb^5mosV+FU!WKG8T_aJ z1^<5dCrrtd`!fGu)*r6_`-?S!%q6TQx8Uf{jBRJQzQ8Bgs$9~MIHjyhB~=l@ibkUL z%HkArB}uErp!HzI;ihw$0_F>A?F|-RUbQ>D8ECndZ$j`p?TF5|ubq!HO^8%m^3v?M zR-j3z!Pn`!`NE7M7vRlJESUE^gdLdYeH@dz@RvU%n$SMS5WCw>sYLC@rg^K&SaM7qK6bfdrit(6ye*< z8jfykA#kpi6_okuN^Ks9`}4i!n(}Z{rvo_q;E~c^VTTqm?jDgAq3`iYD@*vC24w_g z4*8Bi9l)CjFUtdY8|y}N9-I}`IL9#bS;@gx{MaX>9!?PLkC5O^7i)Bp&*>1~$GDk4 zzeIBw>FFiZb(k&eeU!sdJ{If( zNgA%ljp5TpF(kZxtCrA;$g8Z1dRBXTih62HUEhMhTrYOwO7+g_lpAZNRSrH=TbJ8- z_k=1DC`7^uFo|@}MWSm0`IXBc;^|<-d(43h9p=7Qm~Dnz!NGi_|7qV@+qo}Dn~;^K z+!d?Pc-O<5M_Q`#fsY2D&vxB4x5-=jexk9MtErER@pK7_+bq*& z&2bgM88~W39fKDeA^a7hX%+2@%T-*1#9jsW`eVtg4p4lP;x-aIT#o3Xrsovm-arrZ zvvSX-$YU*cx*f)P%|4KOY~K5NF6W@{6tL7=iDgDr3lB>ZMhGvxoK>Ujk!avSj(i^E zC4E$;X3hWchUaSOaorAZye#RrTiRsU(ATu>M|Tfjl6}F0EkC^zE?{oh2i1!nn{{y3 zMx7YVs|2zOZkZU`@C=C+`wUAvz7#lT@X)ZWnY*E z<@lT&Ed=|E$EqfL|0XRtv@YLn8Sc(vTiTyLzm-raI}8<`3iWUs`o1B3=TfVN$E&LC zSydx5`q`yz^>`ikDhkbJ$AoKerjDM@Z2fUFKz+j3iOA6CU!XWwtbwR{0T;67P?7qC z%jAc}=0#c`O0`|LCZ_a)Wsr5DAH|Z!tv={U4FgL$k8%;gmy=mYWx|~tn3d6~{;Ld9 zte-gnwM#O=)+ljTRN|@HLpH|uz2Uq;hhM*{j((sRaZ8-#Z9J;U{f@zkx`~ap^_16k zma2zOh<}Q8_r(VrI!9OD7*IzKtM1CkfezZnGESS&W1O963&~P*0hf5(n5o%|VVRF= zp6+TL?i6-m^!e3=ov}>4@)$C#4kG*Q;-0O9jO~(qy}OojJ1*3G3i3msR?`N8NjPv#H5vp5qOs$OpY$N{#I5 z$$-jmlAQ4QA1s4Hu=Ures+)n2!^WF@TYJBzLoy145", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.png new file mode 100644 index 0000000000000000000000000000000000000000..d76ad7aa01822c0d732cbd8f204bcc488dcac0e9 GIT binary patch literal 3043 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 MMFXvJjccL*0u895&Hw-a literal 0 HcmV?d00001 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/3-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/3-hour-graph-38mm.png new file mode 100644 index 0000000000000000000000000000000000000000..6f18861f4c01d83f5727ae61d1ad9fc19d0d129d GIT binary patch literal 1655 zcmai#dpOez7{^yiayuH@Tyhyw*eo&S5-a!nge?86&2^$RBy=50Lk|UKyuDb zcHV2A@gt-p*D|9SePGSDo^ry*fIu>eKVk!@uo$`qRbuV^V-J%;W6$8DLqOyf4H#ys(ed0%EN?fn1+e0s%5VkuynZjUGE%T=_x)Mn@HJHpa z5){({2}WewZ8N7U^IC?}Gn+U`(2qh%bgwJws=_9`wNx5ixh4IE&NBts)@Pf8f=@56 zeik_2u1X9Xj`j4ssJR?VTClPJ1k1jI0C1Nz8nva@SUF|Gx@@8A0YL9E2Lt%z+E*KiGxEiUpYvaGCQrz-iD-}P2qHfDj zBM5w^U7eZD*o0B;F(|D9k*=?&k-d0W*x%e0_<+yo6k1a^)2i%qeVCCjvX9?Fff2%f z`y?UVQ16S2I&Gm_%k3evQ=3}tmV&PPs0Su-#pg1RB4bzg?X#}OlwPegz@O&j*S#|S znA*_OW5H&+?8ShwEc6mO%}8Hw+~q`>zuyR*>r_ZA%{pJ!c{Y!W6giHKXluv4L#DIZ ze~rZDJTIXDYVaV8-ZeGvDH$fG<6Z$r+Cuw5N6OUvcjf9zKgGxOPI-4>ym*&7qw<2c z(bO?8F5hY8CLwC8&pWdbqzLZOC4G_HykMqR6t1%sPBkYUg6N`7c)nLlp(xB&M(VfOI>z;B48n&D)4nrKjXwNjgKTl*Y6Vk0k8>+SERBc7&F^l;fo+_uyoM+~EWD&h zpIX&vHGNXoB*SOx(aquPQ`5{9DF_!u#ZANuXhFs3{Byy5?G@v4Pk1Qep%?>ETQ6QZ zwejg}F2|kVPKbFu|9Fx^QXp#B=g-_tWbPWR>$ak3%gZfYu&&2aI~0Kk;G)-;)m^hq z1t_AK6WoM`)a2iJi`k3< zM;RB#KZ^XtOHeb359Gd69-2Gu?Js)1$4jpS&~l~7_8ZQ{vJxV3WA$Z>)}dPw11)}^ z_Ak7cW?3$c@CtXd!CEJZ6Dls5&hh-w=7}L1kTL#n=z9YvcIxruG?sa<79SJe19NLJ z$>e`?~b^SX!K$4#AbB$Fqe6%kH`z(EW41*rOzt&neU4HP~t9yAd`x(z|E$@XA_%#je3EYu` z`7^h%wWXcN@S;ges<@laD-1kix!b#~%i$qSyviNYxN+pcxWJmo$42;pI*!G+mYc~; zDned*v$Mk}lL_N4xp?qXVdYlE%&NPO_GZ!-@2J0Q)GMc(5`1$FheeEIsec_s)u!RN zZE8vzR4Q?A_pLm)Bs~N;$%G{2rGEsHl33R|zJ&a#@E=%8;_m`7blJEW^ejnuaPN1A R*K1D$G=Qv literal 0 HcmV?d00001 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/3-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/3-hour-graph-42mm.png new file mode 100644 index 0000000000000000000000000000000000000000..b4bd9938b596764ce20d0ff8e5e92f176b32cfb0 GIT binary patch literal 2270 zcmcImX*?8)7N0_c%ym_wQIjZQBIH?m*@nq74YJIjY&DavXpE4-Sh8ffvd&Um)702* zm@wDQeOa?L8bw4U``lMpQ{tG=;zb&Hxb zW2Hx^?9jy5OFLQ~+8ezez_iIjY6~7BhkHdI#5)C|4+Da4M4+?AjciCZ1hBa0ujsGp z{~GZ>m>vr_D<&JRXrntZ?u`M2uLCc<6wDV!?iCK<7-2l^dP9a=I^RD-jp*c+qkDT+ z7b%4_B|^WVXaL`Rw9ClIZo+ z8>CmOqI~rmNd&jeHS-)+`Nb7y^)qD{SPtGyV zYR6?IX;06vW~Ji=2bFxHug7NdJ-RI#)v;rJqPo^&4q*sA{ASf z3ivm@AA#FXBI8FLRx9%TLwp=%n9C4OQZDaa9y7w{*hKu_E;Bi8pRjJGp$_HEd3s-iKxZupPlw0mg z$8+4eR#oiT_F)cX4-&&}d=UhNfxR|4JbJ!L?!mB%6-b|Mt1!xg-NULGtlRGr8z4ta z>rmmcmmS8izjHe?y&iPL7v;mhHGW>UzesT=(V2FPhalGy#4!_|#nBk8byF`_PPT zqcwvYDM5<0t=1Rz)>WO(x=-+RQ0GZkL^zS5MT_(czT$kvtYlM2pr%?(BigpF_th zliqD31v-}zG%M;{8NS6^Mrh+>{%h~kd;;hk~P#aU+FKkFC>Bm`RCrKX>ct#4iK$FaVjO{})sS$bhxMfuRp}xyNC~uzJST9sgrEPi z%#@FtfIW$CFd^t1s_*GqF}AkoIYq6t!pbJwu4xEjV6p8~6~^&`bwpiE#NgpencbXm zta4aJl_y8J$}mG=imSo$0mZI~Yg>m%7^4~-MRPk21ed`S0etb;c&A zm(R|CAADnHn$&+KB@yd4O0u`<3aJo>z{G&gK=fw?g_b~eDrfqM_akV;SLu2o`srj5 z+1Vn8r(ZROCpvW%P8Gm5@u3}N>v}VO&%D#_vs~7rZUx~-RC7SN)2IQu=2gA~CC+g@ zrk~z-&`oyGpM3EP92ON~;XvrGb%F;+9~ie37a^1t1JP+jnVoHN(#$9&=vMoM<8ndJ zTJb<3IFo$<{BxxS`g-S(J#(*b$x&fIU&AItmflfQfZgWbT(m`mHLI1HtIdB+x>I&H zbW%^}*P~2>qQQ`b@)OrY*AJT~&#M~32pw?ag#>LR*9l>9zSO`FtQ~6{970{DgqkMu z!2P}|7ah$|4qY?oMkVV!=YP^id@XK$q$m$_tcS+A%CxMj?Q8UjPy5~!nt5WzYAv9f zo$aEF;uf#VPe{c@xspKiy@lvORU)Aam#!Qou?V7U17`iDvab6NxJm`M@)KANm-7aT z7r1s7TP;ibtd)c%to5fgw{^ttG?VXN2|~17Il_=%VU7tkbcl^!(Z}>qwN;kK0%LFk zjT|PJ6C}ihIIRz}7yvbSKEDX7+Kih+wqhoeG9{rrIE~#5>IJ;Tn3UJG7174sH^7 zu=g#NYR#=Q%``JCBPHSm06Y0VX_j$%OYk>x=_`>aQ7{c(aY1E7TtnFTFKe)Ok8GRn sX+=BvfA{~{`5)2$*CbEdkyHjK9d0i#`bSyW?r#FHhdbI-!Tj$10kV)m#{d8T literal 0 HcmV?d00001 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json new file mode 100644 index 0000000000..17343c647c --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "watch", + "filename" : "3-hour-graph-38mm.png", + "screen-width" : "<=145", + "scale" : "2x" + }, + { + "idiom" : "watch", + "filename" : "3-hour-graph-42mm.png", + "screen-width" : ">145", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/Graph menu icons/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index f2b392d588..8da55eb2df 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -1,12 +1,12 @@ - - + + - - + + @@ -117,12 +117,12 @@ - + - + @@ -149,16 +149,13 @@ - - - - @@ -260,14 +257,14 @@ - + - + @@ -332,7 +329,94 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -359,7 +443,7 @@ - + From 34c390a83a1383d0d5ef94e6cb40f5bf98df2270 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 6 Oct 2018 08:28:20 -0500 Subject: [PATCH 5/5] Watch Feature Updates (#825) * Watch Feature Updates - Update IOB and COB immediately after carb/bolus send - Adjust complication push budget to spend evenly before midnight - Adjustments for Series 4, including rounded action buttons, correct text margin insets, and using system scroll haptics - Improve efficiency of the chart redraw * Disable crown haptics * Redraw immediately --- Common/Extensions/Double.swift | 6 +- Loop/Managers/DeviceDataManager.swift | 2 + Loop/Managers/WatchDataManager.swift | 143 +++++++++++++----- .../Base.lproj/Localizable.strings | 3 + .../Controllers/ActionHUDController.swift | 12 +- .../AddCarbsInterfaceController.swift | 23 +-- .../BolusInterfaceController.swift | 24 ++- .../Controllers/ChartHUDController.swift | 20 ++- .../Controllers/HUDInterfaceController.swift | 14 +- WatchApp Extension/Extensions/WCSession.swift | 10 +- .../Managers/LoopDataManager.swift | 12 ++ .../Scenes/GlucoseChartScene.swift | 50 ++++-- .../AppIcon.appiconset/Contents.json | 23 ++- .../1-hour-graph.imageset/Contents.json | 10 ++ .../2-hour-graph.imageset/Contents.json | 10 ++ .../3-hour-graph.imageset/Contents.json | 10 ++ .../bolus.imageset/Contents.json | 10 ++ .../carbs.imageset/Contents.json | 10 ++ .../loop/loop_aging.imageset/Contents.json | 16 +- .../loop/loop_fresh.imageset/Contents.json | 16 +- .../loop/loop_stale.imageset/Contents.json | 16 +- .../loop/loop_unknown.imageset/Contents.json | 16 +- .../pre-meal.imageset/Contents.json | 10 ++ .../workout.imageset/Contents.json | 10 ++ WatchApp/Base.lproj/Interface.storyboard | 73 ++++++--- 25 files changed, 398 insertions(+), 151 deletions(-) diff --git a/Common/Extensions/Double.swift b/Common/Extensions/Double.swift index 2247409097..3fbc5bdc9f 100644 --- a/Common/Extensions/Double.swift +++ b/Common/Extensions/Double.swift @@ -8,8 +8,8 @@ import Foundation -extension Double { - func floored(to increment: Double) -> Double { +extension FloatingPoint { + func floored(to increment: Self) -> Self { if increment == 0 { return self } @@ -17,7 +17,7 @@ extension Double { return floor(self / increment) * increment } - func ceiled(to increment: Double) -> Double { + func ceiled(to increment: Self) -> Self { if increment == 0 { return self } diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 95c3c28647..8f7b774343 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -370,6 +370,8 @@ extension DeviceDataManager: CustomDebugStringConvertible { "", pumpManager != nil ? String(reflecting: pumpManager!) : "pumpManager: nil", "", + String(reflecting: watchManager!), + "", String(reflecting: statusExtensionManager!), ].joined(separator: "\n") } diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index e7c055e7dd..6f3fc32866 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -42,33 +42,18 @@ final class WatchDataManager: NSObject { @objc private func updateWatch(_ notification: Notification) { guard let rawUpdateContext = notification.userInfo?[LoopDataManager.LoopUpdateContextKey] as? LoopDataManager.LoopUpdateContext.RawValue, - let updateContext = LoopDataManager.LoopUpdateContext(rawValue: rawUpdateContext), - let session = watchSession + let updateContext = LoopDataManager.LoopUpdateContext(rawValue: rawUpdateContext) else { return } switch updateContext { - case .glucose: - break - case .tempBasal: - break + case .glucose, .tempBasal: + sendWatchContextIfNeeded() case .preferences: sendSettingsIfNeeded() - return default: - return - } - - switch session.activationState { - case .notActivated, .inactive: - session.activate() - case .activated: - createWatchContext { (context) in - if let context = context { - self.sendWatchContext(context) - } - } + break } } @@ -98,6 +83,17 @@ final class WatchDataManager: NSObject { log.default("Transferring LoopSettingsUserInfo") session.transferUserInfo(LoopSettingsUserInfo(settings: settings).rawValue) + } + + private func sendWatchContextIfNeeded() { + guard let session = watchSession, session.isPaired, session.isWatchAppInstalled else { + return + } + + guard case .activated = session.activationState else { + session.activate() + return + } createWatchContext { (context) in if let context = context { @@ -107,32 +103,39 @@ final class WatchDataManager: NSObject { } private func sendWatchContext(_ context: WatchContext) { - if let session = watchSession, session.isPaired && session.isWatchAppInstalled { - let complicationShouldUpdate: Bool + guard let session = watchSession, session.isPaired, session.isWatchAppInstalled else { + return + } + + guard case .activated = session.activationState else { + session.activate() + return + } - 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).minutes >= 30 - let enoughTrendDrift = abs(newGlucose.doubleValue(for: minTrendUnit) - lastGlucose.doubleValue(for: minTrendUnit)) >= minTrendDrift + let complicationShouldUpdate: Bool - complicationShouldUpdate = enoughTimePassed || enoughTrendDrift - } else { - complicationShouldUpdate = true - } + 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) >= session.complicationUserInfoTransferInterval + let enoughTrendDrift = abs(newGlucose.doubleValue(for: minTrendUnit) - lastGlucose.doubleValue(for: minTrendUnit)) >= minTrendDrift - if session.isComplicationEnabled && complicationShouldUpdate { - log.default("transferCurrentComplicationUserInfo") - session.transferCurrentComplicationUserInfo(context.rawValue) - lastComplicationContext = context - } else { - do { - log.default("updateApplicationContext") - try session.updateApplicationContext(context.rawValue) - } catch let error { - log.error(error) - } + complicationShouldUpdate = enoughTimePassed || enoughTrendDrift + } else { + complicationShouldUpdate = true + } + + if session.isComplicationEnabled && complicationShouldUpdate { + log.default("transferCurrentComplicationUserInfo") + session.transferCurrentComplicationUserInfo(context.rawValue) + lastComplicationContext = context + } else { + do { + log.default("updateApplicationContext") + try session.updateApplicationContext(context.rawValue) + } catch let error { + log.error(error) } } } @@ -266,6 +269,7 @@ extension WatchDataManager: WCSessionDelegate { log.error(error) } else { sendSettingsIfNeeded() + sendWatchContextIfNeeded() } case .inactive, .notActivated: break @@ -292,6 +296,7 @@ extension WatchDataManager: WCSessionDelegate { } func sessionDidDeactivate(_ session: WCSession) { + lastSentSettings = nil watchSession = WCSession.default watchSession?.delegate = self watchSession?.activate() @@ -301,3 +306,57 @@ extension WatchDataManager: WCSessionDelegate { sendSettingsIfNeeded() } } + + +extension WatchDataManager { + override var debugDescription: String { + var items = [ + "## WatchDataManager", + "lastSentSettings: \(String(describing: lastSentSettings))", + "lastComplicationContext: \(String(describing: lastComplicationContext))", + ] + + if let session = watchSession { + items.append(String(reflecting: session)) + } else { + items.append(contentsOf: [ + "watchSession: nil" + ]) + } + + return items.joined(separator: "\n") + } +} + + +extension WCSession { + open override var debugDescription: String { + return [ + "\(self)", + "* hasContentPending: \(hasContentPending)", + "* isComplicationEnabled: \(isComplicationEnabled)", + "* isPaired: \(isPaired)", + "* isReachable: \(isReachable)", + "* isWatchAppInstalled: \(isWatchAppInstalled)", + "* outstandingFileTransfers: \(outstandingFileTransfers)", + "* outstandingUserInfoTransfers: \(outstandingUserInfoTransfers)", + "* receivedApplicationContext: \(receivedApplicationContext)", + "* remainingComplicationUserInfoTransfers: \(remainingComplicationUserInfoTransfers)", + "* complicationUserInfoTransferInterval: \(round(complicationUserInfoTransferInterval.minutes)) min", + "* watchDirectoryURL: \(watchDirectoryURL?.absoluteString ?? "nil")", + ].joined(separator: "\n") + } + + fileprivate var complicationUserInfoTransferInterval: TimeInterval { + let now = Date() + let timeUntilMidnight: TimeInterval + + if let midnight = Calendar.current.nextDate(after: now, matching: DateComponents(hour: 0), matchingPolicy: .nextTime) { + timeUntilMidnight = midnight.timeIntervalSince(now) + } else { + timeUntilMidnight = .hours(24) + } + + return timeUntilMidnight / Double(remainingComplicationUserInfoTransfers + 1) + } +} diff --git a/WatchApp Extension/Base.lproj/Localizable.strings b/WatchApp Extension/Base.lproj/Localizable.strings index eec161a5e6..b26c9f6d64 100644 --- a/WatchApp Extension/Base.lproj/Localizable.strings +++ b/WatchApp Extension/Base.lproj/Localizable.strings @@ -1,3 +1,6 @@ +/* Title of the user activity for adding carbs */ +"Add Carb Entry" = "Add Carb Entry"; + /* The title of the alert controller displayed after a bolus attempt fails */ "Bolus Failed" = "Bolus Failed"; diff --git a/WatchApp Extension/Controllers/ActionHUDController.swift b/WatchApp Extension/Controllers/ActionHUDController.swift index e5bfd49e68..71eef5a0f7 100644 --- a/WatchApp Extension/Controllers/ActionHUDController.swift +++ b/WatchApp Extension/Controllers/ActionHUDController.swift @@ -36,7 +36,7 @@ final class ActionHUDController: HUDInterfaceController { override func update() { super.update() - let schedule = loopManager?.settings.glucoseTargetRangeSchedule + let schedule = loopManager.settings.glucoseTargetRangeSchedule let activeOverrideContext: GlucoseRangeSchedule.Override.Context? if let glucoseRangeScheduleOverride = schedule?.override, glucoseRangeScheduleOverride.isActive() { @@ -82,7 +82,7 @@ final class ActionHUDController: HUDInterfaceController { // MARK: - Menu Items @IBAction func togglePreMealMode() { - guard var glucoseTargetRangeSchedule = loopManager?.settings.glucoseTargetRangeSchedule else { + guard var glucoseTargetRangeSchedule = loopManager.settings.glucoseTargetRangeSchedule else { return } if preMealButtonGroup.state == .on { @@ -97,7 +97,7 @@ final class ActionHUDController: HUDInterfaceController { } @IBAction func toggleWorkoutMode() { - guard var glucoseTargetRangeSchedule = loopManager?.settings.glucoseTargetRangeSchedule else { + guard var glucoseTargetRangeSchedule = loopManager.settings.glucoseTargetRangeSchedule else { return } if workoutButtonGroup.state == .on { @@ -127,11 +127,11 @@ final class ActionHUDController: HUDInterfaceController { if let error = error { if self.pendingMessageResponses == 0 { ExtensionDelegate.shared().present(error) - self.updateForOverrideContext(self.loopManager?.settings.glucoseTargetRangeSchedule?.override?.context) + self.updateForOverrideContext(self.loopManager.settings.glucoseTargetRangeSchedule?.override?.context) } } else { if self.pendingMessageResponses == 0 { - self.loopManager?.settings.glucoseTargetRangeSchedule = schedule + self.loopManager.settings.glucoseTargetRangeSchedule = schedule } } } @@ -139,7 +139,7 @@ final class ActionHUDController: HUDInterfaceController { } catch { pendingMessageResponses -= 1 if pendingMessageResponses == 0 { - updateForOverrideContext(self.loopManager?.settings.glucoseTargetRangeSchedule?.override?.context) + updateForOverrideContext(self.loopManager.settings.glucoseTargetRangeSchedule?.override?.context) } presentAlert( withTitle: NSLocalizedString("Send Failed", comment: "The title of the alert controller displayed after a glucose range override send attempt fails"), diff --git a/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift b/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift index 43e66d96e2..5b46a63147 100644 --- a/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift +++ b/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift @@ -181,6 +181,9 @@ final class AddCarbsInterfaceController: WKInterfaceController, IdentifiableClas replyHandler: { (suggestion) in DispatchQueue.main.async { WKInterfaceDevice.current().play(.success) + + ExtensionDelegate.shared().loopManager.addConfirmedCarbEntry(entry) + WKExtension.shared().rootInterfaceController?.presentController(withName: BolusInterfaceController.className, context: suggestion) } }, @@ -215,31 +218,13 @@ extension AddCarbsInterfaceController: WKCrownDelegate { func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) { accumulatedRotation += rotationalDelta let remainder = accumulatedRotation.truncatingRemainder(dividingBy: rotationsPerIncrement) - var delta = Int((accumulatedRotation - remainder) / rotationsPerIncrement) + let delta = Int((accumulatedRotation - remainder) / rotationsPerIncrement) switch inputMode { case .value: - let oldValue = carbValue carbValue += delta - - // If we didn't change, adjust the delta to prevent the haptic - if oldValue == carbValue { - delta = 0 - } case .date: - let oldValue = date date += TimeInterval(minutes: Double(delta)) - - // If we didn't change, adjust the delta to prevent the haptic - if oldValue == date { - delta = 0 - } - } - - if delta > 0 { - WKInterfaceDevice.current().play(.click) - } else if delta < 0 { - WKInterfaceDevice.current().play(.click) } accumulatedRotation = remainder diff --git a/WatchApp Extension/Controllers/BolusInterfaceController.swift b/WatchApp Extension/Controllers/BolusInterfaceController.swift index e3015a67ca..6d83a3da29 100644 --- a/WatchApp Extension/Controllers/BolusInterfaceController.swift +++ b/WatchApp Extension/Controllers/BolusInterfaceController.swift @@ -109,6 +109,10 @@ final class BolusInterfaceController: WKInterfaceController, IdentifiableClass { override func willActivate() { // This method is called when watch view controller is about to be visible to user super.willActivate() + } + + override func didAppear() { + super.didAppear() crownSequencer.focus() } @@ -141,7 +145,11 @@ final class BolusInterfaceController: WKInterfaceController, IdentifiableClass { do { try WCSession.default.sendBolusMessage(bolus) { (error) in DispatchQueue.main.async { - ExtensionDelegate.shared().present(error) + if let error = error { + ExtensionDelegate.shared().present(error) + } else { + ExtensionDelegate.shared().loopManager.addConfirmedBolus(bolus) + } } } } catch { @@ -170,22 +178,10 @@ extension BolusInterfaceController: WKCrownDelegate { accumulatedRotation += rotationalDelta let remainder = accumulatedRotation.truncatingRemainder(dividingBy: rotationsPerValue) - var delta = Int((accumulatedRotation - remainder) / rotationsPerValue) + let delta = Int((accumulatedRotation - remainder) / rotationsPerValue) - let oldValue = pickerValue pickerValue += delta - // If we didn't change, adjust the delta to prevent the haptic - if oldValue == pickerValue { - delta = 0 - } - - if delta > 0 { - WKInterfaceDevice.current().play(.click) - } else if delta < 0 { - WKInterfaceDevice.current().play(.click) - } - accumulatedRotation = remainder } } diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 2545739dc7..1297a0ecc6 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -34,12 +34,13 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { } } private let log = OSLog(category: "ChartHUDController") + private var hasInitialActivation = false override init() { super.init() loopManager = ExtensionDelegate.shared().loopManager - NotificationCenter.default.addObserver(forName: .GlucoseSamplesDidChange, object: loopManager?.glucoseStore, queue: nil) { [weak self] (note) in + NotificationCenter.default.addObserver(forName: .GlucoseSamplesDidChange, object: loopManager.glucoseStore, queue: nil) { [weak self] (note) in self?.log.default("Received GlucoseSamplesDidChange notification: %{public}@. Updating chart", String(describing: note.userInfo ?? [:])) DispatchQueue.main.async { @@ -99,7 +100,14 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { log.default("willActivate() unpausing") } - loopManager?.requestGlucoseBackfillIfNecessary() + if !hasInitialActivation && UserDefaults.standard.startOnChartPage { + log.default("Switching to startOnChartPage") + becomeCurrentPage() + } + + hasInitialActivation = true + + loopManager.requestGlucoseBackfillIfNecessary() } override func didDeactivate() { @@ -112,7 +120,7 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { override func update() { super.update() - guard let activeContext = loopManager?.activeContext else { + guard let activeContext = loopManager.activeContext else { return } @@ -179,15 +187,15 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { } func updateGlucoseChart() { - guard let activeContext = loopManager?.activeContext else { + guard let activeContext = loopManager.activeContext else { return } scene.predictedGlucose = activeContext.predictedGlucose?.values - scene.correctionRange = loopManager?.settings.glucoseTargetRangeSchedule + scene.correctionRange = loopManager.settings.glucoseTargetRangeSchedule scene.unit = activeContext.preferredGlucoseUnit - loopManager?.glucoseStore.getCachedGlucoseSamples(start: .earliestGlucoseCutoff) { (samples) in + loopManager.glucoseStore.getCachedGlucoseSamples(start: .earliestGlucoseCutoff) { (samples) in DispatchQueue.main.async { self.scene.historicalGlucose = samples self.scene.setNeedsUpdate() diff --git a/WatchApp Extension/Controllers/HUDInterfaceController.swift b/WatchApp Extension/Controllers/HUDInterfaceController.swift index 55eb8299d7..efb35dc50c 100644 --- a/WatchApp Extension/Controllers/HUDInterfaceController.swift +++ b/WatchApp Extension/Controllers/HUDInterfaceController.swift @@ -16,11 +16,7 @@ class HUDInterfaceController: WKInterfaceController { @IBOutlet weak var glucoseLabel: WKInterfaceLabel! @IBOutlet weak var eventualGlucoseLabel: WKInterfaceLabel! - weak var loopManager: LoopDataManager? - - override init() { - loopManager = ExtensionDelegate.shared().loopManager - } + var loopManager = ExtensionDelegate.shared().loopManager override func willActivate() { super.willActivate() @@ -46,7 +42,9 @@ class HUDInterfaceController: WKInterfaceController { } func update() { - guard let activeContext = loopManager?.activeContext, let date = activeContext.loopLastRunDate else { + guard let activeContext = loopManager.activeContext, + let date = activeContext.loopLastRunDate + else { loopHUDImage.setLoopImage(.unknown) loopTimer.setHidden(true) return @@ -91,10 +89,10 @@ class HUDInterfaceController: WKInterfaceController { } @IBAction func setBolus() { - var context = loopManager?.activeContext?.bolusSuggestion ?? BolusSuggestionUserInfo(recommendedBolus: nil) + var context = loopManager.activeContext?.bolusSuggestion ?? BolusSuggestionUserInfo(recommendedBolus: nil) if context.maxBolus == nil { - context.maxBolus = loopManager?.settings.maximumBolus + context.maxBolus = loopManager.settings.maximumBolus } presentController(withName: BolusInterfaceController.className, context: context) diff --git a/WatchApp Extension/Extensions/WCSession.swift b/WatchApp Extension/Extensions/WCSession.swift index b135fede49..72055d4245 100644 --- a/WatchApp Extension/Extensions/WCSession.swift +++ b/WatchApp Extension/Extensions/WCSession.swift @@ -49,7 +49,7 @@ extension WCSession { ) } - func sendBolusMessage(_ userInfo: SetBolusUserInfo, errorHandler: @escaping (Error) -> Void) throws { + func sendBolusMessage(_ userInfo: SetBolusUserInfo, completionHandler: @escaping (Error?) -> Void) throws { guard activationState == .activated else { throw MessageError.activation } @@ -59,8 +59,12 @@ extension WCSession { } sendMessage(userInfo.rawValue, - replyHandler: { reply in }, - errorHandler: errorHandler + replyHandler: { reply in + completionHandler(nil) + }, + errorHandler: { error in + completionHandler(error) + } ) } diff --git a/WatchApp Extension/Managers/LoopDataManager.swift b/WatchApp Extension/Managers/LoopDataManager.swift index b8ac90d1f5..7f2b5f9e70 100644 --- a/WatchApp Extension/Managers/LoopDataManager.swift +++ b/WatchApp Extension/Managers/LoopDataManager.swift @@ -69,6 +69,18 @@ extension LoopDataManager { } } + func addConfirmedBolus(_ bolus: SetBolusUserInfo) { + dispatchPrecondition(condition: .onQueue(.main)) + + activeContext?.iob = (activeContext?.iob ?? 0) + bolus.value + } + + func addConfirmedCarbEntry(_ entry: CarbEntryUserInfo) { + dispatchPrecondition(condition: .onQueue(.main)) + + activeContext?.cob = (activeContext?.cob ?? 0) + entry.value + } + func sendDidUpdateContextNotificationIfNecessary() { dispatchPrecondition(condition: .onQueue(.main)) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 26264c64d7..b5a11687ee 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -42,7 +42,7 @@ private extension SKLabelNode { private extension SKSpriteNode { func move(to rect: CGRect, animated: Bool) { - if parent == nil || animated == false { + if parent == nil || animated == false || (size.equalTo(rect.size) && position.equalTo(rect.origin)) { size = rect.size position = rect.origin } else { @@ -55,6 +55,19 @@ private extension SKSpriteNode { } } +extension CGRect { + fileprivate func alignedToScreenScale(_ screenScale: CGFloat) -> CGRect { + let factor = 1 / screenScale + + return CGRect( + x: origin.x.floored(to: factor), + y: origin.y.floored(to: factor), + width: size.width.ceiled(to: factor), + height: size.height.ceiled(to: factor) + ) + } +} + private struct Scaler { let dates: DateInterval let glucoseMin: Double @@ -81,7 +94,7 @@ private struct Scaler { let a = point(max(dates.start, range.start), minY) let b = point(min(dates.end, range.end), maxY) let size = CGSize(width: b.x - a.x, height: max(b.y - a.y, minHeight)) - return CGRect(origin: CGPoint(x: a.x + size.width / 2, y: a.y + size.height / 2), size: size) + return CGRect(origin: CGPoint(x: a.x + size.width / 2, y: a.y + size.height / 2), size: size).alignedToScreenScale(WKInterfaceDevice.current().screenScale) } } @@ -110,6 +123,8 @@ private extension HKUnit { class GlucoseChartScene: SKScene { let log = OSLog(category: "GlucoseChartScene") + var textInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) + var unit: HKUnit? var correctionRange: GlucoseRangeSchedule? var historicalGlucose: [SampleValue]? { @@ -167,14 +182,18 @@ class GlucoseChartScene: SKScene { return lowerBound.. 180: // 44mm + height = 106 + default: + height = 86 + } super.init(size: CGSize(width: screen.width, height: height)) } @@ -206,14 +234,14 @@ class GlucoseChartScene: SKScene { now.zPosition = NodePlane.lines.zPosition addChild(now) - hoursLabel = SKLabelNode.basic(at: CGPoint(x: 5, y: size.height - 5)) + hoursLabel = SKLabelNode.basic(at: CGPoint(x: textInsets.left, y: size.height - textInsets.top)) addChild(hoursLabel) - maxBGLabel = SKLabelNode.basic(at: CGPoint(x: size.width - 5, y: size.height - 5)) + maxBGLabel = SKLabelNode.basic(at: CGPoint(x: size.width - textInsets.right, y: size.height - textInsets.top)) maxBGLabel.horizontalAlignmentMode = .right addChild(maxBGLabel) - minBGLabel = SKLabelNode.basic(at: CGPoint(x: size.width - 5, y: 5)) + minBGLabel = SKLabelNode.basic(at: CGPoint(x: size.width - textInsets.right, y: textInsets.bottom)) minBGLabel.horizontalAlignmentMode = .right minBGLabel.verticalAlignmentMode = .bottom addChild(minBGLabel) @@ -242,8 +270,6 @@ class GlucoseChartScene: SKScene { } } - log.default("childNodesHaveActions: %d", childNodesHaveActions) - return childNodesHaveActions } @@ -275,7 +301,7 @@ class GlucoseChartScene: SKScene { needsUpdate = true if isPaused { - log.default("updateNodes(animated:) unpausing") + log.default("setNeedsUpdate() unpausing") isPaused = false } } @@ -340,7 +366,7 @@ class GlucoseChartScene: SKScene { let (sprite, created) = getSprite(forHash: $0.chartHashValue) sprite.color = .glucose sprite.zPosition = NodePlane.values.zPosition - sprite.move(to: CGRect(origin: origin, size: size), animated: !created) + sprite.move(to: CGRect(origin: origin, size: size).alignedToScreenScale(WKInterfaceDevice.current().screenScale), animated: !created) inactiveNodes.removeValue(forKey: $0.chartHashValue) } diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/WatchApp/Assets.xcassets/AppIcon.appiconset/Contents.json index a8d3101a64..736d59705c 100644 --- a/WatchApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/WatchApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -38,6 +38,20 @@ "role" : "appLauncher", "subtype" : "38mm" }, + { + "size" : "44x44", + "idiom" : "watch", + "scale" : "2x", + "role" : "appLauncher", + "subtype" : "40mm" + }, + { + "size" : "50x50", + "idiom" : "watch", + "scale" : "2x", + "role" : "appLauncher", + "subtype" : "44mm" + }, { "size" : "86x86", "idiom" : "watch", @@ -54,6 +68,13 @@ "role" : "quickLook", "subtype" : "42mm" }, + { + "size" : "108x108", + "idiom" : "watch", + "scale" : "2x", + "role" : "quickLook", + "subtype" : "44mm" + }, { "size" : "1024x1024", "idiom" : "watch-marketing", @@ -65,4 +86,4 @@ "version" : 1, "author" : "xcode" } -} +} \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json index e835ece7e8..ad1e630c38 100644 --- a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json @@ -6,11 +6,21 @@ "screen-width" : "<=145", "scale" : "2x" }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">161" + }, { "idiom" : "watch", "filename" : "1-hour-graph-42mm.png", "screen-width" : ">145", "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/Contents.json index b2b2284ab7..7a15fc41f4 100644 --- a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/Contents.json @@ -6,11 +6,21 @@ "screen-width" : "<=145", "scale" : "2x" }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">161" + }, { "idiom" : "watch", "filename" : "2-hour-graph-42mm.png", "screen-width" : ">145", "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json index 17343c647c..d82030f923 100644 --- a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json @@ -6,11 +6,21 @@ "screen-width" : "<=145", "scale" : "2x" }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">161" + }, { "idiom" : "watch", "filename" : "3-hour-graph-42mm.png", "screen-width" : ">145", "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Assets.xcassets/bolus.imageset/Contents.json b/WatchApp/Assets.xcassets/bolus.imageset/Contents.json index 2dbb853d5c..c2810020f3 100644 --- a/WatchApp/Assets.xcassets/bolus.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/bolus.imageset/Contents.json @@ -6,11 +6,21 @@ "screen-width" : "<=145", "scale" : "2x" }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">161" + }, { "idiom" : "watch", "filename" : "Bolus Icon 42mm.png", "screen-width" : ">145", "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Assets.xcassets/carbs.imageset/Contents.json b/WatchApp/Assets.xcassets/carbs.imageset/Contents.json index ecb8ce2515..5e0d2deaa5 100644 --- a/WatchApp/Assets.xcassets/carbs.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/carbs.imageset/Contents.json @@ -6,11 +6,21 @@ "screen-width" : "<=145", "scale" : "2x" }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">161" + }, { "idiom" : "watch", "filename" : "Carbs Icon 42mm.png", "screen-width" : ">145", "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Assets.xcassets/loop/loop_aging.imageset/Contents.json b/WatchApp/Assets.xcassets/loop/loop_aging.imageset/Contents.json index cb4811aff0..f049e5e770 100644 --- a/WatchApp/Assets.xcassets/loop/loop_aging.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/loop/loop_aging.imageset/Contents.json @@ -2,6 +2,7 @@ "images" : [ { "idiom" : "watch", + "filename" : "loop-aging@42mm.png", "scale" : "2x" }, { @@ -12,9 +13,18 @@ }, { "idiom" : "watch", - "filename" : "loop-aging@42mm.png", - "screen-width" : ">145", - "scale" : "2x" + "scale" : "2x", + "screen-width" : ">161" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Assets.xcassets/loop/loop_fresh.imageset/Contents.json b/WatchApp/Assets.xcassets/loop/loop_fresh.imageset/Contents.json index 585f56d7fa..f4a4a1fadd 100644 --- a/WatchApp/Assets.xcassets/loop/loop_fresh.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/loop/loop_fresh.imageset/Contents.json @@ -2,6 +2,7 @@ "images" : [ { "idiom" : "watch", + "filename" : "loop-fresh@42mm.png", "scale" : "2x" }, { @@ -12,9 +13,18 @@ }, { "idiom" : "watch", - "filename" : "loop-fresh@42mm.png", - "screen-width" : ">145", - "scale" : "2x" + "scale" : "2x", + "screen-width" : ">161" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Assets.xcassets/loop/loop_stale.imageset/Contents.json b/WatchApp/Assets.xcassets/loop/loop_stale.imageset/Contents.json index d849fa9344..abeb896280 100644 --- a/WatchApp/Assets.xcassets/loop/loop_stale.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/loop/loop_stale.imageset/Contents.json @@ -2,6 +2,7 @@ "images" : [ { "idiom" : "watch", + "filename" : "loop-stale@42mm.png", "scale" : "2x" }, { @@ -12,9 +13,18 @@ }, { "idiom" : "watch", - "filename" : "loop-stale@42mm.png", - "screen-width" : ">145", - "scale" : "2x" + "scale" : "2x", + "screen-width" : ">161" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Assets.xcassets/loop/loop_unknown.imageset/Contents.json b/WatchApp/Assets.xcassets/loop/loop_unknown.imageset/Contents.json index 91ea68fbd0..b474a60c23 100644 --- a/WatchApp/Assets.xcassets/loop/loop_unknown.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/loop/loop_unknown.imageset/Contents.json @@ -2,6 +2,7 @@ "images" : [ { "idiom" : "watch", + "filename" : "loop-unknown@42mm.png", "scale" : "2x" }, { @@ -12,9 +13,18 @@ }, { "idiom" : "watch", - "filename" : "loop-unknown@42mm.png", - "screen-width" : ">145", - "scale" : "2x" + "scale" : "2x", + "screen-width" : ">161" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Assets.xcassets/pre-meal.imageset/Contents.json b/WatchApp/Assets.xcassets/pre-meal.imageset/Contents.json index c8be362ad1..581fbb49aa 100644 --- a/WatchApp/Assets.xcassets/pre-meal.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/pre-meal.imageset/Contents.json @@ -6,11 +6,21 @@ "screen-width" : "<=145", "scale" : "2x" }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">161" + }, { "idiom" : "watch", "filename" : "Pre-Meal 42mm.png", "screen-width" : ">145", "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Assets.xcassets/workout.imageset/Contents.json b/WatchApp/Assets.xcassets/workout.imageset/Contents.json index d5d7862146..cc9ab23361 100644 --- a/WatchApp/Assets.xcassets/workout.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/workout.imageset/Contents.json @@ -6,11 +6,21 @@ "screen-width" : "<=145", "scale" : "2x" }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">161" + }, { "idiom" : "watch", "filename" : "Workout 42mm.png", "screen-width" : ">145", "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" } ], "info" : { diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index 8da55eb2df..23d08d8cf7 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -1,18 +1,18 @@ - - + + - - + + - + @@ -98,14 +98,22 @@ - + + + + + + + + + @@ -153,6 +161,8 @@ + + @@ -269,7 +279,7 @@ - + @@ -313,14 +323,22 @@ - + + + + + + + + + @@ -334,7 +352,7 @@ - + @@ -361,10 +379,12 @@ - + + @@ -373,16 +393,19 @@ + - + - + +