From a3d890f56c145418837853cf447da4f0f5be804c Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Fri, 29 Jun 2018 15:56:29 -0700 Subject: [PATCH 01/51] 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. --- .../GlucoseBackfillRequestUserInfo.swift | 40 +++ Common/Models/WatchContext.swift | 32 ++- Common/Models/WatchDatedRange.swift | 54 +++++ Common/Models/WatchHistoricalGlucose.swift | 51 ++++ Common/Models/WatchPredictedGlucose.swift | 52 ++++ .../Base.lproj/MainInterface.storyboard | 4 +- Loop.xcodeproj/project.pbxproj | 72 +++++- Loop/Managers/WatchDataManager.swift | 80 ++++-- .../ComplicationController.swift | 8 +- ...roller.swift => ActionHUDController.swift} | 103 ++------ .../Controllers/ChartHUDController.swift | 140 +++++++++++ .../Controllers/ContextUpdatable.swift | 14 -- .../Controllers/HUDInterfaceController.swift | 81 +++++++ WatchApp Extension/ExtensionDelegate.swift | 19 +- WatchApp Extension/Extensions/Date.swift | 20 ++ .../Extensions/GlucoseStore.swift | 25 ++ .../Extensions/NSUserDefaults.swift | 10 + WatchApp Extension/Extensions/WCSession.swift | 16 ++ WatchApp Extension/Info.plist | 2 + .../Managers/LoopDataManager.swift | 31 +++ .../Managers/StatusChartsManager.swift | 228 ++++++++++++++++++ .../WatchApp Extension.entitlements | 4 + WatchApp/Base.lproj/Interface.storyboard | 90 ++++++- 23 files changed, 1012 insertions(+), 164 deletions(-) create mode 100644 Common/Models/GlucoseBackfillRequestUserInfo.swift create mode 100644 Common/Models/WatchDatedRange.swift create mode 100644 Common/Models/WatchHistoricalGlucose.swift create mode 100644 Common/Models/WatchPredictedGlucose.swift rename WatchApp Extension/Controllers/{StatusInterfaceController.swift => ActionHUDController.swift} (61%) create mode 100644 WatchApp Extension/Controllers/ChartHUDController.swift delete mode 100644 WatchApp Extension/Controllers/ContextUpdatable.swift create mode 100644 WatchApp Extension/Controllers/HUDInterfaceController.swift create mode 100644 WatchApp Extension/Extensions/Date.swift create mode 100644 WatchApp Extension/Extensions/GlucoseStore.swift create mode 100644 WatchApp Extension/Managers/LoopDataManager.swift create mode 100644 WatchApp Extension/Managers/StatusChartsManager.swift diff --git a/Common/Models/GlucoseBackfillRequestUserInfo.swift b/Common/Models/GlucoseBackfillRequestUserInfo.swift new file mode 100644 index 0000000000..899a93434b --- /dev/null +++ b/Common/Models/GlucoseBackfillRequestUserInfo.swift @@ -0,0 +1,40 @@ +// +// GlucoseBackfillRequestUserInfo.swift +// Loop +// +// Created by Bharat Mediratta on 6/21/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation + +struct GlucoseBackfillRequestUserInfo { + let version = 1 + let startDate: Date +} + +extension GlucoseBackfillRequestUserInfo: RawRepresentable { + typealias RawValue = [String: Any] + + static let name = "GlucoseBackfillRequestUserInfo" + + init?(rawValue: RawValue) { + guard + rawValue["v"] as? Int == version, + rawValue["name"] as? String == GlucoseBackfillRequestUserInfo.name, + let startDate = rawValue["sd"] as? Date + else { + return nil + } + + self.startDate = startDate + } + + var rawValue: RawValue { + return [ + "v": version, + "name": GlucoseBackfillRequestUserInfo.name, + "sd": startDate + ] + } +} diff --git a/Common/Models/WatchContext.swift b/Common/Models/WatchContext.swift index 8620c9e111..b7376604e3 100644 --- a/Common/Models/WatchContext.swift +++ b/Common/Models/WatchContext.swift @@ -9,10 +9,11 @@ import Foundation import HealthKit + final class WatchContext: NSObject, RawRepresentable { typealias RawValue = [String: Any] - private let version = 3 + private let version = 4 var preferredGlucoseUnit: HKUnit? var maxBolus: Double? @@ -22,6 +23,8 @@ final class WatchContext: NSObject, RawRepresentable { var eventualGlucose: HKQuantity? var glucoseDate: Date? + var targetRanges: [WatchDatedRange]? + var temporaryOverride: WatchDatedRange? var glucoseRangeScheduleOverride: GlucoseRangeScheduleOverrideUserInfo? var configuredOverrideContexts: [GlucoseRangeScheduleOverrideUserInfo.Context] = [] @@ -41,6 +44,7 @@ final class WatchContext: NSObject, RawRepresentable { var reservoir: Double? var reservoirPercentage: Double? var batteryPercentage: Double? + var predictedGlucose: WatchPredictedGlucose? var cgm: CGM? @@ -56,16 +60,15 @@ 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) + glucose = HKQuantity(unit: unit, doubleValue: glucoseValue) } if let glucoseValue = rawValue["egv"] as? Double { - eventualGlucose = HKQuantity(unit: preferredGlucoseUnit ?? .milligramsPerDeciliter, doubleValue: glucoseValue) + eventualGlucose = HKQuantity(unit: unit, doubleValue: glucoseValue) } glucoseTrendRawValue = rawValue["gt"] as? Int @@ -93,6 +96,18 @@ final class WatchContext: NSObject, RawRepresentable { COB = rawValue["cob"] as? Double maxBolus = rawValue["mb"] as? Double + if let rawValue = rawValue["pg"] as? WatchPredictedGlucose.RawValue { + predictedGlucose = WatchPredictedGlucose(rawValue: rawValue) + } + + if let rawValue = rawValue["tr"] as? [WatchDatedRange.RawValue] { + targetRanges = rawValue.compactMap({return WatchDatedRange(rawValue: $0)}) + } + + if let rawValue = rawValue["to"] as? WatchDatedRange.RawValue { + temporaryOverride = WatchDatedRange(rawValue: rawValue) + } + if let cgmRawValue = rawValue["cgm"] as? CGM.RawValue { cgm = CGM(rawValue: cgmRawValue) } @@ -127,6 +142,11 @@ final class WatchContext: NSObject, RawRepresentable { raw["rbo"] = recommendedBolusDose raw["rp"] = reservoirPercentage + raw["pg"] = predictedGlucose?.rawValue + + raw["tr"] = targetRanges?.map { $0.rawValue } + raw["to"] = temporaryOverride?.rawValue + return raw } } diff --git a/Common/Models/WatchDatedRange.swift b/Common/Models/WatchDatedRange.swift new file mode 100644 index 0000000000..01d3962524 --- /dev/null +++ b/Common/Models/WatchDatedRange.swift @@ -0,0 +1,54 @@ +// +// WatchDatedRangeContext.swift +// WatchApp Extension +// +// Created by Bharat Mediratta on 6/26/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation + + +struct WatchDatedRange { + public let startDate: Date + public let endDate: Date + public let minValue: Double + public let maxValue: Double + + public init(startDate: Date, endDate: Date, minValue: Double, maxValue: Double) { + self.startDate = startDate + self.endDate = endDate + self.minValue = minValue + self.maxValue = maxValue + } +} + + +extension WatchDatedRange: RawRepresentable { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + return [ + "sd": startDate, + "ed": endDate, + "mi": minValue, + "ma": maxValue + ] + } + + init?(rawValue: RawValue) { + guard + let startDate = rawValue["sd"] as? Date, + let endDate = rawValue["ed"] as? Date, + let minValue = rawValue["mi"] as? Double, + let maxValue = rawValue["ma"] as? Double + else { + return nil + } + + self.startDate = startDate + self.endDate = endDate + self.minValue = minValue + self.maxValue = maxValue + } +} diff --git a/Common/Models/WatchHistoricalGlucose.swift b/Common/Models/WatchHistoricalGlucose.swift new file mode 100644 index 0000000000..0e83fa3f8b --- /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) + } + } +} + + +extension WatchHistoricalGlucose: RawRepresentable { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + return [ + "d": samples.map { $0.date }, + "v": samples.map { UInt16($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? [UInt16], + 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 { UInt16($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? [UInt16], + 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 c9e63e32c3..a5c2c5d88b 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -47,7 +47,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 */; }; @@ -119,7 +119,6 @@ 438172D91F4E9E37003C3328 /* NewPumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438172D81F4E9E37003C3328 /* NewPumpEvent.swift */; }; 4381D2261F3C0FDD004ACCF9 /* RileyLinkDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4381D2251F3C0FDD004ACCF9 /* RileyLinkDevice.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 */; }; @@ -215,6 +214,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 */; }; @@ -236,6 +239,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 */; }; @@ -250,6 +254,12 @@ 4F7528A71DFE20CE00C322D6 /* SensorDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EA28611D517E42001BC233 /* SensorDisplayable.swift */; }; 4F7528A91DFE212600C322D6 /* GlucoseTrend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EA285E1D50ED3D001BC233 /* GlucoseTrend.swift */; }; 4F7528AA1DFE215100C322D6 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D5E1DF2459000A04910 /* HKUnit.swift */; }; + 4F7E8AC520E2AB9600AEA65E /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7E8AC420E2AB9600AEA65E /* Date.swift */; }; + 4F7E8AC720E2AC0300AEA65E /* WatchPredictedGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7E8AC620E2AC0300AEA65E /* WatchPredictedGlucose.swift */; }; + 4F7E8AC920E2AC3700AEA65E /* WatchDatedRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7E8AC820E2AC3700AEA65E /* WatchDatedRange.swift */; }; + 4F7E8ACA20E2ACAE00AEA65E /* WatchDatedRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7E8AC820E2AC3700AEA65E /* WatchDatedRange.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 */; }; @@ -265,10 +275,15 @@ 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 */; }; + 4FE3475E20D5D77900A86D03 /* StatusChartsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE3475D20D5D77900A86D03 /* StatusChartsManager.swift */; }; + 4FF0F75E20E1E5D100FC6291 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431E73471FF95A900069B5F7 /* PersistenceController.swift */; }; + 4FF0F75F20E1E5EF00FC6291 /* NSBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430DA58D1D4AEC230097D1CA /* NSBundle.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 */; }; + 4FFEDFBF20E5CF22000BFC58 /* ChartHUDController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFEDFBE20E5CF22000BFC58 /* ChartHUDController.swift */; }; 540DED971E14C75F002B2491 /* EnliteSensorDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */; }; 7D7076351FE06EDE004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076371FE06EDE004AC8EA /* Localizable.strings */; }; 7D70763A1FE06EDF004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D70763C1FE06EDF004AC8EA /* InfoPlist.strings */; }; @@ -459,7 +474,7 @@ 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 = ""; }; @@ -530,7 +545,6 @@ 438172D81F4E9E37003C3328 /* NewPumpEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPumpEvent.swift; sourceTree = ""; }; 4381D2251F3C0FDD004ACCF9 /* RileyLinkDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDevice.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 = ""; }; @@ -632,6 +646,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 = ""; }; @@ -647,13 +663,21 @@ 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 = ""; }; + 4F7E8AC420E2AB9600AEA65E /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; + 4F7E8AC620E2AC0300AEA65E /* WatchPredictedGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchPredictedGlucose.swift; sourceTree = ""; }; + 4F7E8AC820E2AC3700AEA65E /* WatchDatedRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchDatedRange.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 = ""; }; + 4FE3475D20D5D77900A86D03 /* StatusChartsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusChartsManager.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 = ""; }; 540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnliteSensorDisplayable.swift; sourceTree = ""; }; 7D68AAA91FE2DB0A00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; 7D68AAAA1FE2DB0A00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Main.strings; sourceTree = ""; }; @@ -787,9 +811,10 @@ children = ( 4328E01D1CFBE25F00E199AA /* AddCarbsInterfaceController.swift */, 4328E0161CFBE1DA00E199AA /* BolusInterfaceController.swift */, - 43846ADA1D91057000799272 /* ContextUpdatable.swift */, 43A943891B926B7B0051FA24 /* NotificationController.swift */, - 4328E0151CFBE1DA00E199AA /* StatusInterfaceController.swift */, + 4F82654F20E69F9A0031A8F5 /* HUDInterfaceController.swift */, + 4328E0151CFBE1DA00E199AA /* ActionHUDController.swift */, + 4FFEDFBE20E5CF22000BFC58 /* ChartHUDController.swift */, ); path = Controllers; sourceTree = ""; @@ -799,6 +824,8 @@ children = ( 4344629120A7C19800C4BE6F /* ButtonGroup.swift */, 4328E0221CFBE2C500E199AA /* CLKComplicationTemplate.swift */, + 4F7E8AC420E2AB9600AEA65E /* Date.swift */, + 4F73F5FB20E2E7FA00E8D82C /* GlucoseStore.swift */, 4328E0201CFBE2C500E199AA /* IdentifiableClass.swift */, 4328E0231CFBE2C500E199AA /* NSUserDefaults.swift */, 4328E0241CFBE2C500E199AA /* UIColor.swift */, @@ -948,6 +975,7 @@ 43A9438F1B926B7B0051FA24 /* Assets.xcassets */, 4328E0121CFBE1B700E199AA /* Controllers */, 4328E01F1CFBE2B100E199AA /* Extensions */, + 4FE3475F20D5D7FA00A86D03 /* Managers */, 43A943831B926B7B0051FA24 /* Supporting Files */, ); path = "WatchApp Extension"; @@ -1183,6 +1211,15 @@ path = Extensions; sourceTree = ""; }; + 4FE3475F20D5D7FA00A86D03 /* Managers */ = { + isa = PBXGroup; + children = ( + 4FE3475D20D5D77900A86D03 /* StatusChartsManager.swift */, + 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */, + ); + path = Managers; + sourceTree = ""; + }; 4FF4D0FA1E1834BD00846527 /* Common */ = { isa = PBXGroup; children = ( @@ -1198,6 +1235,7 @@ 43673E2E1F37BDA10058AC7C /* Insulin */, 435400301C9F744E00D5819C /* BolusSuggestionUserInfo.swift */, 43DE92581C5479E4001FFDE1 /* CarbEntryUserInfo.swift */, + 4F11D3BF20DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift */, 4309786D1E73DAD100BEBC82 /* CGM.swift */, 894B91CC1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift */, 430B298D2041F56500BA9F93 /* GlucoseThreshold.swift */, @@ -1206,6 +1244,9 @@ 435400331C9F878D00D5819C /* SetBolusUserInfo.swift */, 4F70C2111DE900EA006380B7 /* StatusExtensionContext.swift */, 4FF4D0FF1E18374700846527 /* WatchContext.swift */, + 4F11D3C120DD80B3006E072C /* WatchHistoricalGlucose.swift */, + 4F7E8AC620E2AC0300AEA65E /* WatchPredictedGlucose.swift */, + 4F7E8AC820E2AC3700AEA65E /* WatchDatedRange.swift */, ); path = Models; sourceTree = ""; @@ -1729,6 +1770,7 @@ 4341F4EB1EDB92AC001C936B /* LogglyService.swift in Sources */, 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */, 43BFF0CB1E466C0900FF19A9 /* StateColorPalette.swift in Sources */, + 4F11D3C020DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */, 43F5C2DB1B92A5E1003EB13D /* SettingsTableViewController.swift in Sources */, 434FF1EA1CF26C29000DB779 /* IdentifiableClass.swift in Sources */, 437CCADE1D2858FD0075D2C3 /* AuthenticationViewController.swift in Sources */, @@ -1768,6 +1810,7 @@ 43F5173D1D713DB0000FA422 /* RadioSelectionTableViewController.swift in Sources */, C178249A1E1999FA00D9D25C /* CaseCountable.swift in Sources */, 43DBF04C1C93B8D700B3C386 /* BolusViewController.swift in Sources */, + 4F7E8ACA20E2ACAE00AEA65E /* WatchDatedRange.swift in Sources */, 4FB76FBB1E8C42CF00B39636 /* UIColor.swift in Sources */, 4374B5EF209D84BF00D17AA8 /* OSLog.swift in Sources */, 4F6663941E905FD2009E74FC /* ChartColorPalette+Loop.swift in Sources */, @@ -1795,8 +1838,10 @@ 43CEE6E61E56AFD400CB9116 /* NightscoutUploader.swift in Sources */, 4328E0331CFC091100E199AA /* WatchContext+LoopKit.swift in Sources */, 4F526D611DF8D9A900A04910 /* NetBasal.swift in Sources */, + 4F7E8ACB20E2ACB500AEA65E /* WatchPredictedGlucose.swift in Sources */, 540DED971E14C75F002B2491 /* EnliteSensorDisplayable.swift in Sources */, 436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */, + 4F11D3C220DD80B3006E072C /* WatchHistoricalGlucose.swift in Sources */, 435CB6231F37967800C320C7 /* InsulinModelSettingsViewController.swift in Sources */, 43E2D8C61D204678004DA55F /* KeychainManager.swift in Sources */, 431E73481FF95A900069B5F7 /* PersistenceController.swift in Sources */, @@ -1837,25 +1882,36 @@ 43A943881B926B7B0051FA24 /* ExtensionDelegate.swift in Sources */, 4328E02F1CFBF81800E199AA /* WKInterfaceImage.swift in Sources */, 4F2C15811E0495B200E160D4 /* WatchContext+WatchApp.swift in Sources */, + 4FF0F75F20E1E5EF00FC6291 /* NSBundle.swift in Sources */, 4344629820A8B2D700C4BE6F /* OSLog.swift in Sources */, 4328E02A1CFBE2C500E199AA /* UIColor.swift in Sources */, + 4FDDD23720DC51DF00D04B16 /* LoopDataManager.swift in Sources */, 4328E01B1CFBE1DA00E199AA /* BolusInterfaceController.swift in Sources */, + 4F82655020E69F9A0031A8F5 /* HUDInterfaceController.swift in Sources */, 4328E02B1CFBE2C500E199AA /* WKAlertAction.swift in Sources */, 4365050520A793FA00EA8D7A /* CGM.swift in Sources */, + 4F7E8AC720E2AC0300AEA65E /* WatchPredictedGlucose.swift in Sources */, 4344628E20A7ADD100C4BE6F /* UserDefaults+CGM.swift in Sources */, + 4F7E8AC520E2AB9600AEA65E /* Date.swift in Sources */, + 4FFEDFBF20E5CF22000BFC58 /* ChartHUDController.swift in Sources */, 894B91CE1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.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 */, + 4F73F5FC20E2E7FA00E8D82C /* GlucoseStore.swift in Sources */, + 4FE3475E20D5D77900A86D03 /* StatusChartsManager.swift in Sources */, 432CF87520D8AC950066B889 /* NSUserDefaults.swift in Sources */, 43027F0F1DFE0EC900C51989 /* HKUnit.swift in Sources */, 4344629220A7C19800C4BE6F /* ButtonGroup.swift in Sources */, + 4F7E8AC920E2AC3700AEA65E /* WatchDatedRange.swift in Sources */, 43CB2B2B1D924D450079823D /* WCSession.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 */, 435400351C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 07ed7ab91c..f04373dc4c 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -11,7 +11,6 @@ import UIKit import WatchConnectivity import LoopKit - final class WatchDataManager: NSObject, WCSessionDelegate { unowned let deviceDataManager: DeviceDataManager @@ -48,6 +47,8 @@ final class WatchDataManager: NSObject, WCSessionDelegate { } switch updateContext { + case .glucose: + break case .tempBasal: break case .preferences: @@ -118,13 +119,18 @@ 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) - context.reservoir = reservoir?.unitVolume + let updateGroup = DispatchGroup() + + let startDate = Date().addingTimeInterval(TimeInterval(minutes: -180)) + let endDate = Date().addingTimeInterval(TimeInterval(minutes: 180)) + let context = WatchContext(glucose: glucose, eventualGlucose: state.predictedGlucose?.last, 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.deviceDataManager.sensorInfo?.trendType?.rawValue context.cgm = self.deviceDataManager.cgm @@ -135,17 +141,54 @@ final class WatchDataManager: NSObject, WCSessionDelegate { startDate: override.start, endDate: override.end ) + + context.temporaryOverride = WatchDatedRange( + startDate: override.start, + endDate: override.end ?? .distantFuture, + minValue: override.value.minValue, + maxValue: override.value.maxValue + ) } let configuredOverrideContexts = self.deviceDataManager.loopManager.settings.glucoseTargetRangeSchedule?.configuredOverrideContexts ?? [] let configuredUserInfoOverrideContexts = configuredOverrideContexts.map { $0.correspondingUserInfoContext } context.configuredOverrideContexts = configuredUserInfoOverrideContexts + + context.targetRanges = glucoseTargetRangeSchedule.between(start: startDate, end: endDate).map { + return WatchDatedRange( + startDate: $0.startDate, + endDate: $0.endDate, + minValue: $0.value.minValue, + maxValue: $0.value.maxValue + ) + } + } + + 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() + } + + // 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.deviceDataManager.sensorInfo?.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) } } @@ -193,23 +236,20 @@ final class WatchDataManager: NSObject, WCSessionDelegate { replyHandler([:]) case GlucoseRangeScheduleOverrideUserInfo.name?: + // Successful changes will trigger a preferences change which will update the watch with the new overrides 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 = deviceDataManager.loopManager.settings.glucoseTargetRangeSchedule?.setOverride(overrideContext, from: overrideUserInfo.startDate, until: overrideUserInfo.effectiveEndDate) - - if overrideSuccess == false { - lastActiveOverrideContext = previousActiveOverrideContext - } - - replyHandler([:]) + _ = deviceDataManager.loopManager.settings.glucoseTargetRangeSchedule?.setOverride(overrideUserInfo.context.correspondingOverrideContext, from: overrideUserInfo.startDate, until: overrideUserInfo.effectiveEndDate) } else { - lastActiveOverrideContext = nil deviceDataManager.loopManager.settings.glucoseTargetRangeSchedule?.clearOverride() + } + replyHandler([:]) + case GlucoseBackfillRequestUserInfo.name?: + if let userInfo = GlucoseBackfillRequestUserInfo(rawValue: message), + let manager = deviceDataManager.loopManager { + manager.glucoseStore.getCachedGlucoseSamples(start: userInfo.startDate) { (values) in + replyHandler(WatchHistoricalGlucose(with: values).rawValue) + } + } else { replyHandler([:]) } default: diff --git a/WatchApp Extension/ComplicationController.swift b/WatchApp Extension/ComplicationController.swift index 8caa3c1de3..c99699bd8a 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().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().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().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().activeContext, let glucoseDate = context.glucoseDate, glucoseDate.timeIntervalSince(date) > 0, let template = CLKComplicationTemplate.templateForFamily(complication.family, from: context) diff --git a/WatchApp Extension/Controllers/StatusInterfaceController.swift b/WatchApp Extension/Controllers/ActionHUDController.swift similarity index 61% rename from WatchApp Extension/Controllers/StatusInterfaceController.swift rename to WatchApp Extension/Controllers/ActionHUDController.swift index a3d1d5b4c1..a6ad5eefb8 100644 --- a/WatchApp Extension/Controllers/StatusInterfaceController.swift +++ b/WatchApp Extension/Controllers/ActionHUDController.swift @@ -12,99 +12,29 @@ import CGMBLEKit 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! - +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) - private var lastOverrideContext: GlucoseRangeScheduleOverrideUserInfo.Context? - private var lastContext: WatchContext? - - override func didAppear() { - super.didAppear() - } + private lazy var preMealButtonGroup = ButtonGroup(button: preMealButton, image: preMealButtonImage, background: preMealButtonBackground, onBackgroundColor: .carbsColor, offBackgroundColor: .darkCarbsColor) - override func willActivate() { - super.willActivate() + private lazy var workoutButtonGroup = ButtonGroup(button: workoutButton, image: workoutButtonImage, background: workoutButtonBackground, onBackgroundColor: .workoutColor, offBackgroundColor: .darkWorkoutColor) - updateLoopHUD() - } + override func update() { + super.update() - private func updateLoopHUD() { - guard let date = lastContext?.loopLastRunDate else { - loopHUDImage.setLoopImage(.unknown) + guard let activeContext = loopManager?.activeContext else { 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()) + if let glucoseRangeScheduleOverride = activeContext.glucoseRangeScheduleOverride, glucoseRangeScheduleOverride.dateInterval.contains(Date()) { overrideContext = glucoseRangeScheduleOverride.context } else { @@ -113,19 +43,14 @@ final class StatusInterfaceController: WKInterfaceController, ContextUpdatable { 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 - } + for overrideContext in GlucoseRangeScheduleOverrideUserInfo.Context.allContexts { + let contextButtonGroup = buttonGroup(for: overrideContext) + if !activeContext.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?) { @@ -158,7 +83,7 @@ final class StatusInterfaceController: WKInterfaceController, ContextUpdatable { } @IBAction func setBolus() { - presentController(withName: BolusInterfaceController.className, context: lastContext?.bolusSuggestion) + presentController(withName: BolusInterfaceController.className, context: loopManager?.activeContext?.bolusSuggestion ?? 0) } @IBAction func togglePreMealMode() { diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift new file mode 100644 index 0000000000..5f43069d96 --- /dev/null +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -0,0 +1,140 @@ +// +// ChartInterfaceController.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 + + +final class ChartHUDController: HUDInterfaceController { + @IBOutlet weak var basalLabel: WKInterfaceLabel! + @IBOutlet weak var iobLabel: WKInterfaceLabel! + @IBOutlet weak var cobLabel: WKInterfaceLabel! + @IBOutlet weak var glucoseChart: WKInterfaceImage! + + private var charts = StatusChartsManager() + + override init() { + super.init() + + loopManager = ExtensionDelegate.shared().loopManager + NotificationCenter.default.addObserver(forName: .GlucoseSamplesDidChange, object: loopManager?.glucoseStore, queue: nil) { _ in + DispatchQueue.main.async { + self.updateGlucoseChart() + } + } + } + + override func awake(withContext context: Any?) { + if UserDefaults.standard.startOnChartPage { + self.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. + DispatchQueue.main.async { + self.didAppear() + } + } + } + + override func didAppear() { + super.didAppear() + } + + override func willActivate() { + super.willActivate() + + loopManager?.glucoseStore.maybeRequestGlucoseBackfill() + } + + 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:NSNumber(value: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:NSNumber(value: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:NSNumber(value: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) + } + + updateGlucoseChart() + } + + func updateGlucoseChart() { + guard let activeContext = loopManager?.activeContext else { + return + } + + charts.predictedGlucose = activeContext.predictedGlucose?.values + charts.targetRanges = activeContext.targetRanges + charts.temporaryOverride = activeContext.temporaryOverride + charts.unit = activeContext.preferredGlucoseUnit + + let updateGroup = DispatchGroup() + updateGroup.enter() + loopManager?.glucoseStore.getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { (samples) in + self.charts.historicalGlucose = samples + updateGroup.leave() + } + _ = updateGroup.wait(timeout: .distantFuture) + + self.glucoseChart.setHidden(true) + if let chart = self.charts.glucoseChart() { + self.glucoseChart.setImage(chart) + self.glucoseChart.setHidden(false) + } + } +} 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..9e06aafe72 --- /dev/null +++ b/WatchApp Extension/Controllers/HUDInterfaceController.swift @@ -0,0 +1,81 @@ +// +// 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() + + if activeContextObserver == nil { + activeContextObserver = NotificationCenter.default.addObserver(forName: .ContextUpdated, object: nil, queue: nil) { _ in + DispatchQueue.main.async { + self.update() + } + } + } + } + + override func didAppear() { + update() + } + + 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 + } + }()) + } +} diff --git a/WatchApp Extension/ExtensionDelegate.swift b/WatchApp Extension/ExtensionDelegate.swift index 05ba0a86f1..2147d9e107 100644 --- a/WatchApp Extension/ExtensionDelegate.swift +++ b/WatchApp Extension/ExtensionDelegate.swift @@ -14,6 +14,7 @@ import UserNotifications final class ExtensionDelegate: NSObject, WKExtensionDelegate { + private(set) lazy var loopManager = LoopDataManager() static func shared() -> ExtensionDelegate { return WKExtension.shared().extensionDelegate @@ -50,7 +51,13 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate { } } + func applicationWillResignActive() { + UserDefaults.standard.startOnChartPage = (WKExtension.shared().visibleInterfaceController as? ChartHUDController) != nil + } + func handle(_ backgroundTasks: Set) { + loopManager.glucoseStore.maybeRequestGlucoseBackfill() + for task in backgroundTasks { switch task { case is WKApplicationRefreshBackgroundTask: @@ -101,9 +108,9 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate { } // Main queue only - private(set) var lastContext: WatchContext? { + private(set) var activeContext: WatchContext? { didSet { - WKExtension.shared().rootUpdatableInterfaceController?.update(with: lastContext) + loopManager.activeContext = activeContext if WKExtension.shared().applicationState != .active { WKExtension.shared().scheduleSnapshotRefresh(withPreferredDate: Date(), userInfo: nil) { (_) in } @@ -137,12 +144,12 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate { context.preferredGlucoseUnit = units[type] DispatchQueue.main.async { - self.lastContext = context + self.activeContext = context } } } else { DispatchQueue.main.async { - self.lastContext = context + self.activeContext = context } } } @@ -192,8 +199,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..d70251fec0 --- /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().addingTimeInterval(TimeInterval(hours: -3)) + } + + static var StaleGlucoseCutoff: Date { + return Date().addingTimeInterval(-TimeInterval(minutes: 4.5)) + } +} diff --git a/WatchApp Extension/Extensions/GlucoseStore.swift b/WatchApp Extension/Extensions/GlucoseStore.swift new file mode 100644 index 0000000000..a71520a09f --- /dev/null +++ b/WatchApp Extension/Extensions/GlucoseStore.swift @@ -0,0 +1,25 @@ +// +// 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 { + func maybeRequestGlucoseBackfill() { + getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { samples in + let latestDate = samples.last?.startDate ?? .EarliestGlucoseCutoff + if latestDate < .StaleGlucoseCutoff { + let userInfo = GlucoseBackfillRequestUserInfo(startDate: latestDate) + WCSession.default.sendGlucoseBackfillRequestMessage(userInfo) { (context) in + self.addGlucose(context.samples) { _ in } + } + } + } + } +} diff --git a/WatchApp Extension/Extensions/NSUserDefaults.swift b/WatchApp Extension/Extensions/NSUserDefaults.swift index b45ba55feb..cddb16ca67 100644 --- a/WatchApp Extension/Extensions/NSUserDefaults.swift +++ b/WatchApp Extension/Extensions/NSUserDefaults.swift @@ -12,6 +12,16 @@ import Foundation extension UserDefaults { private enum Key: String { case ComplicationDataLastRefreshed = "com.loudnate.Naterade.ComplicationDataLastRefreshed" + 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) + } } var complicationDataLastRefreshed: Date { diff --git a/WatchApp Extension/Extensions/WCSession.swift b/WatchApp Extension/Extensions/WCSession.swift index 11c9114131..6b8240fd32 100644 --- a/WatchApp Extension/Extensions/WCSession.swift +++ b/WatchApp Extension/Extensions/WCSession.swift @@ -69,4 +69,20 @@ extension WCSession { errorHandler: errorHandler ) } + + func sendGlucoseBackfillRequestMessage(_ userInfo: GlucoseBackfillRequestUserInfo, successHandler: @escaping (WatchHistoricalGlucose) -> Void) { + // Backfill is optional so we ignore any errors + guard activationState == .activated, isReachable else { + return + } + + sendMessage(userInfo.rawValue, + replyHandler: { reply in + if let context = WatchHistoricalGlucose(rawValue: reply as WatchHistoricalGlucose.RawValue) { + successHandler(context) + } + }, + errorHandler: { reply in } + ) + } } diff --git a/WatchApp Extension/Info.plist b/WatchApp Extension/Info.plist index 3e7d85c8a2..1f19e9d8de 100644 --- a/WatchApp Extension/Info.plist +++ b/WatchApp Extension/Info.plist @@ -48,5 +48,7 @@ $(PRODUCT_MODULE_NAME).StatusInterfaceController WKExtensionDelegateClassName $(PRODUCT_MODULE_NAME).ExtensionDelegate + AppGroupIdentifier + $(APP_GROUP_IDENTIFIER) diff --git a/WatchApp Extension/Managers/LoopDataManager.swift b/WatchApp Extension/Managers/LoopDataManager.swift new file mode 100644 index 0000000000..2934e60848 --- /dev/null +++ b/WatchApp Extension/Managers/LoopDataManager.swift @@ -0,0 +1,31 @@ +// +// HealthKitManager.swift +// WatchApp Extension +// +// Created by Bharat Mediratta on 6/21/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation +import HealthKit +import LoopKit + +class LoopDataManager { + var glucoseStore: GlucoseStore + var activeContext: WatchContext? { + didSet { + NotificationCenter.default.post(name: .ContextUpdated, object: nil) + } + } + + init() { + glucoseStore = GlucoseStore( + healthStore: HKHealthStore(), + cacheStore: PersistenceController.controllerInAppGroupDirectory(), + cacheLength: .hours(24)) + } +} + +extension Notification.Name { + static let ContextUpdated = Notification.Name(rawValue: "com.loopkit.notification.ContextUpdated") +} diff --git a/WatchApp Extension/Managers/StatusChartsManager.swift b/WatchApp Extension/Managers/StatusChartsManager.swift new file mode 100644 index 0000000000..6b56edf203 --- /dev/null +++ b/WatchApp Extension/Managers/StatusChartsManager.swift @@ -0,0 +1,228 @@ +// +// StatusChartsManager.swift +// WatchApp Extension +// +// Created by Bharat Mediratta on 6/16/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation +import CoreGraphics +import UIKit +import HealthKit +import LoopKit + + +class StatusChartsManager { + var unit: HKUnit? + var targetRanges: [WatchDatedRange]? + var temporaryOverride: WatchDatedRange? + var historicalGlucose: [SampleValue]? + var predictedGlucose: [SampleValue]? + + func glucoseChart() -> UIImage? { + guard let unit = unit, let historicalGlucose = historicalGlucose, historicalGlucose.count > 0 else { + return nil + } + + // Choose the min/max values from across all of our data sources + var sampleValues = historicalGlucose.map { $0.quantity.doubleValue(for: unit) } + sampleValues += predictedGlucose?.map { $0.quantity.doubleValue(for: unit) } ?? [] + sampleValues += targetRanges?.map { [$0.maxValue, $0.minValue] }.flatMap { $0 } ?? [] + if let temporaryOverride = temporaryOverride { + sampleValues += [temporaryOverride.maxValue, temporaryOverride.minValue] + } + let bgMax = CGFloat(sampleValues.max()!) * 1.1 + let bgMin = CGFloat(sampleValues.min()!) * 0.75 + + let glucoseChartSize = CGSize(width: 270, height: 152) + let xMax = glucoseChartSize.width + let yMax = glucoseChartSize.height + let timeNow = CGFloat(Date().timeIntervalSince1970) + + let dateMax = predictedGlucose?.last?.startDate ?? Date().addingTimeInterval(TimeInterval(minutes: 180)) + let dateMin = historicalGlucose.first?.startDate ?? Date().addingTimeInterval(TimeInterval(minutes: -180)) + let timeMax = CGFloat(dateMax.timeIntervalSince1970) + let timeMin = CGFloat(dateMin.timeIntervalSince1970) + let yScale = yMax/(bgMax - bgMin) + let xScale = xMax/(timeMax - timeMin) + let xNow: CGFloat = xScale * (timeNow - timeMin) + let pointSize: CGFloat = 4 + // When we draw points, they are drawn in a rectangle specified + // by its corner coords, so often need to shift by half a point: + let halfPoint = pointSize / 2 + + var x: CGFloat = 0.0 + var y: CGFloat = 0.0 + + let pointColor = UIColor(red:158/255, green:215/255, blue:245/255, alpha:1) + // Target and override are the same, but with different alpha: + let rangeColor = UIColor(red:158/255, green:215/255, blue:245/255, alpha:0.4) + let overrideColor = UIColor(red:158/255, green:215/255, blue:245/255, alpha:0.6) + // Different color for main range(s) when override is active + let rangeOverridenColor = UIColor(red:158/255, green:215/255, blue:245/255, alpha:0.2) + let highColor = UIColor(red:158/255, green:158/255, blue:24/255, alpha:1) + let lowColor = UIColor(red:158/255, green:58/255, blue:24/255, alpha:1) + + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + + let attrs = [ + NSAttributedStringKey.font: UIFont(name: "HelveticaNeue", size: 20)!, + NSAttributedStringKey.paragraphStyle: paragraphStyle, + NSAttributedStringKey.foregroundColor: UIColor.white, + NSAttributedStringKey.backgroundColor: UIColor.black, + ] + + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .none + + let bgMaxLabel = numberFormatter.string(from: NSNumber(value: Double(bgMax)))! + let bgMinLabel = numberFormatter.string(from: NSNumber(value: Double(bgMin)))! + + UIGraphicsBeginImageContext(glucoseChartSize) + let imContext = UIGraphicsGetCurrentContext()! + + UIColor.darkGray.setStroke() + // Mark the current time with a dashed line: + imContext.setLineDash(phase: 1, lengths: [6, 6]) + imContext.setLineWidth(3) + imContext.strokeLineSegments(between: [CGPoint(x: xNow, y: 0), CGPoint(x: xNow, y: yMax - 1)]) + // Clear the dash pattern: + imContext.setLineDash(phase: 0, lengths:[]) + + // Set color for glucose points and target range: + pointColor.setFill() + + // Plot target ranges: + if let chartTargetRanges = targetRanges { + rangeColor.setFill() + + // Check for overrides first, since we will color the main + // range(s) differently depending on active override: + + // Override of target ranges. Overrides that have + // expired already can still show up here, so we need + // to check and only show if they are active: + if let override = temporaryOverride, override.endDate > Date() { + overrideColor.setFill() + + // Top left corner is start date and max value: + // Might be off the graph so keep it in: + var targetStart = CGFloat(override.startDate.timeIntervalSince1970) + // Only show the part of the override that is in the future: + if targetStart < timeNow { + targetStart = timeNow + } + var targetEnd = CGFloat(override.endDate.timeIntervalSince1970) + if targetEnd > timeMax { + targetEnd = timeMax + } + x = xScale * (targetStart - timeMin) + // Don't let end go off the chart: + let xEnd = xScale * (targetEnd - timeMin) + let rangeWidth = xEnd - x + y = yScale * (bgMax - CGFloat(override.maxValue)) + // Make sure range is at least a couple of pixels high: + let rangeHeight = max(yScale * (bgMax - CGFloat(override.minValue)) - y , 3) + + imContext.fill(CGRect(x: x, y: y, width: rangeWidth, height: rangeHeight)) + // To mimic the Loop interface, add a second box + // after this that reverts to original target color: + if targetEnd < timeMax { + rangeColor.setFill() + imContext.fill(CGRect(x: x+rangeWidth, y: y, width: xMax - (x+rangeWidth), height: rangeHeight)) + } + // Set a lighter color for main range(s) to follow: + rangeOverridenColor.setFill() + } + + // chartTargetRanges may be an array, so need to + // iterate over it and possibly plot a target change if needed: + + for targetRange in chartTargetRanges { + // Top left corner is start date and max value: + // Might be off the graph so keep it in: + var targetStart = CGFloat(targetRange.startDate.timeIntervalSince1970) + if targetStart < timeMin { + targetStart = timeMin + } + var targetEnd = CGFloat(targetRange.endDate.timeIntervalSince1970) + if targetEnd > timeMax { + targetEnd = timeMax + } + x = xScale * (targetStart - timeMin) + // Don't let end go off the chart: + let xEnd = xScale * (targetEnd - timeMin) + let rangeWidth = xEnd - x + y = yScale * (bgMax - CGFloat(targetRange.maxValue)) + // Make sure range is at least a couple of pixels high: + let rangeHeight = max(yScale * (bgMax - CGFloat(targetRange.minValue)) - y , 3) + + imContext.fill(CGRect(x: x, y: y, width: rangeWidth, height: rangeHeight)) + } + + } + + pointColor.setFill() + + // Draw the glucose points: + historicalGlucose.forEach { sample in + let bgFloat = CGFloat(sample.quantity.doubleValue(for: unit)) + x = xScale * (CGFloat(sample.startDate.timeIntervalSince1970) - timeMin) + y = yScale * (bgMax - bgFloat) + if bgFloat > bgMax { + // 'high' on graph is low y coords: + y = halfPoint + highColor.setFill() + } else if bgFloat < bgMin { + y = yMax - 2 + lowColor.setFill() + } else { + pointColor.setFill() + } + // Start by half a point width back to make + // rectangle centered on where we want point center: + imContext.fillEllipse(in: CGRect(x: x - halfPoint, y: y - halfPoint, width: pointSize, height: pointSize)) + } + + pointColor.setStroke() + imContext.setLineDash(phase: 11, lengths: [10, 6]) + imContext.setLineWidth(3) + // Create a path with the predicted glucose values: + imContext.beginPath() + var predictedPoints: [CGPoint] = [] + + if let predictedGlucose = predictedGlucose, predictedGlucose.count > 2 { + predictedGlucose.forEach { (sample) in + let bgFloat = CGFloat(sample.quantity.doubleValue(for: unit)) + x = xScale * (CGFloat(sample.startDate.timeIntervalSince1970) - timeMin) + y = yScale * (bgMax - bgFloat) + predictedPoints.append(CGPoint(x: x, y: y)) + } + + // Add points to the path, then draw it: + imContext.addLines(between: predictedPoints) + imContext.strokePath() + } + // Clear the dash pattern: + imContext.setLineDash(phase: 0, lengths:[]) + + // Put labels last so they are on top of text or points + // in case of overlap. + // Add a label for max BG on y axis + bgMaxLabel.draw(with: CGRect(x: 6, y: 4, width: 40, height: 40), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) + // Add a label for min BG on y axis + bgMinLabel.draw(with: CGRect(x: 6, y: yMax-28, width: 40, height: 40), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) + + let timeLabel = "+\(Int(dateMax.timeIntervalSinceNow.hours))h" + timeLabel.draw(with: CGRect(x: xMax - 50, y: 4, width: 40, height: 40), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) + + // Draw the box + UIColor.darkGray.setStroke() + imContext.stroke(CGRect(origin: CGPoint(x: 0, y: 0), size: glucoseChartSize)) + + return UIGraphicsGetImageFromCurrentImageContext() + } +} diff --git a/WatchApp Extension/WatchApp Extension.entitlements b/WatchApp Extension/WatchApp Extension.entitlements index e10f4302d5..8d88cb3139 100644 --- a/WatchApp Extension/WatchApp Extension.entitlements +++ b/WatchApp Extension/WatchApp Extension.entitlements @@ -4,5 +4,9 @@ com.apple.developer.healthkit + com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index f2b392d588..cf9be92c47 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,74 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -359,7 +423,7 @@ - + From 0e8f0f4894789c29be9651ae017f77131d64b6cb Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Fri, 29 Jun 2018 16:08:11 -0700 Subject: [PATCH 02/51] Fix filename mismatches in comments --- Common/Models/WatchDatedRange.swift | 2 +- Common/Models/WatchPredictedGlucose.swift | 2 +- WatchApp Extension/Managers/LoopDataManager.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Common/Models/WatchDatedRange.swift b/Common/Models/WatchDatedRange.swift index 01d3962524..855f22bd6e 100644 --- a/Common/Models/WatchDatedRange.swift +++ b/Common/Models/WatchDatedRange.swift @@ -1,5 +1,5 @@ // -// WatchDatedRangeContext.swift +// WatchDatedRange.swift // WatchApp Extension // // Created by Bharat Mediratta on 6/26/18. diff --git a/Common/Models/WatchPredictedGlucose.swift b/Common/Models/WatchPredictedGlucose.swift index 060105481b..1236a3a30d 100644 --- a/Common/Models/WatchPredictedGlucose.swift +++ b/Common/Models/WatchPredictedGlucose.swift @@ -1,5 +1,5 @@ // -// WatchPredictedGlucoseContext.swift +// WatchPredictedGlucose.swift // WatchApp Extension // // Created by Bharat Mediratta on 6/26/18. diff --git a/WatchApp Extension/Managers/LoopDataManager.swift b/WatchApp Extension/Managers/LoopDataManager.swift index 2934e60848..8564868047 100644 --- a/WatchApp Extension/Managers/LoopDataManager.swift +++ b/WatchApp Extension/Managers/LoopDataManager.swift @@ -1,5 +1,5 @@ // -// HealthKitManager.swift +// LoopDataManager.swift // WatchApp Extension // // Created by Bharat Mediratta on 6/21/18. From 6d1ee8d47aec11d8875dc08ea35063efac42e9a5 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Wed, 18 Jul 2018 18:27:54 -0700 Subject: [PATCH 03/51] Prototype of a SpriteKit-based glucose chart --- Loop.xcodeproj/project.pbxproj | 16 +- .../Controllers/ChartHUDController.swift | 38 ++- .../Managers/StatusChartsManager.swift | 228 ------------------ .../Scenes/GlucoseChartScene.swift | 137 +++++++++++ WatchApp/Base.lproj/Interface.storyboard | 21 +- 5 files changed, 186 insertions(+), 254 deletions(-) delete mode 100644 WatchApp Extension/Managers/StatusChartsManager.swift create mode 100644 WatchApp Extension/Scenes/GlucoseChartScene.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 6ea22bd89b..595058300e 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -246,6 +246,7 @@ 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 */; }; 4F7E8AC920E2AC3700AEA65E /* WatchDatedRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7E8AC820E2AC3700AEA65E /* WatchDatedRange.swift */; }; @@ -268,7 +269,6 @@ 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 */; }; - 4FE3475E20D5D77900A86D03 /* StatusChartsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE3475D20D5D77900A86D03 /* StatusChartsManager.swift */; }; 4FF0F75E20E1E5D100FC6291 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431E73471FF95A900069B5F7 /* PersistenceController.swift */; }; 4FF0F75F20E1E5EF00FC6291 /* NSBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430DA58D1D4AEC230097D1CA /* NSBundle.swift */; }; 4FF4D0F81E1725B000846527 /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434F54561D287FDB002A9274 /* NibLoadable.swift */; }; @@ -651,6 +651,7 @@ 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 = ""; }; 4F7E8AC820E2AC3700AEA65E /* WatchDatedRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchDatedRange.swift; sourceTree = ""; }; @@ -659,7 +660,6 @@ 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 = ""; }; - 4FE3475D20D5D77900A86D03 /* StatusChartsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusChartsManager.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 = ""; }; 7D68AAA91FE2DB0A00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; @@ -956,6 +956,7 @@ 4328E0121CFBE1B700E199AA /* Controllers */, 4328E01F1CFBE2B100E199AA /* Extensions */, 4FE3475F20D5D7FA00A86D03 /* Managers */, + 4F75F0052100146B00B5570E /* Scenes */, 43A943831B926B7B0051FA24 /* Supporting Files */, ); path = "WatchApp Extension"; @@ -1169,6 +1170,14 @@ path = Models; sourceTree = ""; }; + 4F75F0052100146B00B5570E /* Scenes */ = { + isa = PBXGroup; + children = ( + 4F75F00120FCFE8C00B5570E /* GlucoseChartScene.swift */, + ); + path = Scenes; + sourceTree = ""; + }; 4FB76FC31E8C575900B39636 /* Managers */ = { isa = PBXGroup; children = ( @@ -1190,7 +1199,6 @@ 4FE3475F20D5D7FA00A86D03 /* Managers */ = { isa = PBXGroup; children = ( - 4FE3475D20D5D77900A86D03 /* StatusChartsManager.swift */, 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */, ); path = Managers; @@ -1850,6 +1858,7 @@ 435400311C9F744E00D5819C /* BolusSuggestionUserInfo.swift in Sources */, 43A9438A1B926B7B0051FA24 /* NotificationController.swift in Sources */, 43A943881B926B7B0051FA24 /* ExtensionDelegate.swift in Sources */, + 4F75F00220FCFE8C00B5570E /* GlucoseChartScene.swift in Sources */, 4328E02F1CFBF81800E199AA /* WKInterfaceImage.swift in Sources */, 4F2C15811E0495B200E160D4 /* WatchContext+WatchApp.swift in Sources */, 4FF0F75F20E1E5EF00FC6291 /* NSBundle.swift in Sources */, @@ -1870,7 +1879,6 @@ 4328E01E1CFBE25F00E199AA /* AddCarbsInterfaceController.swift in Sources */, 4328E0261CFBE2C500E199AA /* IdentifiableClass.swift in Sources */, 4F73F5FC20E2E7FA00E8D82C /* GlucoseStore.swift in Sources */, - 4FE3475E20D5D77900A86D03 /* StatusChartsManager.swift in Sources */, 432CF87520D8AC950066B889 /* NSUserDefaults.swift in Sources */, 43027F0F1DFE0EC900C51989 /* HKUnit.swift in Sources */, 4344629220A7C19800C4BE6F /* ButtonGroup.swift in Sources */, diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 5f43069d96..3bae6fcee0 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -10,15 +10,15 @@ import WatchKit import WatchConnectivity import CGMBLEKit import LoopKit +import SpriteKit - -final class ChartHUDController: HUDInterfaceController { +final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { @IBOutlet weak var basalLabel: WKInterfaceLabel! @IBOutlet weak var iobLabel: WKInterfaceLabel! @IBOutlet weak var cobLabel: WKInterfaceLabel! - @IBOutlet weak var glucoseChart: WKInterfaceImage! + @IBOutlet weak var glucoseScene: WKInterfaceSKScene! - private var charts = StatusChartsManager() + private let scene = GlucoseChartScene() override init() { super.init() @@ -29,6 +29,8 @@ final class ChartHUDController: HUDInterfaceController { self.updateGlucoseChart() } } + + glucoseScene.presentScene(scene) } override func awake(withContext context: Any?) { @@ -37,6 +39,8 @@ final class ChartHUDController: HUDInterfaceController { // 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() } @@ -48,6 +52,9 @@ final class ChartHUDController: HUDInterfaceController { } override func willActivate() { + crownSequencer.delegate = self + crownSequencer.focus() + super.willActivate() loopManager?.glucoseStore.maybeRequestGlucoseBackfill() @@ -118,23 +125,28 @@ final class ChartHUDController: HUDInterfaceController { return } - charts.predictedGlucose = activeContext.predictedGlucose?.values - charts.targetRanges = activeContext.targetRanges - charts.temporaryOverride = activeContext.temporaryOverride - charts.unit = activeContext.preferredGlucoseUnit + scene.predictedGlucose = activeContext.predictedGlucose?.values + scene.targetRanges = activeContext.targetRanges + scene.temporaryOverride = activeContext.temporaryOverride + scene.unit = activeContext.preferredGlucoseUnit let updateGroup = DispatchGroup() updateGroup.enter() loopManager?.glucoseStore.getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { (samples) in - self.charts.historicalGlucose = samples + self.scene.historicalGlucose = samples updateGroup.leave() } _ = updateGroup.wait(timeout: .distantFuture) + } + + // MARK: WKCrownDelegate + var crownAccumulator = 0.0 - self.glucoseChart.setHidden(true) - if let chart = self.charts.glucoseChart() { - self.glucoseChart.setImage(chart) - self.glucoseChart.setHidden(false) + func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) { + crownAccumulator += rotationalDelta + if abs(crownAccumulator) >= 1.0 { + scene.visibleHours += 2 * Int(crownAccumulator) + crownAccumulator = 0 } } } diff --git a/WatchApp Extension/Managers/StatusChartsManager.swift b/WatchApp Extension/Managers/StatusChartsManager.swift deleted file mode 100644 index 6b56edf203..0000000000 --- a/WatchApp Extension/Managers/StatusChartsManager.swift +++ /dev/null @@ -1,228 +0,0 @@ -// -// StatusChartsManager.swift -// WatchApp Extension -// -// Created by Bharat Mediratta on 6/16/18. -// Copyright © 2018 LoopKit Authors. All rights reserved. -// - -import Foundation -import CoreGraphics -import UIKit -import HealthKit -import LoopKit - - -class StatusChartsManager { - var unit: HKUnit? - var targetRanges: [WatchDatedRange]? - var temporaryOverride: WatchDatedRange? - var historicalGlucose: [SampleValue]? - var predictedGlucose: [SampleValue]? - - func glucoseChart() -> UIImage? { - guard let unit = unit, let historicalGlucose = historicalGlucose, historicalGlucose.count > 0 else { - return nil - } - - // Choose the min/max values from across all of our data sources - var sampleValues = historicalGlucose.map { $0.quantity.doubleValue(for: unit) } - sampleValues += predictedGlucose?.map { $0.quantity.doubleValue(for: unit) } ?? [] - sampleValues += targetRanges?.map { [$0.maxValue, $0.minValue] }.flatMap { $0 } ?? [] - if let temporaryOverride = temporaryOverride { - sampleValues += [temporaryOverride.maxValue, temporaryOverride.minValue] - } - let bgMax = CGFloat(sampleValues.max()!) * 1.1 - let bgMin = CGFloat(sampleValues.min()!) * 0.75 - - let glucoseChartSize = CGSize(width: 270, height: 152) - let xMax = glucoseChartSize.width - let yMax = glucoseChartSize.height - let timeNow = CGFloat(Date().timeIntervalSince1970) - - let dateMax = predictedGlucose?.last?.startDate ?? Date().addingTimeInterval(TimeInterval(minutes: 180)) - let dateMin = historicalGlucose.first?.startDate ?? Date().addingTimeInterval(TimeInterval(minutes: -180)) - let timeMax = CGFloat(dateMax.timeIntervalSince1970) - let timeMin = CGFloat(dateMin.timeIntervalSince1970) - let yScale = yMax/(bgMax - bgMin) - let xScale = xMax/(timeMax - timeMin) - let xNow: CGFloat = xScale * (timeNow - timeMin) - let pointSize: CGFloat = 4 - // When we draw points, they are drawn in a rectangle specified - // by its corner coords, so often need to shift by half a point: - let halfPoint = pointSize / 2 - - var x: CGFloat = 0.0 - var y: CGFloat = 0.0 - - let pointColor = UIColor(red:158/255, green:215/255, blue:245/255, alpha:1) - // Target and override are the same, but with different alpha: - let rangeColor = UIColor(red:158/255, green:215/255, blue:245/255, alpha:0.4) - let overrideColor = UIColor(red:158/255, green:215/255, blue:245/255, alpha:0.6) - // Different color for main range(s) when override is active - let rangeOverridenColor = UIColor(red:158/255, green:215/255, blue:245/255, alpha:0.2) - let highColor = UIColor(red:158/255, green:158/255, blue:24/255, alpha:1) - let lowColor = UIColor(red:158/255, green:58/255, blue:24/255, alpha:1) - - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - - let attrs = [ - NSAttributedStringKey.font: UIFont(name: "HelveticaNeue", size: 20)!, - NSAttributedStringKey.paragraphStyle: paragraphStyle, - NSAttributedStringKey.foregroundColor: UIColor.white, - NSAttributedStringKey.backgroundColor: UIColor.black, - ] - - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = .none - - let bgMaxLabel = numberFormatter.string(from: NSNumber(value: Double(bgMax)))! - let bgMinLabel = numberFormatter.string(from: NSNumber(value: Double(bgMin)))! - - UIGraphicsBeginImageContext(glucoseChartSize) - let imContext = UIGraphicsGetCurrentContext()! - - UIColor.darkGray.setStroke() - // Mark the current time with a dashed line: - imContext.setLineDash(phase: 1, lengths: [6, 6]) - imContext.setLineWidth(3) - imContext.strokeLineSegments(between: [CGPoint(x: xNow, y: 0), CGPoint(x: xNow, y: yMax - 1)]) - // Clear the dash pattern: - imContext.setLineDash(phase: 0, lengths:[]) - - // Set color for glucose points and target range: - pointColor.setFill() - - // Plot target ranges: - if let chartTargetRanges = targetRanges { - rangeColor.setFill() - - // Check for overrides first, since we will color the main - // range(s) differently depending on active override: - - // Override of target ranges. Overrides that have - // expired already can still show up here, so we need - // to check and only show if they are active: - if let override = temporaryOverride, override.endDate > Date() { - overrideColor.setFill() - - // Top left corner is start date and max value: - // Might be off the graph so keep it in: - var targetStart = CGFloat(override.startDate.timeIntervalSince1970) - // Only show the part of the override that is in the future: - if targetStart < timeNow { - targetStart = timeNow - } - var targetEnd = CGFloat(override.endDate.timeIntervalSince1970) - if targetEnd > timeMax { - targetEnd = timeMax - } - x = xScale * (targetStart - timeMin) - // Don't let end go off the chart: - let xEnd = xScale * (targetEnd - timeMin) - let rangeWidth = xEnd - x - y = yScale * (bgMax - CGFloat(override.maxValue)) - // Make sure range is at least a couple of pixels high: - let rangeHeight = max(yScale * (bgMax - CGFloat(override.minValue)) - y , 3) - - imContext.fill(CGRect(x: x, y: y, width: rangeWidth, height: rangeHeight)) - // To mimic the Loop interface, add a second box - // after this that reverts to original target color: - if targetEnd < timeMax { - rangeColor.setFill() - imContext.fill(CGRect(x: x+rangeWidth, y: y, width: xMax - (x+rangeWidth), height: rangeHeight)) - } - // Set a lighter color for main range(s) to follow: - rangeOverridenColor.setFill() - } - - // chartTargetRanges may be an array, so need to - // iterate over it and possibly plot a target change if needed: - - for targetRange in chartTargetRanges { - // Top left corner is start date and max value: - // Might be off the graph so keep it in: - var targetStart = CGFloat(targetRange.startDate.timeIntervalSince1970) - if targetStart < timeMin { - targetStart = timeMin - } - var targetEnd = CGFloat(targetRange.endDate.timeIntervalSince1970) - if targetEnd > timeMax { - targetEnd = timeMax - } - x = xScale * (targetStart - timeMin) - // Don't let end go off the chart: - let xEnd = xScale * (targetEnd - timeMin) - let rangeWidth = xEnd - x - y = yScale * (bgMax - CGFloat(targetRange.maxValue)) - // Make sure range is at least a couple of pixels high: - let rangeHeight = max(yScale * (bgMax - CGFloat(targetRange.minValue)) - y , 3) - - imContext.fill(CGRect(x: x, y: y, width: rangeWidth, height: rangeHeight)) - } - - } - - pointColor.setFill() - - // Draw the glucose points: - historicalGlucose.forEach { sample in - let bgFloat = CGFloat(sample.quantity.doubleValue(for: unit)) - x = xScale * (CGFloat(sample.startDate.timeIntervalSince1970) - timeMin) - y = yScale * (bgMax - bgFloat) - if bgFloat > bgMax { - // 'high' on graph is low y coords: - y = halfPoint - highColor.setFill() - } else if bgFloat < bgMin { - y = yMax - 2 - lowColor.setFill() - } else { - pointColor.setFill() - } - // Start by half a point width back to make - // rectangle centered on where we want point center: - imContext.fillEllipse(in: CGRect(x: x - halfPoint, y: y - halfPoint, width: pointSize, height: pointSize)) - } - - pointColor.setStroke() - imContext.setLineDash(phase: 11, lengths: [10, 6]) - imContext.setLineWidth(3) - // Create a path with the predicted glucose values: - imContext.beginPath() - var predictedPoints: [CGPoint] = [] - - if let predictedGlucose = predictedGlucose, predictedGlucose.count > 2 { - predictedGlucose.forEach { (sample) in - let bgFloat = CGFloat(sample.quantity.doubleValue(for: unit)) - x = xScale * (CGFloat(sample.startDate.timeIntervalSince1970) - timeMin) - y = yScale * (bgMax - bgFloat) - predictedPoints.append(CGPoint(x: x, y: y)) - } - - // Add points to the path, then draw it: - imContext.addLines(between: predictedPoints) - imContext.strokePath() - } - // Clear the dash pattern: - imContext.setLineDash(phase: 0, lengths:[]) - - // Put labels last so they are on top of text or points - // in case of overlap. - // Add a label for max BG on y axis - bgMaxLabel.draw(with: CGRect(x: 6, y: 4, width: 40, height: 40), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) - // Add a label for min BG on y axis - bgMinLabel.draw(with: CGRect(x: 6, y: yMax-28, width: 40, height: 40), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) - - let timeLabel = "+\(Int(dateMax.timeIntervalSinceNow.hours))h" - timeLabel.draw(with: CGRect(x: xMax - 50, y: 4, width: 40, height: 40), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) - - // Draw the box - UIColor.darkGray.setStroke() - imContext.stroke(CGRect(origin: CGPoint(x: 0, y: 0), size: glucoseChartSize)) - - return UIGraphicsGetImageFromCurrentImageContext() - } -} diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift new file mode 100644 index 0000000000..488a4adabe --- /dev/null +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -0,0 +1,137 @@ +// +// GlucoseScene.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 + +// Stashing the extensions here for ease of development, but they should likely +// move into their own files as appropriate +extension UIColor { + static let glucoseTintColor = UIColor(red: 0 / 255, green: 176 / 255, blue: 255 / 255, alpha: 1) + static let gridColor = UIColor(white: 193 / 255, alpha: 1) + static let nowColor = UIColor(white: 193 / 255, alpha: 0.5) +} + +extension SampleValue { + var hashValue: Double { + return 17 * quantity.doubleValue(for: HKUnit.milligramsPerDeciliter) + + 23 * startDate.timeIntervalSince1970 + } +} + +extension SKLabelNode { + static func basic() -> SKLabelNode { + let basic = SKLabelNode(text: "--") + basic.fontSize = 12 + basic.fontName = "HelveticaNeue" + basic.fontColor = .white + basic.alpha = 0.8 + basic.verticalAlignmentMode = .top + basic.horizontalAlignmentMode = .left + return basic + } +} + +class GlucoseChartScene: SKScene { + var unit: HKUnit? + var targetRanges: [WatchDatedRange]? + var temporaryOverride: WatchDatedRange? + var historicalGlucose: [SampleValue]? + var predictedGlucose: [SampleValue]? + var visibleHours: Int = 2 { + didSet { + if visibleHours > 6 { + visibleHours = 6 + } else if visibleHours < 2 { + visibleHours = 2 + } else { + WKInterfaceDevice.current().play(.success) + nextUpdate = Date() + } + } + } + + private var nextUpdate = Date() + private var hoursLabel: SKLabelNode! + private var sampleNodes: [Double: SKShapeNode] = [:] + + override init() { + // Use the fixed sizes specified in the storyboard, based on our guess of the model size + var sceneSize: CGSize + if WKInterfaceDevice.current().screenBounds.width > 136 { + sceneSize = CGSize(width: 154, height: 110) + } else { + sceneSize = CGSize(width: 134, height: 110) + } + super.init(size: sceneSize) + + scaleMode = .aspectFit + backgroundColor = .clear + + let frame = SKShapeNode(rectOf: size, cornerRadius: 5) + frame.position = CGPoint(x: size.width / 2, y: size.height / 2) + frame.lineWidth = 2 + frame.fillColor = .clear + frame.strokeColor = .gridColor + addChild(frame) + + let dashedPath = CGPath(rect: CGRect(origin: CGPoint(x: size.width / 2, y: 0), size: CGSize(width: 0, height: size.height)), transform: nil).copy(dashingWithPhase: 0, lengths: [4.0, 3.0]) + let now = SKShapeNode(path: dashedPath) + now.strokeColor = .nowColor + addChild(now) + + hoursLabel = SKLabelNode.basic() + hoursLabel.position = CGPoint(x: 5, y: size.height - 5) + addChild(hoursLabel) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + override func update(_ currentTime: TimeInterval) { + guard let unit = unit, Date() >= nextUpdate else { + return + } + + let window = TimeInterval(hours: Double(visibleHours)) + let start = Date() - (window / 2) + let xScale = size.width / CGFloat(window) + let yScale = size.height / CGFloat(200 - 50) + + hoursLabel.text = "\(Int(visibleHours))h" + + var expiredNodes = sampleNodes + historicalGlucose?.filter { $0.startDate > start }.forEach { + let hashValue = $0.hashValue + if sampleNodes[hashValue] == nil { + let node = SKShapeNode(circleOfRadius: 1) + node.fillColor = .glucoseTintColor + node.strokeColor = .clear + node.position = position + sampleNodes[$0.hashValue] = node + addChild(node) + } + + sampleNodes[hashValue]!.position = + CGPoint(x: CGFloat($0.startDate.timeIntervalSince(start)) * xScale, + y: CGFloat($0.quantity.doubleValue(for: unit) - 50) * yScale) + expiredNodes.removeValue(forKey: hashValue) + } + + expiredNodes.forEach { + sampleNodes.removeValue(forKey: $0.key) + $0.value.removeFromParent() + } + + nextUpdate = Date() + TimeInterval(minutes: 1) + } +} diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index cf9be92c47..a35cf42d90 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -1,6 +1,6 @@ - + @@ -334,7 +334,7 @@ - + @@ -368,11 +368,6 @@ - - - - - + + + + + + + + - + - + From f9676a4116e28853113fa6082bc3092257f7e8ba Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Wed, 18 Jul 2018 20:52:09 -0700 Subject: [PATCH 04/51] Tweak the scene size for 42mm watches --- WatchApp Extension/Controllers/ChartHUDController.swift | 4 ++-- WatchApp Extension/Scenes/GlucoseChartScene.swift | 2 +- WatchApp/Base.lproj/Interface.storyboard | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 3bae6fcee0..4398a3e1ae 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -144,8 +144,8 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) { crownAccumulator += rotationalDelta - if abs(crownAccumulator) >= 1.0 { - scene.visibleHours += 2 * Int(crownAccumulator) + if abs(crownAccumulator) >= 0.5 { + scene.visibleHours += 2 * Int(sign(crownAccumulator)) crownAccumulator = 0 } } diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 488a4adabe..06f09e0436 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -67,7 +67,7 @@ class GlucoseChartScene: SKScene { // Use the fixed sizes specified in the storyboard, based on our guess of the model size var sceneSize: CGSize if WKInterfaceDevice.current().screenBounds.width > 136 { - sceneSize = CGSize(width: 154, height: 110) + sceneSize = CGSize(width: 154, height: 86) } else { sceneSize = CGSize(width: 134, height: 110) } diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index a35cf42d90..5690e844aa 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -382,7 +382,7 @@ - + From 1899045fd0eb07c3fdef419076a749495fa42441 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Fri, 20 Jul 2018 14:35:35 -0700 Subject: [PATCH 05/51] Add the rest of the chart components --- .../Controllers/ChartHUDController.swift | 12 +- .../Scenes/GlucoseChartScene.swift | 146 +++++++++++++----- 2 files changed, 111 insertions(+), 47 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 4398a3e1ae..50f6ba14a9 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -125,15 +125,15 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { return } - scene.predictedGlucose = activeContext.predictedGlucose?.values - scene.targetRanges = activeContext.targetRanges - scene.temporaryOverride = activeContext.temporaryOverride - scene.unit = activeContext.preferredGlucoseUnit + scene.data.predictedGlucose = activeContext.predictedGlucose?.values + scene.data.targetRanges = activeContext.targetRanges + scene.data.temporaryOverride = activeContext.temporaryOverride + scene.data.unit = activeContext.preferredGlucoseUnit let updateGroup = DispatchGroup() updateGroup.enter() loopManager?.glucoseStore.getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { (samples) in - self.scene.historicalGlucose = samples + self.scene.data.historicalGlucose = samples updateGroup.leave() } _ = updateGroup.wait(timeout: .distantFuture) @@ -145,7 +145,7 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) { crownAccumulator += rotationalDelta if abs(crownAccumulator) >= 0.5 { - scene.visibleHours += 2 * Int(sign(crownAccumulator)) + scene.visibleHours += Int(sign(crownAccumulator)) crownAccumulator = 0 } } diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 06f09e0436..611c7c4d0a 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -18,17 +18,11 @@ extension UIColor { static let glucoseTintColor = UIColor(red: 0 / 255, green: 176 / 255, blue: 255 / 255, alpha: 1) static let gridColor = UIColor(white: 193 / 255, alpha: 1) static let nowColor = UIColor(white: 193 / 255, alpha: 0.5) -} - -extension SampleValue { - var hashValue: Double { - return 17 * quantity.doubleValue(for: HKUnit.milligramsPerDeciliter) + - 23 * startDate.timeIntervalSince1970 - } + static let rangeColor = UIColor(red: 158/255, green: 215/255, blue: 245/255, alpha: 1) } extension SKLabelNode { - static func basic() -> SKLabelNode { + static func basic(at position: CGPoint) -> SKLabelNode { let basic = SKLabelNode(text: "--") basic.fontSize = 12 basic.fontName = "HelveticaNeue" @@ -36,32 +30,77 @@ extension SKLabelNode { basic.alpha = 0.8 basic.verticalAlignmentMode = .top basic.horizontalAlignmentMode = .left + basic.position = position return basic } } -class GlucoseChartScene: SKScene { +struct ChartData { var unit: HKUnit? - var targetRanges: [WatchDatedRange]? var temporaryOverride: WatchDatedRange? var historicalGlucose: [SampleValue]? var predictedGlucose: [SampleValue]? - var visibleHours: Int = 2 { + var targetRanges: [WatchDatedRange]? +} + +struct Scaler { + let start: Date + let bgMin: Double + let xScale: CGFloat + let yScale: CGFloat + + func point(_ x: Date, _ y: Double) -> CGPoint { + return CGPoint(x: CGFloat(x.timeIntervalSince(start)) * xScale, y: CGFloat(y - bgMin) * yScale) + } + + func rect(for range: WatchDatedRange) -> CGRect { + let a = point(range.startDate, range.minValue) + let b = point(range.endDate, range.maxValue) + return CGRect(origin: a, size: CGSize(width: b.x - a.x, height: b.y - a.y)) + } +} + +extension HKUnit { + var highWatermark: Double { + if unitString == "mg/dL" { + return 250.0 + } else { + return 14.0 + } + } + + var lowWatermark: Double { + if unitString == "mg/dL" { + return 50.0 + } else { + return 2.8 + } + } +} + +class GlucoseChartScene: SKScene { + var data: ChartData = ChartData() { didSet { - if visibleHours > 6 { - visibleHours = 6 - } else if visibleHours < 2 { - visibleHours = 2 - } else { + nextUpdate = Date() + } + } + + var visibleHours: Int = 1 { + didSet { + if (1...3).contains(visibleHours) { WKInterfaceDevice.current().play(.success) nextUpdate = Date() + } else { + visibleHours = max(0, min(3, visibleHours)) } } } private var nextUpdate = Date() + private var dataLayer: SKNode! private var hoursLabel: SKLabelNode! - private var sampleNodes: [Double: SKShapeNode] = [:] + private var maxBGLabel: SKLabelNode! + private var minBGLabel: SKLabelNode! override init() { // Use the fixed sizes specified in the storyboard, based on our guess of the model size @@ -73,6 +112,7 @@ class GlucoseChartScene: SKScene { } super.init(size: sceneSize) + anchorPoint = CGPoint(x: 0, y: 0) scaleMode = .aspectFit backgroundColor = .clear @@ -88,9 +128,20 @@ class GlucoseChartScene: SKScene { now.strokeColor = .nowColor addChild(now) - hoursLabel = SKLabelNode.basic() - hoursLabel.position = CGPoint(x: 5, y: size.height - 5) + hoursLabel = SKLabelNode.basic(at: CGPoint(x: 5, y: size.height - 5)) addChild(hoursLabel) + + maxBGLabel = SKLabelNode.basic(at: CGPoint(x: size.width - 5, y: size.height - 5)) + maxBGLabel.horizontalAlignmentMode = .right + addChild(maxBGLabel) + + minBGLabel = SKLabelNode.basic(at: CGPoint(x: size.width - 5, y: 5)) + minBGLabel.horizontalAlignmentMode = .right + minBGLabel.verticalAlignmentMode = .bottom + addChild(minBGLabel) + + dataLayer = SKNode() + addChild(dataLayer) } required init?(coder aDecoder: NSCoder) { @@ -98,38 +149,51 @@ class GlucoseChartScene: SKScene { } override func update(_ currentTime: TimeInterval) { - guard let unit = unit, Date() >= nextUpdate else { + guard let unit = data.unit, Date() >= nextUpdate else { return } let window = TimeInterval(hours: Double(visibleHours)) - let start = Date() - (window / 2) - let xScale = size.width / CGFloat(window) - let yScale = size.height / CGFloat(200 - 50) + let scaler = Scaler(start: Date() - window, + bgMin: unit.lowWatermark, + xScale: size.width / CGFloat(window * 2), + yScale: size.height / CGFloat(unit.highWatermark - unit.lowWatermark)) + + let numberFormatter = NumberFormatter.glucoseFormatter(for: unit) + minBGLabel.text = numberFormatter.string(from: unit.lowWatermark) + maxBGLabel.text = numberFormatter.string(from: unit.highWatermark) hoursLabel.text = "\(Int(visibleHours))h" - var expiredNodes = sampleNodes - historicalGlucose?.filter { $0.startDate > start }.forEach { - let hashValue = $0.hashValue - if sampleNodes[hashValue] == nil { - let node = SKShapeNode(circleOfRadius: 1) - node.fillColor = .glucoseTintColor - node.strokeColor = .clear - node.position = position - sampleNodes[$0.hashValue] = node - addChild(node) - } + dataLayer.removeAllChildren() + if let range = data.temporaryOverride { + let node = SKShapeNode(rect: scaler.rect(for: range)) + node.fillColor = UIColor.rangeColor.withAlphaComponent(0.4) + node.strokeColor = .clear + dataLayer.addChild(node) + } + + data.targetRanges?.enumerated().forEach { (i, range) in + let node = SKShapeNode(rect: scaler.rect(for: range)) + node.fillColor = UIColor.rangeColor.withAlphaComponent(data.temporaryOverride != nil ? 0.15 : 0.4) + node.strokeColor = .clear + dataLayer.addChild(node) + } - sampleNodes[hashValue]!.position = - CGPoint(x: CGFloat($0.startDate.timeIntervalSince(start)) * xScale, - y: CGFloat($0.quantity.doubleValue(for: unit) - 50) * yScale) - expiredNodes.removeValue(forKey: hashValue) + data.historicalGlucose?.filter { $0.startDate > scaler.start }.forEach { + let node = SKShapeNode(circleOfRadius: 1) + node.fillColor = .glucoseTintColor + node.strokeColor = .glucoseTintColor + node.position = scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) + dataLayer.addChild(node) } - expiredNodes.forEach { - sampleNodes.removeValue(forKey: $0.key) - $0.value.removeFromParent() + if let predictedGlucose = data.predictedGlucose, predictedGlucose.count > 2 { + let predictedPath = CGMutablePath() + predictedPath.addLines(between: predictedGlucose.map { + scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) + }) + dataLayer.addChild(SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3]))) } nextUpdate = Date() + TimeInterval(minutes: 1) From 1f681c01b6dd310f7782b57703294c961462e023 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Fri, 20 Jul 2018 14:44:21 -0700 Subject: [PATCH 06/51] Set high watermark higher for mg/dl --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 611c7c4d0a..aaa741f679 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -63,7 +63,7 @@ struct Scaler { extension HKUnit { var highWatermark: Double { if unitString == "mg/dL" { - return 250.0 + return 400.0 } else { return 14.0 } From fcdd4f6a07292aa68fa6305ee84aa9c683cfb0f1 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Fri, 20 Jul 2018 16:11:02 -0700 Subject: [PATCH 07/51] Clean up pause/unpause semantics and set the max hours to 4 --- .../Scenes/GlucoseChartScene.swift | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index aaa741f679..3661fae672 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -81,22 +81,22 @@ extension HKUnit { class GlucoseChartScene: SKScene { var data: ChartData = ChartData() { didSet { - nextUpdate = Date() + isPaused = false } } var visibleHours: Int = 1 { didSet { - if (1...3).contains(visibleHours) { + if (1...4).contains(visibleHours) { WKInterfaceDevice.current().play(.success) - nextUpdate = Date() + isPaused = false } else { - visibleHours = max(0, min(3, visibleHours)) + visibleHours = max(0, min(4, visibleHours)) } } } - private var nextUpdate = Date() + private var timer: Timer? private var dataLayer: SKNode! private var hoursLabel: SKLabelNode! private var maxBGLabel: SKLabelNode! @@ -142,6 +142,11 @@ class GlucoseChartScene: SKScene { dataLayer = SKNode() addChild(dataLayer) + + // Force an update once a minute + Timer.scheduledTimer(withTimeInterval: TimeInterval(minutes: 1), repeats: true) { _ in + self.isPaused = false + } } required init?(coder aDecoder: NSCoder) { @@ -149,7 +154,7 @@ class GlucoseChartScene: SKScene { } override func update(_ currentTime: TimeInterval) { - guard let unit = data.unit, Date() >= nextUpdate else { + guard let unit = data.unit else { return } @@ -196,6 +201,6 @@ class GlucoseChartScene: SKScene { dataLayer.addChild(SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3]))) } - nextUpdate = Date() + TimeInterval(minutes: 1) + isPaused = true } } From 6add1cef5b0bb9fc011b4cdeaca796848655f9a0 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Fri, 20 Jul 2018 20:39:55 -0700 Subject: [PATCH 08/51] The crown now adjusts the height of the graph --- .../Controllers/ChartHUDController.swift | 2 +- .../Scenes/GlucoseChartScene.swift | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 50f6ba14a9..0c594f8150 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -145,7 +145,7 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) { crownAccumulator += rotationalDelta if abs(crownAccumulator) >= 0.5 { - scene.visibleHours += Int(sign(crownAccumulator)) + scene.visibleBg += Int(sign(crownAccumulator)) crownAccumulator = 0 } } diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 3661fae672..e11de3070b 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -61,11 +61,11 @@ struct Scaler { } extension HKUnit { - var highWatermark: Double { + var highWatermarkRange: [Double] { if unitString == "mg/dL" { - return 400.0 + return [150.0, 200.0, 250.0, 300.0, 350.0, 400.0] } else { - return 14.0 + return [8.3, 11.1, 13.9, 16.6, 19.4, 22.2] } } @@ -81,22 +81,24 @@ extension HKUnit { class GlucoseChartScene: SKScene { var data: ChartData = ChartData() { didSet { - isPaused = false + needsUpdate = true } } - var visibleHours: Int = 1 { + var visibleBg: Int = 1 { didSet { - if (1...4).contains(visibleHours) { + if let range = data.unit?.highWatermarkRange, (0.. Date: Sat, 21 Jul 2018 10:27:05 -0700 Subject: [PATCH 09/51] Fix override range rendering to match the Loop app ui --- .../Scenes/GlucoseChartScene.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index e11de3070b..a6215189e8 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -156,7 +156,7 @@ class GlucoseChartScene: SKScene { } override func update(_ currentTime: TimeInterval) { - guard let unit = data.unit, needsUpdate == true else { + guard let unit = data.unit, needsUpdate else { return } @@ -174,15 +174,22 @@ class GlucoseChartScene: SKScene { dataLayer.removeAllChildren() if let range = data.temporaryOverride { - let node = SKShapeNode(rect: scaler.rect(for: range)) - node.fillColor = UIColor.rangeColor.withAlphaComponent(0.4) - node.strokeColor = .clear - dataLayer.addChild(node) + var rect = scaler.rect(for: range) + let node1 = SKShapeNode(rect: rect) + node1.fillColor = UIColor.rangeColor.withAlphaComponent(0.2) + node1.strokeColor = .clear + dataLayer.addChild(node1) + + rect.size.width = size.width + let node2 = SKShapeNode(rect: rect) + node2.fillColor = UIColor.rangeColor.withAlphaComponent(0.2) + node2.strokeColor = .clear + dataLayer.addChild(node2) } data.targetRanges?.enumerated().forEach { (i, range) in let node = SKShapeNode(rect: scaler.rect(for: range)) - node.fillColor = UIColor.rangeColor.withAlphaComponent(data.temporaryOverride != nil ? 0.15 : 0.4) + node.fillColor = UIColor.rangeColor.withAlphaComponent(data.temporaryOverride != nil ? 0.2 : 0.4) node.strokeColor = .clear dataLayer.addChild(node) } From 1bb0697a6a98806cf29262a685362a813ed878d8 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Sat, 21 Jul 2018 14:32:12 -0700 Subject: [PATCH 10/51] Small optimizations --- .../Scenes/GlucoseChartScene.swift | 32 ++++++++++--------- WatchApp/Base.lproj/Interface.storyboard | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index a6215189e8..3b6205557a 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -35,6 +35,15 @@ extension SKLabelNode { } } +extension SKShapeNode { + static func basic(color: UIColor, rect: CGRect) -> SKShapeNode { + let node = SKShapeNode(rect: rect) + node.fillColor = color + node.strokeColor = .clear + return node + } +} + struct ChartData { var unit: HKUnit? var temporaryOverride: WatchDatedRange? @@ -173,25 +182,18 @@ class GlucoseChartScene: SKScene { hoursLabel.text = "\(Int(visibleHours))h" dataLayer.removeAllChildren() + data.targetRanges?.enumerated().forEach { (i, range) in + let color = UIColor.rangeColor.withAlphaComponent(data.temporaryOverride != nil ? 0.2 : 0.4) + dataLayer.addChild(SKShapeNode.basic(color: color, rect: scaler.rect(for: range))) + } + if let range = data.temporaryOverride { + let color = UIColor.rangeColor.withAlphaComponent(0.2) var rect = scaler.rect(for: range) - let node1 = SKShapeNode(rect: rect) - node1.fillColor = UIColor.rangeColor.withAlphaComponent(0.2) - node1.strokeColor = .clear - dataLayer.addChild(node1) + dataLayer.addChild(SKShapeNode.basic(color: color, rect: rect)) rect.size.width = size.width - let node2 = SKShapeNode(rect: rect) - node2.fillColor = UIColor.rangeColor.withAlphaComponent(0.2) - node2.strokeColor = .clear - dataLayer.addChild(node2) - } - - data.targetRanges?.enumerated().forEach { (i, range) in - let node = SKShapeNode(rect: scaler.rect(for: range)) - node.fillColor = UIColor.rangeColor.withAlphaComponent(data.temporaryOverride != nil ? 0.2 : 0.4) - node.strokeColor = .clear - dataLayer.addChild(node) + dataLayer.addChild(SKShapeNode.basic(color: color, rect: rect)) } data.historicalGlucose?.filter { $0.startDate > scaler.start }.forEach { diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index 5690e844aa..ba5f3b68f5 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -380,7 +380,7 @@ - + From 8c822b5cbd1b54e3c3039d7b857faff074d6df0f Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Sat, 21 Jul 2018 15:53:33 -0700 Subject: [PATCH 11/51] Clean up the rendering code to pause between data changes --- .../Controllers/ChartHUDController.swift | 14 +++--- .../Scenes/GlucoseChartScene.swift | 45 +++++++++---------- WatchApp/Base.lproj/Interface.storyboard | 2 +- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 0c594f8150..eaaf1916bc 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -125,18 +125,20 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { return } - scene.data.predictedGlucose = activeContext.predictedGlucose?.values - scene.data.targetRanges = activeContext.targetRanges - scene.data.temporaryOverride = activeContext.temporaryOverride - scene.data.unit = activeContext.preferredGlucoseUnit + scene.predictedGlucose = activeContext.predictedGlucose?.values + scene.targetRanges = activeContext.targetRanges + scene.temporaryOverride = activeContext.temporaryOverride + scene.unit = activeContext.preferredGlucoseUnit let updateGroup = DispatchGroup() updateGroup.enter() loopManager?.glucoseStore.getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { (samples) in - self.scene.data.historicalGlucose = samples + self.scene.historicalGlucose = samples updateGroup.leave() } _ = updateGroup.wait(timeout: .distantFuture) + + scene.updateNodes() } // MARK: WKCrownDelegate @@ -144,7 +146,7 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) { crownAccumulator += rotationalDelta - if abs(crownAccumulator) >= 0.5 { + if abs(crownAccumulator) >= 0.25 { scene.visibleBg += Int(sign(crownAccumulator)) crownAccumulator = 0 } diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 3b6205557a..1ccf6c8eed 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -44,14 +44,6 @@ extension SKShapeNode { } } -struct ChartData { - var unit: HKUnit? - var temporaryOverride: WatchDatedRange? - var historicalGlucose: [SampleValue]? - var predictedGlucose: [SampleValue]? - var targetRanges: [WatchDatedRange]? -} - struct Scaler { let start: Date let bgMin: Double @@ -88,17 +80,17 @@ extension HKUnit { } class GlucoseChartScene: SKScene { - var data: ChartData = ChartData() { - didSet { - needsUpdate = true - } - } + var unit: HKUnit? + var temporaryOverride: WatchDatedRange? + var historicalGlucose: [SampleValue]? + var predictedGlucose: [SampleValue]? + var targetRanges: [WatchDatedRange]? var visibleBg: Int = 1 { didSet { - if let range = data.unit?.highWatermarkRange, (0.. scaler.start }.forEach { + historicalGlucose?.filter { $0.startDate > scaler.start }.forEach { let node = SKShapeNode(circleOfRadius: 1) node.fillColor = .glucoseTintColor node.strokeColor = .glucoseTintColor @@ -204,7 +203,7 @@ class GlucoseChartScene: SKScene { dataLayer.addChild(node) } - if let predictedGlucose = data.predictedGlucose, predictedGlucose.count > 2 { + if let predictedGlucose = predictedGlucose, predictedGlucose.count > 2 { let predictedPath = CGMutablePath() predictedPath.addLines(between: predictedGlucose.map { scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) @@ -212,6 +211,6 @@ class GlucoseChartScene: SKScene { dataLayer.addChild(SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3]))) } - needsUpdate = false + isPaused = false } } diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index ba5f3b68f5..5690e844aa 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -380,7 +380,7 @@ - + From af72a096c9ef9af2ff24a8c10b2687559614fdf2 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Sat, 21 Jul 2018 15:54:23 -0700 Subject: [PATCH 12/51] Remove debug code --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 1ccf6c8eed..d53ce028e3 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -157,7 +157,6 @@ class GlucoseChartScene: SKScene { } override func update(_ currentTime: TimeInterval) { - print("Update \(currentTime)") DispatchQueue.main.async { self.isPaused = true } From 5eda3829e17362cd619c3c0b6ec2059985e43543 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Sat, 21 Jul 2018 18:01:49 -0700 Subject: [PATCH 13/51] Highlight the upper BG when it changes --- .../Controllers/ChartHUDController.swift | 2 +- WatchApp Extension/Scenes/GlucoseChartScene.swift | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index eaaf1916bc..c4fecddd55 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -138,7 +138,7 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { } _ = updateGroup.wait(timeout: .distantFuture) - scene.updateNodes() + scene.updateData() } // MARK: WKCrownDelegate diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index d53ce028e3..48a30bbe3d 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -90,7 +90,10 @@ class GlucoseChartScene: SKScene { didSet { if let range = unit?.highWatermarkRange, (0.. Date: Sun, 22 Jul 2018 16:05:32 -0700 Subject: [PATCH 14/51] Make updateGlucoseChart async to avoid hitting CPU limits --- .../Controllers/ChartHUDController.swift | 16 +++------ .../Scenes/GlucoseChartScene.swift | 33 +++++++++++-------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index c4fecddd55..153354334e 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -125,20 +125,14 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { return } - scene.predictedGlucose = activeContext.predictedGlucose?.values - scene.targetRanges = activeContext.targetRanges - scene.temporaryOverride = activeContext.temporaryOverride - scene.unit = activeContext.preferredGlucoseUnit + scene.data.predictedGlucose = activeContext.predictedGlucose?.values + scene.data.targetRanges = activeContext.targetRanges + scene.data.temporaryOverride = activeContext.temporaryOverride + scene.data.unit = activeContext.preferredGlucoseUnit - let updateGroup = DispatchGroup() - updateGroup.enter() loopManager?.glucoseStore.getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { (samples) in - self.scene.historicalGlucose = samples - updateGroup.leave() + self.scene.data.historicalGlucose = samples } - _ = updateGroup.wait(timeout: .distantFuture) - - scene.updateData() } // MARK: WKCrownDelegate diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 48a30bbe3d..9c05413cae 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -79,21 +79,29 @@ extension HKUnit { } } -class GlucoseChartScene: SKScene { +struct ChartData { var unit: HKUnit? var temporaryOverride: WatchDatedRange? var historicalGlucose: [SampleValue]? var predictedGlucose: [SampleValue]? var targetRanges: [WatchDatedRange]? +} + +class GlucoseChartScene: SKScene { + var data = ChartData() { + didSet { + updateNodes() + } + } var visibleBg: Int = 1 { didSet { - if let range = unit?.highWatermarkRange, (0.. scaler.start }.forEach { + data.historicalGlucose?.filter { $0.startDate > scaler.start }.forEach { let node = SKShapeNode(circleOfRadius: 1) node.fillColor = .glucoseTintColor node.strokeColor = .glucoseTintColor @@ -207,7 +214,7 @@ class GlucoseChartScene: SKScene { dataLayer.addChild(node) } - if let predictedGlucose = predictedGlucose, predictedGlucose.count > 2 { + if let predictedGlucose = data.predictedGlucose, predictedGlucose.count > 2 { let predictedPath = CGMutablePath() predictedPath.addLines(between: predictedGlucose.map { scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) From 8ee3df89e3ff3ca35521e12173032f212fa93099 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Mon, 23 Jul 2018 11:55:50 -0700 Subject: [PATCH 15/51] More stability fixes & remove graph corner radius --- .../Controllers/ChartHUDController.swift | 11 +++++---- .../Scenes/GlucoseChartScene.swift | 24 +++++++------------ WatchApp/Base.lproj/Interface.storyboard | 2 +- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 153354334e..bcc58bf07a 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -125,13 +125,14 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { return } - scene.data.predictedGlucose = activeContext.predictedGlucose?.values - scene.data.targetRanges = activeContext.targetRanges - scene.data.temporaryOverride = activeContext.temporaryOverride - scene.data.unit = activeContext.preferredGlucoseUnit + scene.predictedGlucose = activeContext.predictedGlucose?.values + scene.targetRanges = activeContext.targetRanges + scene.temporaryOverride = activeContext.temporaryOverride + scene.unit = activeContext.preferredGlucoseUnit loopManager?.glucoseStore.getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { (samples) in - self.scene.data.historicalGlucose = samples + self.scene.historicalGlucose = samples + self.scene.updateNodes() } } diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 9c05413cae..366e8265ce 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -79,24 +79,16 @@ extension HKUnit { } } -struct ChartData { +class GlucoseChartScene: SKScene { var unit: HKUnit? var temporaryOverride: WatchDatedRange? var historicalGlucose: [SampleValue]? var predictedGlucose: [SampleValue]? var targetRanges: [WatchDatedRange]? -} - -class GlucoseChartScene: SKScene { - var data = ChartData() { - didSet { - updateNodes() - } - } var visibleBg: Int = 1 { didSet { - if let range = data.unit?.highWatermarkRange, (0.. scaler.start }.forEach { + historicalGlucose?.filter { $0.startDate > scaler.start }.forEach { let node = SKShapeNode(circleOfRadius: 1) node.fillColor = .glucoseTintColor node.strokeColor = .glucoseTintColor @@ -214,7 +206,7 @@ class GlucoseChartScene: SKScene { dataLayer.addChild(node) } - if let predictedGlucose = data.predictedGlucose, predictedGlucose.count > 2 { + if let predictedGlucose = predictedGlucose, predictedGlucose.count > 2 { let predictedPath = CGMutablePath() predictedPath.addLines(between: predictedGlucose.map { scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index 5690e844aa..4bdfafccbc 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -378,7 +378,7 @@ - + From 10069602dea558869090d4a0ccb864e713ade262 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Mon, 23 Jul 2018 12:27:24 -0700 Subject: [PATCH 16/51] Force updateNode() to run on the main queue to avoid contention --- WatchApp Extension/Controllers/ChartHUDController.swift | 6 ++++-- WatchApp Extension/Scenes/GlucoseChartScene.swift | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index bcc58bf07a..db486a6cb8 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -131,8 +131,10 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { scene.unit = activeContext.preferredGlucoseUnit loopManager?.glucoseStore.getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { (samples) in - self.scene.historicalGlucose = samples - self.scene.updateNodes() + DispatchQueue.main.async { + self.scene.historicalGlucose = samples + self.scene.updateNodes() + } } } diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 366e8265ce..a7949c5a24 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -108,6 +108,8 @@ class GlucoseChartScene: SKScene { private var minBGLabel: SKLabelNode! override init() { + dispatchPrecondition(condition: .onQueue(.main)) + // Use the fixed sizes specified in the storyboard, based on our guess of the model size var sceneSize: CGSize if WKInterfaceDevice.current().screenBounds.width > 136 { From 1f535e5bd47dcde159c5e014f820df9060422851 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Tue, 24 Jul 2018 17:51:05 -0700 Subject: [PATCH 17/51] Improve watch size detection code --- .../Scenes/GlucoseChartScene.swift | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index a7949c5a24..d2f8385f0a 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -45,13 +45,13 @@ extension SKShapeNode { } struct Scaler { - let start: Date - let bgMin: Double + let startDate: Date + let glucoseMin: Double let xScale: CGFloat let yScale: CGFloat func point(_ x: Date, _ y: Double) -> CGPoint { - return CGPoint(x: CGFloat(x.timeIntervalSince(start)) * xScale, y: CGFloat(y - bgMin) * yScale) + return CGPoint(x: CGFloat(x.timeIntervalSince(startDate)) * xScale, y: CGFloat(y - glucoseMin) * yScale) } func rect(for range: WatchDatedRange) -> CGRect { @@ -79,6 +79,22 @@ extension HKUnit { } } +extension WKInterfaceDevice { + enum WatchSize { + case Watch38mm + case Watch42mm + } + + func watchSize() -> WatchSize { + switch screenBounds.width { + case 136: + return .Watch38mm + default: + return .Watch42mm + } + } +} + class GlucoseChartScene: SKScene { var unit: HKUnit? var temporaryOverride: WatchDatedRange? @@ -108,16 +124,15 @@ class GlucoseChartScene: SKScene { private var minBGLabel: SKLabelNode! override init() { - dispatchPrecondition(condition: .onQueue(.main)) - // Use the fixed sizes specified in the storyboard, based on our guess of the model size - var sceneSize: CGSize - if WKInterfaceDevice.current().screenBounds.width > 136 { - sceneSize = CGSize(width: 154, height: 86) - } else { - sceneSize = CGSize(width: 134, height: 110) - } - super.init(size: sceneSize) + super.init(size: { + switch WKInterfaceDevice.current().watchSize() { + case .Watch38mm: + return CGSize(width: 134, height: 110) + case .Watch42mm: + return CGSize(width: 154, height: 86) + } + }()) anchorPoint = CGPoint(x: 0, y: 0) scaleMode = .aspectFit @@ -169,13 +184,15 @@ class GlucoseChartScene: SKScene { } func updateNodes() { + dispatchPrecondition(condition: .onQueue(.main)) + guard let unit = unit else { return } let window = TimeInterval(hours: Double(visibleHours)) - let scaler = Scaler(start: Date() - window, - bgMin: unit.lowWatermark, + let scaler = Scaler(startDate: Date() - window, + glucoseMin: unit.lowWatermark, xScale: size.width / CGFloat(window * 2), yScale: size.height / CGFloat(unit.highWatermarkRange[visibleBg] - unit.lowWatermark)) @@ -200,7 +217,7 @@ class GlucoseChartScene: SKScene { dataLayer.addChild(SKShapeNode.basic(color: color, rect: rect)) } - historicalGlucose?.filter { $0.startDate > scaler.start }.forEach { + historicalGlucose?.filter { $0.startDate > scaler.startDate }.forEach { let node = SKShapeNode(circleOfRadius: 1) node.fillColor = .glucoseTintColor node.strokeColor = .glucoseTintColor From 334ae6a99e55558ab71653ce0eeafde6dbdda908 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Wed, 25 Jul 2018 15:43:57 -0700 Subject: [PATCH 18/51] Switch to the more efficient SpriteNode --- .../Scenes/GlucoseChartScene.swift | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index d2f8385f0a..b7557b4d66 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -35,11 +35,10 @@ extension SKLabelNode { } } -extension SKShapeNode { - static func basic(color: UIColor, rect: CGRect) -> SKShapeNode { - let node = SKShapeNode(rect: rect) - node.fillColor = color - node.strokeColor = .clear +extension SKSpriteNode { + static func basic(color: UIColor, rect: CGRect) -> SKSpriteNode { + let node = SKSpriteNode(color: color, size: rect.size) + node.position = rect.origin return node } } @@ -54,10 +53,11 @@ struct Scaler { return CGPoint(x: CGFloat(x.timeIntervalSince(startDate)) * xScale, y: CGFloat(y - glucoseMin) * yScale) } - func rect(for range: WatchDatedRange) -> CGRect { + func centerRect(for range: WatchDatedRange) -> CGRect { let a = point(range.startDate, range.minValue) let b = point(range.endDate, range.maxValue) - return CGRect(origin: a, size: CGSize(width: b.x - a.x, height: b.y - a.y)) + let size = CGSize(width: b.x - a.x, height: b.y - a.y) + return CGRect(origin: CGPoint(x: a.x + size.width / 2, y: a.y + size.height / 2), size: size) } } @@ -205,22 +205,20 @@ class GlucoseChartScene: SKScene { dataLayer.removeAllChildren() targetRanges?.enumerated().forEach { (i, range) in let color = UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.2 : 0.4) - dataLayer.addChild(SKShapeNode.basic(color: color, rect: scaler.rect(for: range))) + dataLayer.addChild(SKSpriteNode.basic(color: color, rect: scaler.centerRect(for: range))) } if let range = temporaryOverride { let color = UIColor.rangeColor.withAlphaComponent(0.2) - var rect = scaler.rect(for: range) - dataLayer.addChild(SKShapeNode.basic(color: color, rect: rect)) + var rect = scaler.centerRect(for: range) + dataLayer.addChild(SKSpriteNode.basic(color: color, rect: rect)) rect.size.width = size.width - dataLayer.addChild(SKShapeNode.basic(color: color, rect: rect)) + dataLayer.addChild(SKSpriteNode.basic(color: color, rect: rect)) } historicalGlucose?.filter { $0.startDate > scaler.startDate }.forEach { - let node = SKShapeNode(circleOfRadius: 1) - node.fillColor = .glucoseTintColor - node.strokeColor = .glucoseTintColor + let node = SKSpriteNode(color: .glucoseTintColor, size: CGSize(width: 2, height: 2)) node.position = scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) dataLayer.addChild(node) } From 9d714e7488fb1d7642f4544f24e82ebef98aff13 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Wed, 25 Jul 2018 16:47:12 -0700 Subject: [PATCH 19/51] Only cache 4 hours of glucose history --- WatchApp Extension/Managers/LoopDataManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WatchApp Extension/Managers/LoopDataManager.swift b/WatchApp Extension/Managers/LoopDataManager.swift index 8564868047..45a34b59f7 100644 --- a/WatchApp Extension/Managers/LoopDataManager.swift +++ b/WatchApp Extension/Managers/LoopDataManager.swift @@ -22,7 +22,7 @@ class LoopDataManager { glucoseStore = GlucoseStore( healthStore: HKHealthStore(), cacheStore: PersistenceController.controllerInAppGroupDirectory(), - cacheLength: .hours(24)) + cacheLength: .hours(4)) } } From deeddb21e066f760a9dac23ac6c0b3807b097676 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Wed, 25 Jul 2018 17:21:32 -0700 Subject: [PATCH 20/51] Remove pause/unpause code- it doesn't save significant CPU --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index b7557b4d66..96ea984d83 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -108,7 +108,7 @@ class GlucoseChartScene: SKScene { WKInterfaceDevice.current().play(.success) maxBGLabel.setScale(2.0) - maxBGLabel.run(SKAction.scale(to: 1.0, duration: 1.0), withKey: "highlight") + maxBGLabel.run(SKAction.scale(to: 1.0, duration: 1.0)) updateNodes() } else { visibleBg = oldValue @@ -175,14 +175,6 @@ class GlucoseChartScene: SKScene { super.init(coder: aDecoder) } - override func update(_ currentTime: TimeInterval) { - if maxBGLabel.action(forKey: "highlight") == nil { - DispatchQueue.main.async { - self.isPaused = true - } - } - } - func updateNodes() { dispatchPrecondition(condition: .onQueue(.main)) @@ -230,7 +222,5 @@ class GlucoseChartScene: SKScene { }) dataLayer.addChild(SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3]))) } - - isPaused = false } } From 8a39819508c9f312c8c30147b822a5d9f978cc93 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Wed, 25 Jul 2018 17:47:25 -0700 Subject: [PATCH 21/51] Try unpausing the WKInterfaceSKScene to get the animation to stop freezing --- WatchApp Extension/Controllers/ChartHUDController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index db486a6cb8..f08917fd3f 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -58,6 +58,7 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { super.willActivate() loopManager?.glucoseStore.maybeRequestGlucoseBackfill() + glucoseScene.isPaused = false } override func update() { From abd224932b5dae029394937a06dcc89d6b470f06 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Thu, 26 Jul 2018 09:32:59 -0700 Subject: [PATCH 22/51] Revert "Remove pause/unpause code- it doesn't save significant CPU" This reverts commit deeddb21e066f760a9dac23ac6c0b3807b097676. --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 96ea984d83..b7557b4d66 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -108,7 +108,7 @@ class GlucoseChartScene: SKScene { WKInterfaceDevice.current().play(.success) maxBGLabel.setScale(2.0) - maxBGLabel.run(SKAction.scale(to: 1.0, duration: 1.0)) + maxBGLabel.run(SKAction.scale(to: 1.0, duration: 1.0), withKey: "highlight") updateNodes() } else { visibleBg = oldValue @@ -175,6 +175,14 @@ class GlucoseChartScene: SKScene { super.init(coder: aDecoder) } + override func update(_ currentTime: TimeInterval) { + if maxBGLabel.action(forKey: "highlight") == nil { + DispatchQueue.main.async { + self.isPaused = true + } + } + } + func updateNodes() { dispatchPrecondition(condition: .onQueue(.main)) @@ -222,5 +230,7 @@ class GlucoseChartScene: SKScene { }) dataLayer.addChild(SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3]))) } + + isPaused = false } } From 1c9faab1abf65c9489d82e82497fc8c7162f0f0e Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Thu, 26 Jul 2018 21:55:45 -0700 Subject: [PATCH 23/51] Move the IOB/COB below the graph --- .../Scenes/GlucoseChartScene.swift | 2 +- WatchApp/Base.lproj/Interface.storyboard | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index b7557b4d66..b827ff8956 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -128,7 +128,7 @@ class GlucoseChartScene: SKScene { super.init(size: { switch WKInterfaceDevice.current().watchSize() { case .Watch38mm: - return CGSize(width: 134, height: 110) + return CGSize(width: 134, height: 61) case .Watch42mm: return CGSize(width: 154, height: 86) } diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index 4bdfafccbc..6da9658e8c 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -351,41 +351,41 @@ - - + + + + + + + + - - - - - - - - - From 88540c8209d7bfd10be34d69d87d393443771ff4 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Fri, 27 Jul 2018 09:44:06 -0700 Subject: [PATCH 24/51] Whitespace and comment cleanup --- .../Controllers/ActionHUDController.swift | 2 +- .../Controllers/ChartHUDController.swift | 16 ++++++++-------- .../Scenes/GlucoseChartScene.swift | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/WatchApp Extension/Controllers/ActionHUDController.swift b/WatchApp Extension/Controllers/ActionHUDController.swift index a6ad5eefb8..8386b11f20 100644 --- a/WatchApp Extension/Controllers/ActionHUDController.swift +++ b/WatchApp Extension/Controllers/ActionHUDController.swift @@ -1,5 +1,5 @@ // -// StatusInterfaceController.swift +// ActionHUDController.swift // Loop // // Created by Nathan Racklyeft on 5/29/16. diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index f08917fd3f..5003bc0712 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -1,5 +1,5 @@ // -// ChartInterfaceController.swift +// ChartHUDController.swift // Loop // // Created by Bharat Mediratta on 6/26/18. @@ -70,14 +70,14 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { 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:NSNumber(value:activeInsulin)) { iobLabel.setText(String(format: NSLocalizedString( @@ -86,21 +86,21 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { 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:NSNumber(value: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() @@ -109,7 +109,7 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { basalFormatter.maximumFractionDigits = 3 basalFormatter.positivePrefix = basalFormatter.plusSign let valueStr = basalFormatter.string(from:NSNumber(value:tempBasal)) - + let basalLabelText = String(format: NSLocalizedString( "%1$@ U/hr", comment: "The subtitle format describing the current temp basal rate. (1: localized basal rate description)"), diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index b827ff8956..b10b9bc1c9 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -1,5 +1,5 @@ // -// GlucoseScene.swift +// GlucoseChartScene.swift // WatchApp Extension // // Created by Bharat Mediratta on 7/16/18. From 5682404f28e7aceba5b1dd7f6377f59621fa983b Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Sat, 28 Jul 2018 09:22:46 -0700 Subject: [PATCH 25/51] Animate moving points on the chart --- .../Scenes/GlucoseChartScene.swift | 92 ++++++++++++++----- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index b10b9bc1c9..d62b1c3c14 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -11,6 +11,8 @@ import SpriteKit import HealthKit import LoopKit import WatchKit +import UIKit + // Stashing the extensions here for ease of development, but they should likely // move into their own files as appropriate @@ -35,14 +37,6 @@ extension SKLabelNode { } } -extension SKSpriteNode { - static func basic(color: UIColor, rect: CGRect) -> SKSpriteNode { - let node = SKSpriteNode(color: color, size: rect.size) - node.position = rect.origin - return node - } -} - struct Scaler { let startDate: Date let glucoseMin: Double @@ -95,6 +89,27 @@ extension WKInterfaceDevice { } } +extension WatchDatedRange { + var hashValue: UInt64 { + var hashValue: Double + hashValue = 2 * minValue + hashValue += 3 * maxValue + hashValue += 5 * startDate.timeIntervalSince1970 + hashValue += 7 * endDate.timeIntervalSince1970 + return UInt64(hashValue) + } +} + +extension SampleValue { + var hashValue: UInt64 { + var hashValue: Double + hashValue = 2 * startDate.timeIntervalSince1970 + hashValue += 3 * quantity.doubleValue(for: HKUnit.milligramsPerDeciliter) + return UInt64(hashValue) + } +} + + class GlucoseChartScene: SKScene { var unit: HKUnit? var temporaryOverride: WatchDatedRange? @@ -118,10 +133,12 @@ class GlucoseChartScene: SKScene { private var visibleHours: Int = 3 private var timer: Timer? - private var dataLayer: SKNode! private var hoursLabel: SKLabelNode! private var maxBGLabel: SKLabelNode! private var minBGLabel: SKLabelNode! + private var nodes: [UInt64 : SKNode] = [:] + private var expire: [UInt64 : SKNode] = [:] + private var predictedPathNode: SKShapeNode? override init() { // Use the fixed sizes specified in the storyboard, based on our guess of the model size @@ -162,9 +179,6 @@ class GlucoseChartScene: SKScene { minBGLabel.verticalAlignmentMode = .bottom addChild(minBGLabel) - dataLayer = SKNode() - addChild(dataLayer) - // Force an update once a minute Timer.scheduledTimer(withTimeInterval: TimeInterval(minutes: 1), repeats: true) { _ in self.updateNodes() @@ -176,13 +190,29 @@ class GlucoseChartScene: SKScene { } override func update(_ currentTime: TimeInterval) { - if maxBGLabel.action(forKey: "highlight") == nil { + if maxBGLabel.action(forKey: "highlight") == nil && predictedPathNode?.action(forKey: "move") == nil { DispatchQueue.main.async { self.isPaused = true } } } + func animate(nodeFor hashValue: UInt64, to color: UIColor, and rect: CGRect) { + if let node = nodes[hashValue] as? SKSpriteNode { + node.color = color + node.run(SKAction.group([ + SKAction.move(to: rect.origin, duration: 0.25), + SKAction.resize(toWidth: rect.size.width, duration: 0.25), + SKAction.resize(toHeight: rect.size.height, duration: 0.25)])) + expire.removeValue(forKey: hashValue) + } else { + let node = SKSpriteNode(color: color, size: rect.size) + node.position = rect.origin + nodes[hashValue] = node + addChild(node) + } + } + func updateNodes() { dispatchPrecondition(condition: .onQueue(.main)) @@ -202,33 +232,45 @@ class GlucoseChartScene: SKScene { maxBGLabel.text = numberFormatter.string(from: unit.highWatermarkRange[visibleBg]) hoursLabel.text = "\(Int(visibleHours))h" - dataLayer.removeAllChildren() - targetRanges?.enumerated().forEach { (i, range) in - let color = UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.2 : 0.4) - dataLayer.addChild(SKSpriteNode.basic(color: color, rect: scaler.centerRect(for: range))) + expire = nodes + targetRanges?.forEach { range in + animate(nodeFor: range.hashValue, + to: UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.2 : 0.4), + and: scaler.centerRect(for: range)) } if let range = temporaryOverride { let color = UIColor.rangeColor.withAlphaComponent(0.2) - var rect = scaler.centerRect(for: range) - dataLayer.addChild(SKSpriteNode.basic(color: color, rect: rect)) + animate(nodeFor: range.hashValue, to: color, and: scaler.centerRect(for: range)) - rect.size.width = size.width - dataLayer.addChild(SKSpriteNode.basic(color: color, rect: rect)) + let extendedRange = WatchDatedRange(startDate: range.startDate, endDate: Date() + window, minValue: range.minValue, maxValue: range.maxValue) + animate(nodeFor: extendedRange.hashValue, to: color, and: scaler.centerRect(for: extendedRange)) } historicalGlucose?.filter { $0.startDate > scaler.startDate }.forEach { - let node = SKSpriteNode(color: .glucoseTintColor, size: CGSize(width: 2, height: 2)) - node.position = scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) - dataLayer.addChild(node) + let origin = scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) + let size = CGSize(width: 2, height: 2) + animate(nodeFor: $0.hashValue, to: .glucoseTintColor, and: CGRect(origin: origin, size: size)) } + 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)) }) - dataLayer.addChild(SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3]))) + + predictedPathNode = SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3])) + addChild(predictedPathNode!) + predictedPathNode!.alpha = 0 + predictedPathNode!.run(SKAction.sequence([ + SKAction.wait(forDuration: 0.25), + SKAction.fadeIn(withDuration: 0.75) + ]), withKey: "move") + } + expire.forEach { key, value in + nodes.removeValue(forKey: key) + value.removeFromParent() } isPaused = false From e636fb61750b979f49cee67fe5f8cad9d815c44f Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Sat, 28 Jul 2018 10:04:57 -0700 Subject: [PATCH 26/51] Only animate the predicted path when adjusting the y-axis --- .../Controllers/ChartHUDController.swift | 2 +- .../Scenes/GlucoseChartScene.swift | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 5003bc0712..c25f5ca60c 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -134,7 +134,7 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { loopManager?.glucoseStore.getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { (samples) in DispatchQueue.main.async { self.scene.historicalGlucose = samples - self.scene.updateNodes() + self.scene.updateNodes(animatePath: false) } } } diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index d62b1c3c14..848a43354a 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -124,7 +124,7 @@ class GlucoseChartScene: SKScene { maxBGLabel.setScale(2.0) maxBGLabel.run(SKAction.scale(to: 1.0, duration: 1.0), withKey: "highlight") - updateNodes() + updateNodes(animatePath: true) } else { visibleBg = oldValue } @@ -181,7 +181,7 @@ class GlucoseChartScene: SKScene { // Force an update once a minute Timer.scheduledTimer(withTimeInterval: TimeInterval(minutes: 1), repeats: true) { _ in - self.updateNodes() + self.updateNodes(animatePath: false) } } @@ -213,7 +213,7 @@ class GlucoseChartScene: SKScene { } } - func updateNodes() { + func updateNodes(animatePath: Bool) { dispatchPrecondition(condition: .onQueue(.main)) guard let unit = unit else { @@ -262,11 +262,14 @@ class GlucoseChartScene: SKScene { predictedPathNode = SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3])) addChild(predictedPathNode!) - predictedPathNode!.alpha = 0 - predictedPathNode!.run(SKAction.sequence([ - SKAction.wait(forDuration: 0.25), - SKAction.fadeIn(withDuration: 0.75) - ]), withKey: "move") + + if animatePath { + predictedPathNode!.alpha = 0 + predictedPathNode!.run(SKAction.sequence([ + SKAction.wait(forDuration: 0.25), + SKAction.fadeIn(withDuration: 0.75) + ]), withKey: "move") + } } expire.forEach { key, value in nodes.removeValue(forKey: key) From b241a8039a2ca493bccedf671403adc7f8712bcc Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Sat, 28 Jul 2018 11:03:54 -0700 Subject: [PATCH 27/51] Clean up sprite animation code --- .../Controllers/ChartHUDController.swift | 2 +- .../Scenes/GlucoseChartScene.swift | 51 ++++++++++--------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index c25f5ca60c..406a713d81 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -134,7 +134,7 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { loopManager?.glucoseStore.getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { (samples) in DispatchQueue.main.async { self.scene.historicalGlucose = samples - self.scene.updateNodes(animatePath: false) + self.scene.updateNodes(animated: false) } } } diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 848a43354a..bdb566c399 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -47,7 +47,7 @@ struct Scaler { return CGPoint(x: CGFloat(x.timeIntervalSince(startDate)) * xScale, y: CGFloat(y - glucoseMin) * yScale) } - func centerRect(for range: WatchDatedRange) -> CGRect { + func rect(for range: WatchDatedRange) -> CGRect { let a = point(range.startDate, range.minValue) let b = point(range.endDate, range.maxValue) let size = CGSize(width: b.x - a.x, height: b.y - a.y) @@ -124,7 +124,7 @@ class GlucoseChartScene: SKScene { maxBGLabel.setScale(2.0) maxBGLabel.run(SKAction.scale(to: 1.0, duration: 1.0), withKey: "highlight") - updateNodes(animatePath: true) + updateNodes(animated: true) } else { visibleBg = oldValue } @@ -137,7 +137,6 @@ class GlucoseChartScene: SKScene { private var maxBGLabel: SKLabelNode! private var minBGLabel: SKLabelNode! private var nodes: [UInt64 : SKNode] = [:] - private var expire: [UInt64 : SKNode] = [:] private var predictedPathNode: SKShapeNode? override init() { @@ -181,7 +180,7 @@ class GlucoseChartScene: SKScene { // Force an update once a minute Timer.scheduledTimer(withTimeInterval: TimeInterval(minutes: 1), repeats: true) { _ in - self.updateNodes(animatePath: false) + self.updateNodes(animated: false) } } @@ -197,23 +196,28 @@ class GlucoseChartScene: SKScene { } } - func animate(nodeFor hashValue: UInt64, to color: UIColor, and rect: CGRect) { - if let node = nodes[hashValue] as? SKSpriteNode { - node.color = color + func updateSprite(for hashValue: UInt64, to color: UIColor, at rect: CGRect, animated: Bool) { + if nodes[hashValue] as? SKSpriteNode == nil { + nodes[hashValue] = SKSpriteNode(color: color, size: rect.size) + nodes[hashValue]!.position = rect.origin + addChild(nodes[hashValue]!) + } + + let node = nodes[hashValue] as! SKSpriteNode + node.color = color + + if animated { node.run(SKAction.group([ SKAction.move(to: rect.origin, duration: 0.25), SKAction.resize(toWidth: rect.size.width, duration: 0.25), SKAction.resize(toHeight: rect.size.height, duration: 0.25)])) - expire.removeValue(forKey: hashValue) } else { - let node = SKSpriteNode(color: color, size: rect.size) + node.size = rect.size node.position = rect.origin - nodes[hashValue] = node - addChild(node) } } - func updateNodes(animatePath: Bool) { + func updateNodes(animated: Bool) { dispatchPrecondition(condition: .onQueue(.main)) guard let unit = unit else { @@ -232,25 +236,26 @@ class GlucoseChartScene: SKScene { maxBGLabel.text = numberFormatter.string(from: unit.highWatermarkRange[visibleBg]) hoursLabel.text = "\(Int(visibleHours))h" - expire = nodes + let expire = nodes targetRanges?.forEach { range in - animate(nodeFor: range.hashValue, - to: UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.2 : 0.4), - and: scaler.centerRect(for: range)) + updateSprite(for: range.hashValue, + to: UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.2 : 0.4), + at: scaler.rect(for: range), + animated: animated) } if let range = temporaryOverride { let color = UIColor.rangeColor.withAlphaComponent(0.2) - animate(nodeFor: range.hashValue, to: color, and: scaler.centerRect(for: range)) + updateSprite(for: range.hashValue, to: color, at: scaler.rect(for: range), animated: animated) let extendedRange = WatchDatedRange(startDate: range.startDate, endDate: Date() + window, minValue: range.minValue, maxValue: range.maxValue) - animate(nodeFor: extendedRange.hashValue, to: color, and: scaler.centerRect(for: extendedRange)) + updateSprite(for: extendedRange.hashValue, to: color, at: scaler.rect(for: extendedRange), animated: animated) } historicalGlucose?.filter { $0.startDate > scaler.startDate }.forEach { let origin = scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) let size = CGSize(width: 2, height: 2) - animate(nodeFor: $0.hashValue, to: .glucoseTintColor, and: CGRect(origin: origin, size: size)) + updateSprite(for: $0.hashValue, to: .glucoseTintColor, at: CGRect(origin: origin, size: size), animated: animated) } predictedPathNode?.removeFromParent() @@ -263,7 +268,7 @@ class GlucoseChartScene: SKScene { predictedPathNode = SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3])) addChild(predictedPathNode!) - if animatePath { + if animated { predictedPathNode!.alpha = 0 predictedPathNode!.run(SKAction.sequence([ SKAction.wait(forDuration: 0.25), @@ -271,9 +276,9 @@ class GlucoseChartScene: SKScene { ]), withKey: "move") } } - expire.forEach { key, value in - nodes.removeValue(forKey: key) - value.removeFromParent() + expire.filter { hash, _ in nodes[hash] == nil }.forEach { hash, node in + node.removeFromParent() + nodes.removeValue(forKey: hash) } isPaused = false From 7895644b5ed508f63676817dd46f171fffb8127e Mon Sep 17 00:00:00 2001 From: elnjensen Date: Sun, 29 Jul 2018 09:32:36 -0400 Subject: [PATCH 28/51] Adjust scene size on 38mm watch --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 2 +- WatchApp/Base.lproj/Interface.storyboard | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index bdb566c399..77518f97f2 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -144,7 +144,7 @@ class GlucoseChartScene: SKScene { super.init(size: { switch WKInterfaceDevice.current().watchSize() { case .Watch38mm: - return CGSize(width: 134, height: 61) + return CGSize(width: 134, height: 68) case .Watch42mm: return CGSize(width: 154, height: 86) } diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index 6da9658e8c..d93853cfd1 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -1,12 +1,12 @@ - + - + @@ -368,10 +368,10 @@ - + - + From 3adc650c46892fc679b8644d19102771b1b66876 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Sun, 29 Jul 2018 14:24:57 -0400 Subject: [PATCH 29/51] Only show overrides that haven't yet expired --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 77518f97f2..2fd44be9ec 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -244,7 +244,7 @@ class GlucoseChartScene: SKScene { animated: animated) } - if let range = temporaryOverride { + if let range = temporaryOverride, range.endDate > Date() { let color = UIColor.rangeColor.withAlphaComponent(0.2) updateSprite(for: range.hashValue, to: color, at: scaler.rect(for: range), animated: animated) From 56cd2dac4b3b12235070ffc4e436ff9cbfdc2907 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Sun, 29 Jul 2018 20:59:06 -0700 Subject: [PATCH 30/51] Fix node expiration code --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index bdb566c399..6651884763 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -236,26 +236,30 @@ class GlucoseChartScene: SKScene { maxBGLabel.text = numberFormatter.string(from: unit.highWatermarkRange[visibleBg]) hoursLabel.text = "\(Int(visibleHours))h" - let expire = nodes + var expire = nodes targetRanges?.forEach { range in updateSprite(for: range.hashValue, to: UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.2 : 0.4), at: scaler.rect(for: range), animated: animated) + expire.removeValue(forKey: range.hashValue) } if let range = temporaryOverride { let color = UIColor.rangeColor.withAlphaComponent(0.2) updateSprite(for: range.hashValue, to: color, at: scaler.rect(for: range), animated: animated) + expire.removeValue(forKey: range.hashValue) let extendedRange = WatchDatedRange(startDate: range.startDate, endDate: Date() + window, minValue: range.minValue, maxValue: range.maxValue) updateSprite(for: extendedRange.hashValue, to: color, at: scaler.rect(for: extendedRange), animated: animated) + expire.removeValue(forKey: extendedRange.hashValue) } historicalGlucose?.filter { $0.startDate > scaler.startDate }.forEach { let origin = scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) let size = CGSize(width: 2, height: 2) updateSprite(for: $0.hashValue, to: .glucoseTintColor, at: CGRect(origin: origin, size: size), animated: animated) + expire.removeValue(forKey: $0.hashValue) } predictedPathNode?.removeFromParent() @@ -276,7 +280,7 @@ class GlucoseChartScene: SKScene { ]), withKey: "move") } } - expire.filter { hash, _ in nodes[hash] == nil }.forEach { hash, node in + expire.forEach { hash, node in node.removeFromParent() nodes.removeValue(forKey: hash) } From 2a067bc0af9e99fae07f2b60d6a5244f628ce8bb Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Mon, 30 Jul 2018 14:41:18 -0700 Subject: [PATCH 31/51] Break the more complex updateSprite into two more manageable pieces: getSprite() and SKSpriteKitNode.move() --- .../Scenes/GlucoseChartScene.swift | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 6651884763..f375929b33 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -37,6 +37,20 @@ extension SKLabelNode { } } +extension SKSpriteNode { + func move(to rect: CGRect, animated: Bool) { + if parent == nil || animated == false { + size = rect.size + position = rect.origin + } else { + run(SKAction.group([ + SKAction.move(to: rect.origin, duration: 0.25), + SKAction.resize(toWidth: rect.size.width, duration: 0.25), + SKAction.resize(toHeight: rect.size.height, duration: 0.25)])) + } + } +} + struct Scaler { let startDate: Date let glucoseMin: Double @@ -136,7 +150,7 @@ class GlucoseChartScene: SKScene { private var hoursLabel: SKLabelNode! private var maxBGLabel: SKLabelNode! private var minBGLabel: SKLabelNode! - private var nodes: [UInt64 : SKNode] = [:] + private var nodes: [UInt64 : SKSpriteNode] = [:] private var predictedPathNode: SKShapeNode? override init() { @@ -196,25 +210,12 @@ class GlucoseChartScene: SKScene { } } - func updateSprite(for hashValue: UInt64, to color: UIColor, at rect: CGRect, animated: Bool) { - if nodes[hashValue] as? SKSpriteNode == nil { - nodes[hashValue] = SKSpriteNode(color: color, size: rect.size) - nodes[hashValue]!.position = rect.origin + func getSprite(forHash hashValue: UInt64) -> SKSpriteNode { + if nodes[hashValue] == nil { + nodes[hashValue] = SKSpriteNode(color: .clear, size: CGSize(width: 0, height: 0)) addChild(nodes[hashValue]!) } - - let node = nodes[hashValue] as! SKSpriteNode - node.color = color - - if animated { - node.run(SKAction.group([ - SKAction.move(to: rect.origin, duration: 0.25), - SKAction.resize(toWidth: rect.size.width, duration: 0.25), - SKAction.resize(toHeight: rect.size.height, duration: 0.25)])) - } else { - node.size = rect.size - node.position = rect.origin - } + return nodes[hashValue]! } func updateNodes(animated: Bool) { @@ -238,27 +239,32 @@ class GlucoseChartScene: SKScene { var expire = nodes targetRanges?.forEach { range in - updateSprite(for: range.hashValue, - to: UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.2 : 0.4), - at: scaler.rect(for: range), - animated: animated) + let sprite = getSprite(forHash: range.hashValue) + sprite.color = UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.2 : 0.4) + sprite.move(to: scaler.rect(for: range), animated: animated) expire.removeValue(forKey: range.hashValue) } if let range = temporaryOverride { let color = UIColor.rangeColor.withAlphaComponent(0.2) - updateSprite(for: range.hashValue, to: color, at: scaler.rect(for: range), animated: animated) + let sprite1 = getSprite(forHash: range.hashValue) + sprite1.color = color + sprite1.move(to: scaler.rect(for: range), animated: animated) expire.removeValue(forKey: range.hashValue) let extendedRange = WatchDatedRange(startDate: range.startDate, endDate: Date() + window, minValue: range.minValue, maxValue: range.maxValue) - updateSprite(for: extendedRange.hashValue, to: color, at: scaler.rect(for: extendedRange), animated: animated) + let sprite2 = getSprite(forHash: extendedRange.hashValue) + sprite2.color = color + sprite2.move(to: scaler.rect(for: extendedRange), animated: animated) expire.removeValue(forKey: extendedRange.hashValue) } historicalGlucose?.filter { $0.startDate > scaler.startDate }.forEach { let origin = scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) let size = CGSize(width: 2, height: 2) - updateSprite(for: $0.hashValue, to: .glucoseTintColor, at: CGRect(origin: origin, size: size), animated: animated) + let sprite = getSprite(forHash: $0.hashValue) + sprite.color = .glucoseTintColor + sprite.move(to: CGRect(origin: origin, size: size), animated: animated) expire.removeValue(forKey: $0.hashValue) } @@ -280,6 +286,12 @@ class GlucoseChartScene: SKScene { ]), withKey: "move") } } + + // Any new nodes we created in this pass need to be added to the scene. And any nodes + // that are no longer necessary can be removed from the scene and deleted. + nodes.values.filter { $0.parent == nil }.forEach { + addChild($0) + } expire.forEach { hash, node in node.removeFromParent() nodes.removeValue(forKey: hash) From fbd66221a99971ef3b8c39133e692553375f8f18 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Mon, 30 Jul 2018 17:34:42 -0700 Subject: [PATCH 32/51] Clean up variable names and comments around how we expire inactive nodes for readability --- .../Scenes/GlucoseChartScene.swift | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index f375929b33..6e7d2460bf 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -237,12 +237,14 @@ class GlucoseChartScene: SKScene { maxBGLabel.text = numberFormatter.string(from: unit.highWatermarkRange[visibleBg]) hoursLabel.text = "\(Int(visibleHours))h" - var expire = nodes + // Keep track of the nodes we started this pass with so we can expire obsolete nodes at the end + var inactiveNodes = nodes + targetRanges?.forEach { range in let sprite = getSprite(forHash: range.hashValue) sprite.color = UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.2 : 0.4) sprite.move(to: scaler.rect(for: range), animated: animated) - expire.removeValue(forKey: range.hashValue) + inactiveNodes.removeValue(forKey: range.hashValue) } if let range = temporaryOverride { @@ -250,13 +252,13 @@ class GlucoseChartScene: SKScene { let sprite1 = getSprite(forHash: range.hashValue) sprite1.color = color sprite1.move(to: scaler.rect(for: range), animated: animated) - expire.removeValue(forKey: range.hashValue) + inactiveNodes.removeValue(forKey: range.hashValue) let extendedRange = WatchDatedRange(startDate: range.startDate, endDate: Date() + window, minValue: range.minValue, maxValue: range.maxValue) let sprite2 = getSprite(forHash: extendedRange.hashValue) sprite2.color = color sprite2.move(to: scaler.rect(for: extendedRange), animated: animated) - expire.removeValue(forKey: extendedRange.hashValue) + inactiveNodes.removeValue(forKey: extendedRange.hashValue) } historicalGlucose?.filter { $0.startDate > scaler.startDate }.forEach { @@ -265,7 +267,7 @@ class GlucoseChartScene: SKScene { let sprite = getSprite(forHash: $0.hashValue) sprite.color = .glucoseTintColor sprite.move(to: CGRect(origin: origin, size: size), animated: animated) - expire.removeValue(forKey: $0.hashValue) + inactiveNodes.removeValue(forKey: $0.hashValue) } predictedPathNode?.removeFromParent() @@ -287,12 +289,8 @@ class GlucoseChartScene: SKScene { } } - // Any new nodes we created in this pass need to be added to the scene. And any nodes - // that are no longer necessary can be removed from the scene and deleted. - nodes.values.filter { $0.parent == nil }.forEach { - addChild($0) - } - expire.forEach { hash, node in + // Any inactive nodes can be safely removed + inactiveNodes.forEach { hash, node in node.removeFromParent() nodes.removeValue(forKey: hash) } From bd6b55a1e81cd6a73c280119de164dbf31f20585 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Mon, 30 Jul 2018 17:41:08 -0700 Subject: [PATCH 33/51] Add some comments to aid readability --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 6e7d2460bf..3272100a4a 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -247,16 +247,18 @@ class GlucoseChartScene: SKScene { inactiveNodes.removeValue(forKey: range.hashValue) } + // 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 = temporaryOverride { - let color = UIColor.rangeColor.withAlphaComponent(0.2) let sprite1 = getSprite(forHash: range.hashValue) - sprite1.color = color + sprite1.color = UIColor.rangeColor.withAlphaComponent(0.2) sprite1.move(to: scaler.rect(for: range), animated: animated) inactiveNodes.removeValue(forKey: range.hashValue) let extendedRange = WatchDatedRange(startDate: range.startDate, endDate: Date() + window, minValue: range.minValue, maxValue: range.maxValue) let sprite2 = getSprite(forHash: extendedRange.hashValue) - sprite2.color = color + sprite2.color = UIColor.rangeColor.withAlphaComponent(0.2) sprite2.move(to: scaler.rect(for: extendedRange), animated: animated) inactiveNodes.removeValue(forKey: extendedRange.hashValue) } @@ -281,6 +283,7 @@ class GlucoseChartScene: SKScene { addChild(predictedPathNode!) if animated { + // SKShapeNode paths cannot be easily animated. Make it vanish, then fade in at the new location. predictedPathNode!.alpha = 0 predictedPathNode!.run(SKAction.sequence([ SKAction.wait(forDuration: 0.25), From f1da6770f1baec9af270b90c32d828468a2a3dce Mon Sep 17 00:00:00 2001 From: elnjensen Date: Tue, 31 Jul 2018 09:40:13 -0400 Subject: [PATCH 34/51] Change visible hours with force touch menu --- .../Controllers/ChartHUDController.swift | 10 +++++++++- .../Scenes/GlucoseChartScene.swift | 2 +- WatchApp/Base.lproj/Interface.storyboard | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 406a713d81..6aaf758078 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -17,7 +17,15 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { @IBOutlet weak var iobLabel: WKInterfaceLabel! @IBOutlet weak var cobLabel: WKInterfaceLabel! @IBOutlet weak var glucoseScene: WKInterfaceSKScene! - + @IBAction func setChartWindow1Hour() { + scene.visibleHours = 1 + } + @IBAction func setChartWindow3Hours() { + scene.visibleHours = 3 + } + @IBAction func setChartWindow2Hours() { + scene.visibleHours = 2 + } private let scene = GlucoseChartScene() override init() { diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 645a85ac1d..aa8e703b76 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -145,7 +145,7 @@ class GlucoseChartScene: SKScene { } } - private var visibleHours: Int = 3 + var visibleHours: Int = 3 private var timer: Timer? private var hoursLabel: SKLabelNode! private var maxBGLabel: SKLabelNode! diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index d93853cfd1..5d9117d5cf 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -387,6 +387,25 @@ + + + + + + + + + + + + + + + + + + + From bc46343abc9b8ba7d9970d3a7bc3ec5433b9dd64 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Tue, 31 Jul 2018 23:54:07 -0400 Subject: [PATCH 35/51] Revert changes in tools versions in storyboard --- WatchApp/Base.lproj/Interface.storyboard | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index 5d9117d5cf..140f79fe86 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -1,12 +1,12 @@ - + - + From 0a23e33bacf2e58d2547e95d88aadbb7d2c25558 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Wed, 1 Aug 2018 00:04:42 -0400 Subject: [PATCH 36/51] Add icons for duration menu --- Loop.xcodeproj/project.pbxproj | 32 ++++++++++++++++++ .../1-hour-graph-38mm.imageset/Contents.json | 21 ++++++++++++ .../Graph menu icons/1-hour-graph-42mm.png | Bin 0 -> 2687 bytes .../2-hour-graph-38mm.imageset/Contents.json | 21 ++++++++++++ .../Graph menu icons/2-hour-graph-42mm.png | Bin 0 -> 3097 bytes .../3-hour-graph-38mm.imageset/Contents.json | 21 ++++++++++++ .../Graph menu icons/3-hour-graph-42mm.png | Bin 0 -> 3157 bytes WatchApp/Base.lproj/Interface.storyboard | 12 +++++-- 8 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/Contents.json create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.imageset/Contents.json create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.png diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 595058300e..6dc13b97e8 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -22,6 +22,12 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 01C75A422111384100444723 /* 1-hour-graph-38mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A3F2111384100444723 /* 1-hour-graph-38mm.png */; }; + 01C75A432111384100444723 /* 2-hour-graph-38mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A402111384100444723 /* 2-hour-graph-38mm.png */; }; + 01C75A442111384100444723 /* 3-hour-graph-38mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A412111384100444723 /* 3-hour-graph-38mm.png */; }; + 01C75A4E2111631200444723 /* 1-hour-graph-42mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A4B2111631100444723 /* 1-hour-graph-42mm.png */; }; + 01C75A4F2111631200444723 /* 3-hour-graph-42mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A4C2111631100444723 /* 3-hour-graph-42mm.png */; }; + 01C75A502111631200444723 /* 2-hour-graph-42mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A4D2111631200444723 /* 2-hour-graph-42mm.png */; }; 43027F0F1DFE0EC900C51989 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D5E1DF2459000A04910 /* HKUnit.swift */; }; 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */; }; 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */; }; @@ -442,6 +448,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 01C75A3F2111384100444723 /* 1-hour-graph-38mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "1-hour-graph-38mm.png"; sourceTree = ""; }; + 01C75A402111384100444723 /* 2-hour-graph-38mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "2-hour-graph-38mm.png"; sourceTree = ""; }; + 01C75A412111384100444723 /* 3-hour-graph-38mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "3-hour-graph-38mm.png"; sourceTree = ""; }; + 01C75A4B2111631100444723 /* 1-hour-graph-42mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "1-hour-graph-42mm.png"; sourceTree = ""; }; + 01C75A4C2111631100444723 /* 3-hour-graph-42mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "3-hour-graph-42mm.png"; sourceTree = ""; }; + 01C75A4D2111631200444723 /* 2-hour-graph-42mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "2-hour-graph-42mm.png"; sourceTree = ""; }; 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 43076BF21DFDBC4B0012A723 /* it.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = it.lproj; sourceTree = ""; }; @@ -790,6 +802,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 01C75A3E2111384100444723 /* Graph menu icons */ = { + isa = PBXGroup; + children = ( + 01C75A3F2111384100444723 /* 1-hour-graph-38mm.png */, + 01C75A4B2111631100444723 /* 1-hour-graph-42mm.png */, + 01C75A4D2111631200444723 /* 2-hour-graph-42mm.png */, + 01C75A4C2111631100444723 /* 3-hour-graph-42mm.png */, + 01C75A402111384100444723 /* 2-hour-graph-38mm.png */, + 01C75A412111384100444723 /* 3-hour-graph-38mm.png */, + ); + name = "Graph menu icons"; + path = "Assets.xcassets/Graph menu icons"; + sourceTree = ""; + }; 4328E0121CFBE1B700E199AA /* Controllers */ = { isa = PBXGroup; children = ( @@ -1565,6 +1591,12 @@ buildActionMask = 2147483647; files = ( C1C73F0D1DE3D0270022FC89 /* InfoPlist.strings in Resources */, + 01C75A4E2111631200444723 /* 1-hour-graph-42mm.png in Resources */, + 01C75A4F2111631200444723 /* 3-hour-graph-42mm.png in Resources */, + 01C75A422111384100444723 /* 1-hour-graph-38mm.png in Resources */, + 01C75A432111384100444723 /* 2-hour-graph-38mm.png in Resources */, + 01C75A502111631200444723 /* 2-hour-graph-42mm.png in Resources */, + 01C75A442111384100444723 /* 3-hour-graph-38mm.png in Resources */, 894F71E21FFEC4D8007D365C /* Assets.xcassets in Resources */, 43A943761B926B7B0051FA24 /* Interface.storyboard in Resources */, ); diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json new file mode 100644 index 0000000000..2b91a034e6 --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "1-hour-graph-38mm.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file 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 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 literal 0 HcmV?d00001 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.imageset/Contents.json new file mode 100644 index 0000000000..a3f9207025 --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "3-hour-graph-38mm.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.png new file mode 100644 index 0000000000000000000000000000000000000000..c7474e6a2d825c0a8c32c750e9aaca5d06ef8663 GIT binary patch literal 3157 zcma)8c{~)_7oSOyK`By5#u!^MV{5_KWemn%mQaJi$QZ^jV_(V^qwKPiHCZB(EkdTO zd4(iPwvcR1ridTad+)dX@$Tny?m73|?^(X*{&S;@43HckJ`ex^;6UkUne1w#Jvqp{ zyT=|`E8A5-PZOjDpuG3w{O*C(UB}WB0ASWzuWtdsJ)wCVLw^@Du&n{jiM$Iiy;w+W(0z(iu&H{z$iS%32TDI zIQSso|Bd*&Q^a0MMMEMEyUTTtk%|lg{y)Cocm#Zp?!VFhHQS%w-QlW$b}Rk)P%0n^ zSdTIQ01`%Nshd%NOKEC0 zM`meOuYNp4KZ1s6upC+#jL*+08Gzn7cgfC9Tz{kx*Z6_+^0h@pqahb6$nFe2?;*vJFXYFz#M|A`7Q6auTvd}Exd zSU86}cKJV=Zr;+j}d6bJyV&)z3 z2Qj)5Hj}z-!qMC378tZ@k#BE^itT84{s%_nb;)(=VOHEtU76%O zVElN}Kp~*Rj=AS8q7(&Z;o~z5mc)qt5lGLs#-XZlFJbw zcXQ60Er^cz8a@F!5|Vm4OO9zHy6MwOz5C*%SbSzr#F+ySZOiA+>N)DmYiIh8hb_`s zuhJ2Bd7rN(LaT*Su)Zr^Ckb8M#ffi?0d#bSN2CC`K$ti1uAUFDSq^E*`iC@`FYY@W zn8r5c{KD1o-qYt9ai3iM4|WC&xHq$QeIu-$Jer)%uKw=W;{fBqXpx77T$vGT`^=Jg z)kCdwbG{iXSm=?c*q=Ne zDINAX1E!~yl4)vmp~WdBT+fv)Kc;V~g@M0Pxx*IMVbyzOa&5N%G}ATt<@ieD$w<+` zL%IQ?Jt3O!pB`=vl;_ty)rhqCV2jOFc53Gy#Lepc_z~uQ@qOT!U=px$rIh#7G<*FW zs%Ec;DDnEa=S6(AluLYthKw821uPA!^A;rAC$bOMzTYS~Kr`N#5~U52i84!Ffk-K{ z#T~mMG0~V)<3}rWtnwFX)rKllM&zfh-t<}PT=g-RPGSb@ailz)peUFyB~NR7z18`o zJGRPr9pDkCfE$q`9{gtT5i!G3<+7coome{RT0|hkZ4}+`hgXPZ*403hV+^%KgB6khCEw|-~7>%eq z_#h)7i?>I`7Md%s91;_6oE&zD^M+a)XhP27RF{sPOHM0I=HL0~?X3*POJ+)@FVR~1 zH2_EfSo!)D1Ov&*?&t1MXbG>5&s2dHmOxS;uDy?q!dV{SBOrrLEi6uPldVzItm zA{F5>ki2Br{K!A2pC_9u6RmX3de%6{_pAbMvGyvodF&qaF^!u0@Q454;=o5W6E~J= zP*;MKgl=A`hi9goGv!k-H~i(E(i6c`MO%{S zSs7yKSQ&1qw89S-h^(t|(k!O0=m(01G2{{yOS8wK4xt?PM=1Fb9*Y+RM zwPG$q9SYK$RmJzyPj*A85Ad$Fg{lm$xu%4xo!4LaH?ZGU_#Q6xn9sb;W#GiL1w5F_ z)1cr|DzqWG+N=|$hXc+srwBsxLq$x$&^8~f$lKZsg)i2p2kVZT`y1ucSAJx1wiT~= z{*l8LE;RS#ZtnNnN@;Mr7pFYNl-@`dtg@7|5jjp9I@=8!e9gdY2AyzIs!9rg-$pSB&zCCF_#v?=ZAz_AuqTO7-v^)w$E zpVidy@7yz0N zb=};syaf}Hg>dn)%c{ znBYP4Z$TkJuviTOww2rV*)Z(%$F%ciH&a>}j`2cd5v4T0qy=%Eo&Dif>@AD$)q~LG z6=gD^xgL_Gixo*zFG~%g7N+k61R}YW==IL~(t<^ESpS()o8e*za#MNT{5>Hydw+e@ MMFXvJjccL*0u895&Hw-a literal 0 HcmV?d00001 diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index 140f79fe86..7079506e01 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -389,17 +389,23 @@ - + + + - + + + - + + + From 621e7c26b464afedd20aaeaf1a75a040b8d061a3 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Wed, 1 Aug 2018 00:09:51 -0400 Subject: [PATCH 37/51] Rearrange menu actions --- WatchApp Extension/Controllers/ChartHUDController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 6aaf758078..2a896c429a 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -20,12 +20,12 @@ final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { @IBAction func setChartWindow1Hour() { scene.visibleHours = 1 } - @IBAction func setChartWindow3Hours() { - scene.visibleHours = 3 - } @IBAction func setChartWindow2Hours() { scene.visibleHours = 2 } + @IBAction func setChartWindow3Hours() { + scene.visibleHours = 3 + } private let scene = GlucoseChartScene() override init() { From 9a8c6244de0c2b3a1226e0a6d7cde642bc0d567f Mon Sep 17 00:00:00 2001 From: elnjensen Date: Wed, 1 Aug 2018 11:04:16 -0400 Subject: [PATCH 38/51] Fix asset locations for graph menu icons --- Loop.xcodeproj/project.pbxproj | 32 ------------------ .../1-hour-graph-38mm.png | Bin 0 -> 2658 bytes .../1-hour-graph-38mm.imageset/Contents.json | 8 ----- .../Graph menu icons/1-hour-graph-38mm.png | Bin 0 -> 2658 bytes .../1-hour-graph-42mm.png | Bin 0 -> 2687 bytes .../1-hour-graph-42mm.imageset/Contents.json | 13 +++++++ .../2-hour-graph-38mm.png | Bin 0 -> 2969 bytes .../2-hour-graph-38mm.imageset/Contents.json | 8 ----- .../Graph menu icons/2-hour-graph-38mm.png | Bin 0 -> 2969 bytes .../2-hour-graph-42mm.png | Bin 0 -> 3097 bytes .../2-hour-graph-42mm.imageset/Contents.json | 13 +++++++ .../3-hour-graph-38mm.png | Bin 0 -> 3043 bytes .../3-hour-graph-38mm.imageset/Contents.json | 8 ----- .../Graph menu icons/3-hour-graph-38mm.png | Bin 0 -> 3043 bytes .../3-hour-graph-42mm.png | Bin 0 -> 3157 bytes .../3-hour-graph-42mm.imageset/Contents.json | 13 +++++++ .../Graph menu icons/Contents.json | 6 ++++ 17 files changed, 45 insertions(+), 56 deletions(-) create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/1-hour-graph-38mm.png 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.imageset/1-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.imageset/Contents.json create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/2-hour-graph-38mm.png 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.imageset/2-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/Contents.json create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.imageset/3-hour-graph-38mm.png 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.imageset/3-hour-graph-42mm.png create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.imageset/Contents.json create mode 100644 WatchApp/Assets.xcassets/Graph menu icons/Contents.json diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 6dc13b97e8..595058300e 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -22,12 +22,6 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 01C75A422111384100444723 /* 1-hour-graph-38mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A3F2111384100444723 /* 1-hour-graph-38mm.png */; }; - 01C75A432111384100444723 /* 2-hour-graph-38mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A402111384100444723 /* 2-hour-graph-38mm.png */; }; - 01C75A442111384100444723 /* 3-hour-graph-38mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A412111384100444723 /* 3-hour-graph-38mm.png */; }; - 01C75A4E2111631200444723 /* 1-hour-graph-42mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A4B2111631100444723 /* 1-hour-graph-42mm.png */; }; - 01C75A4F2111631200444723 /* 3-hour-graph-42mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A4C2111631100444723 /* 3-hour-graph-42mm.png */; }; - 01C75A502111631200444723 /* 2-hour-graph-42mm.png in Resources */ = {isa = PBXBuildFile; fileRef = 01C75A4D2111631200444723 /* 2-hour-graph-42mm.png */; }; 43027F0F1DFE0EC900C51989 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D5E1DF2459000A04910 /* HKUnit.swift */; }; 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */; }; 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */; }; @@ -448,12 +442,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 01C75A3F2111384100444723 /* 1-hour-graph-38mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "1-hour-graph-38mm.png"; sourceTree = ""; }; - 01C75A402111384100444723 /* 2-hour-graph-38mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "2-hour-graph-38mm.png"; sourceTree = ""; }; - 01C75A412111384100444723 /* 3-hour-graph-38mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "3-hour-graph-38mm.png"; sourceTree = ""; }; - 01C75A4B2111631100444723 /* 1-hour-graph-42mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "1-hour-graph-42mm.png"; sourceTree = ""; }; - 01C75A4C2111631100444723 /* 3-hour-graph-42mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "3-hour-graph-42mm.png"; sourceTree = ""; }; - 01C75A4D2111631200444723 /* 2-hour-graph-42mm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "2-hour-graph-42mm.png"; sourceTree = ""; }; 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 43076BF21DFDBC4B0012A723 /* it.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = it.lproj; sourceTree = ""; }; @@ -802,20 +790,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 01C75A3E2111384100444723 /* Graph menu icons */ = { - isa = PBXGroup; - children = ( - 01C75A3F2111384100444723 /* 1-hour-graph-38mm.png */, - 01C75A4B2111631100444723 /* 1-hour-graph-42mm.png */, - 01C75A4D2111631200444723 /* 2-hour-graph-42mm.png */, - 01C75A4C2111631100444723 /* 3-hour-graph-42mm.png */, - 01C75A402111384100444723 /* 2-hour-graph-38mm.png */, - 01C75A412111384100444723 /* 3-hour-graph-38mm.png */, - ); - name = "Graph menu icons"; - path = "Assets.xcassets/Graph menu icons"; - sourceTree = ""; - }; 4328E0121CFBE1B700E199AA /* Controllers */ = { isa = PBXGroup; children = ( @@ -1591,12 +1565,6 @@ buildActionMask = 2147483647; files = ( C1C73F0D1DE3D0270022FC89 /* InfoPlist.strings in Resources */, - 01C75A4E2111631200444723 /* 1-hour-graph-42mm.png in Resources */, - 01C75A4F2111631200444723 /* 3-hour-graph-42mm.png in Resources */, - 01C75A422111384100444723 /* 1-hour-graph-38mm.png in Resources */, - 01C75A432111384100444723 /* 2-hour-graph-38mm.png in Resources */, - 01C75A502111631200444723 /* 2-hour-graph-42mm.png in Resources */, - 01C75A442111384100444723 /* 3-hour-graph-38mm.png in Resources */, 894F71E21FFEC4D8007D365C /* Assets.xcassets in Resources */, 43A943761B926B7B0051FA24 /* Interface.storyboard in Resources */, ); diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/1-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/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-38mm.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json index 2b91a034e6..577ab9b5d4 100644 --- a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json @@ -4,14 +4,6 @@ "idiom" : "universal", "filename" : "1-hour-graph-38mm.png", "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" } ], "info" : { 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.imageset/1-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.imageset/1-hour-graph-42mm.png new file mode 100644 index 0000000000000000000000000000000000000000..63f7579ca6ca6d0617ce634afbe5061e204c8a16 GIT binary patch literal 2687 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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-38mm.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/Contents.json index db20fa3ec4..8c3238626f 100644 --- a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/Contents.json @@ -4,14 +4,6 @@ "idiom" : "universal", "filename" : "2-hour-graph-38mm.png", "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" } ], "info" : { 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.imageset/2-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/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-42mm.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/Contents.json new file mode 100644 index 0000000000..5180b2c3cf --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "2-hour-graph-42mm.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.imageset/3-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.imageset/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-42mm.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.imageset/Contents.json new file mode 100644 index 0000000000..b265055d94 --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "3-hour-graph-42mm.png", + "scale" : "1x" + } + ], + "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 From 396df06e003da1d62700353dfda63904c0fad1c4 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Wed, 1 Aug 2018 18:34:55 -0400 Subject: [PATCH 39/51] Make mmol/L graph limits be integers --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index aa8e703b76..9379798464 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -74,7 +74,7 @@ extension HKUnit { if unitString == "mg/dL" { return [150.0, 200.0, 250.0, 300.0, 350.0, 400.0] } else { - return [8.3, 11.1, 13.9, 16.6, 19.4, 22.2] + return [8.0, 11.0, 14.0, 17.0, 20.0, 23.0] } } @@ -82,7 +82,7 @@ extension HKUnit { if unitString == "mg/dL" { return 50.0 } else { - return 2.8 + return 3.0 } } } From a1d21d92f5e057a4980bd3c7f802e2f98f14dfe0 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Wed, 1 Aug 2018 18:41:11 -0400 Subject: [PATCH 40/51] Force minimum height for dated range rects --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 9379798464..7492de855c 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -64,7 +64,7 @@ struct Scaler { func rect(for range: WatchDatedRange) -> CGRect { let a = point(range.startDate, range.minValue) let b = point(range.endDate, range.maxValue) - let size = CGSize(width: b.x - a.x, height: b.y - a.y) + let size = CGSize(width: b.x - a.x, height: max(b.y - a.y, 2)) return CGRect(origin: CGPoint(x: a.x + size.width / 2, y: a.y + size.height / 2), size: size) } } From 6a6c53112483d2138c0dd794eaebdadf9ba18426 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Wed, 1 Aug 2018 18:53:35 -0400 Subject: [PATCH 41/51] Make target ranges more visible --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 7492de855c..4f65592b5b 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -242,7 +242,7 @@ class GlucoseChartScene: SKScene { targetRanges?.forEach { range in let sprite = getSprite(forHash: range.hashValue) - sprite.color = UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.2 : 0.4) + sprite.color = UIColor.rangeColor.withAlphaComponent(temporaryOverride != nil ? 0.4 : 0.6) sprite.move(to: scaler.rect(for: range), animated: animated) inactiveNodes.removeValue(forKey: range.hashValue) } @@ -252,13 +252,13 @@ class GlucoseChartScene: SKScene { // extends to the end of the visible window. if let range = temporaryOverride { let sprite1 = getSprite(forHash: range.hashValue) - sprite1.color = UIColor.rangeColor.withAlphaComponent(0.2) + sprite1.color = UIColor.rangeColor.withAlphaComponent(0.6) sprite1.move(to: scaler.rect(for: range), animated: animated) inactiveNodes.removeValue(forKey: range.hashValue) let extendedRange = WatchDatedRange(startDate: range.startDate, endDate: Date() + window, minValue: range.minValue, maxValue: range.maxValue) let sprite2 = getSprite(forHash: extendedRange.hashValue) - sprite2.color = UIColor.rangeColor.withAlphaComponent(0.2) + sprite2.color = UIColor.rangeColor.withAlphaComponent(0.4) sprite2.move(to: scaler.rect(for: extendedRange), animated: animated) inactiveNodes.removeValue(forKey: extendedRange.hashValue) } From 0b25d2e2ec75c498a461f606b57a871d4e054bdd Mon Sep 17 00:00:00 2001 From: elnjensen Date: Wed, 1 Aug 2018 19:16:20 -0400 Subject: [PATCH 42/51] Only show future part of override ranges --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 4f65592b5b..df0bc614a6 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -61,8 +61,9 @@ struct Scaler { return CGPoint(x: CGFloat(x.timeIntervalSince(startDate)) * xScale, y: CGFloat(y - glucoseMin) * yScale) } - func rect(for range: WatchDatedRange) -> CGRect { - let a = point(range.startDate, range.minValue) + func rect(for range: WatchDatedRange, future: Bool = false) -> CGRect { + let startDate = future ? Date() : range.startDate + let a = point(startDate, range.minValue) let b = point(range.endDate, range.maxValue) let size = CGSize(width: b.x - a.x, height: max(b.y - a.y, 2)) return CGRect(origin: CGPoint(x: a.x + size.width / 2, y: a.y + size.height / 2), size: size) @@ -250,16 +251,16 @@ class GlucoseChartScene: SKScene { // 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 = temporaryOverride { + if let range = temporaryOverride, range.endDate > Date() { let sprite1 = getSprite(forHash: range.hashValue) sprite1.color = UIColor.rangeColor.withAlphaComponent(0.6) - sprite1.move(to: scaler.rect(for: range), animated: animated) + sprite1.move(to: scaler.rect(for: range, future: true), animated: animated) inactiveNodes.removeValue(forKey: range.hashValue) let extendedRange = WatchDatedRange(startDate: range.startDate, endDate: Date() + window, minValue: range.minValue, maxValue: range.maxValue) let sprite2 = getSprite(forHash: extendedRange.hashValue) sprite2.color = UIColor.rangeColor.withAlphaComponent(0.4) - sprite2.move(to: scaler.rect(for: extendedRange), animated: animated) + sprite2.move(to: scaler.rect(for: extendedRange, future: true), animated: animated) inactiveNodes.removeValue(forKey: extendedRange.hashValue) } From c4800fb5e80bf276a4d12bb196dbb5e20142ef66 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Wed, 1 Aug 2018 19:16:20 -0400 Subject: [PATCH 43/51] Revert "Only show future part of override ranges" This reverts commit 0b25d2e2ec75c498a461f606b57a871d4e054bdd. --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index df0bc614a6..4f65592b5b 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -61,9 +61,8 @@ struct Scaler { return CGPoint(x: CGFloat(x.timeIntervalSince(startDate)) * xScale, y: CGFloat(y - glucoseMin) * yScale) } - func rect(for range: WatchDatedRange, future: Bool = false) -> CGRect { - let startDate = future ? Date() : range.startDate - let a = point(startDate, range.minValue) + func rect(for range: WatchDatedRange) -> CGRect { + let a = point(range.startDate, range.minValue) let b = point(range.endDate, range.maxValue) let size = CGSize(width: b.x - a.x, height: max(b.y - a.y, 2)) return CGRect(origin: CGPoint(x: a.x + size.width / 2, y: a.y + size.height / 2), size: size) @@ -251,16 +250,16 @@ class GlucoseChartScene: SKScene { // 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 = temporaryOverride, range.endDate > Date() { + if let range = temporaryOverride { let sprite1 = getSprite(forHash: range.hashValue) sprite1.color = UIColor.rangeColor.withAlphaComponent(0.6) - sprite1.move(to: scaler.rect(for: range, future: true), animated: animated) + sprite1.move(to: scaler.rect(for: range), animated: animated) inactiveNodes.removeValue(forKey: range.hashValue) let extendedRange = WatchDatedRange(startDate: range.startDate, endDate: Date() + window, minValue: range.minValue, maxValue: range.maxValue) let sprite2 = getSprite(forHash: extendedRange.hashValue) sprite2.color = UIColor.rangeColor.withAlphaComponent(0.4) - sprite2.move(to: scaler.rect(for: extendedRange, future: true), animated: animated) + sprite2.move(to: scaler.rect(for: extendedRange), animated: animated) inactiveNodes.removeValue(forKey: extendedRange.hashValue) } From b6b4ec346a787ce6f3c9e14625d128d24d549b83 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Wed, 1 Aug 2018 21:13:00 -0400 Subject: [PATCH 44/51] Expire overrides once they move into past --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 4f65592b5b..2aa4cee3f5 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -250,7 +250,7 @@ class GlucoseChartScene: SKScene { // 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 = temporaryOverride { + if let range = temporaryOverride, range.endDate > Date() { let sprite1 = getSprite(forHash: range.hashValue) sprite1.color = UIColor.rangeColor.withAlphaComponent(0.6) sprite1.move(to: scaler.rect(for: range), animated: animated) From f168e163641062f0ba6d7f5cfd4baabb6310c69d Mon Sep 17 00:00:00 2001 From: elnjensen Date: Thu, 2 Aug 2018 00:34:00 -0400 Subject: [PATCH 45/51] Set min range height via variable --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 2aa4cee3f5..1256839379 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -61,10 +61,11 @@ struct Scaler { return CGPoint(x: CGFloat(x.timeIntervalSince(startDate)) * xScale, y: CGFloat(y - glucoseMin) * yScale) } - func rect(for range: WatchDatedRange) -> CGRect { + func rect(for range: WatchDatedRange, minHeight: CGFloat = 2) -> CGRect { let a = point(range.startDate, range.minValue) let b = point(range.endDate, range.maxValue) - let size = CGSize(width: b.x - a.x, height: max(b.y - a.y, 2)) + // Enforce a minimum height so that narrow target ranges still show up: + 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) } } From 5db1eb5f1004e75b14de5725c247d738f0fabffe Mon Sep 17 00:00:00 2001 From: elnjensen Date: Thu, 2 Aug 2018 01:58:11 -0400 Subject: [PATCH 46/51] Rearrange image assets for graph menu --- .../1-hour-graph-38mm.png | Bin 2658 -> 0 bytes .../1-hour-graph-38mm.imageset/Contents.json | 13 ------------ .../1-hour-graph-42mm.png | Bin 2687 -> 0 bytes .../1-hour-graph-42mm.imageset/Contents.json | 13 ------------ .../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 ++++++++++++++++++ .../2-hour-graph-38mm.png | Bin 2969 -> 0 bytes .../2-hour-graph-38mm.imageset/Contents.json | 13 ------------ .../2-hour-graph-42mm.png | Bin 3097 -> 0 bytes .../2-hour-graph-42mm.imageset/Contents.json | 13 ------------ .../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 ++++++++++++++++++ .../3-hour-graph-38mm.png | Bin 3043 -> 0 bytes .../3-hour-graph-38mm.imageset/Contents.json | 13 ------------ .../3-hour-graph-42mm.png | Bin 3157 -> 0 bytes .../3-hour-graph-42mm.imageset/Contents.json | 13 ------------ .../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 ++++++++++++++++++ WatchApp/Base.lproj/Interface.storyboard | 12 +++-------- 22 files changed, 63 insertions(+), 87 deletions(-) delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/1-hour-graph-38mm.png delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.imageset/1-hour-graph-42mm.png delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.imageset/Contents.json 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 delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/2-hour-graph-38mm.png delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/Contents.json delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/2-hour-graph-42mm.png delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/Contents.json 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 delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.imageset/3-hour-graph-38mm.png delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.imageset/Contents.json delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.imageset/3-hour-graph-42mm.png delete mode 100644 WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.imageset/Contents.json 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 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/1-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/1-hour-graph-38mm.png deleted file mode 100644 index 3db2e8a0a2da1c313a8c7a113a315df3c5fd4149..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json deleted file mode 100644 index 577ab9b5d4..0000000000 --- a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.imageset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "1-hour-graph-38mm.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.imageset/1-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.imageset/1-hour-graph-42mm.png deleted file mode 100644 index 63f7579ca6ca6d0617ce634afbe5061e204c8a16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2687 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 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.imageset/2-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/2-hour-graph-38mm.png deleted file mode 100644 index 6f60342c722470b31762a4ea2d6a01da940e2121..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2969 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 MM-!n@tZp0dUz9&!xc~qF diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/Contents.json deleted file mode 100644 index 8c3238626f..0000000000 --- a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.imageset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "2-hour-graph-38mm.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/2-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/2-hour-graph-42mm.png deleted file mode 100644 index a09880e901a1aab3fe182595664e2380514a3de4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3097 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 diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/Contents.json deleted file mode 100644 index 5180b2c3cf..0000000000 --- a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.imageset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "2-hour-graph-42mm.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file 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.imageset/3-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.imageset/3-hour-graph-38mm.png deleted file mode 100644 index d76ad7aa01822c0d732cbd8f204bcc488dcac0e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3043 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 MMFXvJjccL*0u895&Hw-a diff --git a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.imageset/Contents.json deleted file mode 100644 index b265055d94..0000000000 --- a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.imageset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "3-hour-graph-42mm.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file 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/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index 7079506e01..998a9b9ba8 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -389,23 +389,17 @@ - - - + - - - + - - - + From e1c3886a0f3e13202aa2d96184a3ee51a7e10e77 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Thu, 2 Aug 2018 18:46:00 -0400 Subject: [PATCH 47/51] Clarify minimum height on dated range --- WatchApp Extension/Scenes/GlucoseChartScene.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index 1256839379..ab774813c1 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -61,10 +61,10 @@ struct Scaler { return CGPoint(x: CGFloat(x.timeIntervalSince(startDate)) * xScale, y: CGFloat(y - glucoseMin) * yScale) } + // By default enforce a minimum height so that the range is visible func rect(for range: WatchDatedRange, minHeight: CGFloat = 2) -> CGRect { let a = point(range.startDate, range.minValue) let b = point(range.endDate, range.maxValue) - // Enforce a minimum height so that narrow target ranges still show up: 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) } From 3f33e04bec10599420e9704fd4f07cd469ff0688 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Sun, 5 Aug 2018 14:15:37 -0400 Subject: [PATCH 48/51] Don't send expired overrides in watch context --- Loop/Managers/WatchDataManager.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 580e480ccc..9de2660e9e 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -148,6 +148,10 @@ final class WatchDataManager: NSObject, WCSessionDelegate { minValue: override.value.minValue, maxValue: override.value.maxValue ) + + if (context.temporaryOverride?.endDate)! < Date() { + context.temporaryOverride = nil + } } let configuredOverrideContexts = self.deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.configuredOverrideContexts ?? [] From 028c95fd2c96620e93fc50b62b7b56277d760f27 Mon Sep 17 00:00:00 2001 From: elnjensen Date: Sun, 5 Aug 2018 18:44:07 -0400 Subject: [PATCH 49/51] Rearrange testing of override end date --- Loop/Managers/WatchDataManager.swift | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 9de2660e9e..cf66c4bd26 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -142,15 +142,14 @@ final class WatchDataManager: NSObject, WCSessionDelegate { endDate: override.end ) - context.temporaryOverride = WatchDatedRange( - startDate: override.start, - endDate: override.end ?? .distantFuture, - minValue: override.value.minValue, - maxValue: override.value.maxValue - ) - - if (context.temporaryOverride?.endDate)! < Date() { - context.temporaryOverride = nil + let endDate = override.end ?? .distantFuture + if endDate > Date() { + context.temporaryOverride = WatchDatedRange( + startDate: override.start, + endDate: endDate, + minValue: override.value.minValue, + maxValue: override.value.maxValue + ) } } From 17690d960b8fdd9b2d3ce1f6139dfdfb15acc81e Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Wed, 8 Aug 2018 15:41:41 -0700 Subject: [PATCH 50/51] Use Int16 instead of UInt16 to handle negative numbers in predicted glucose --- Common/Models/WatchHistoricalGlucose.swift | 4 ++-- Common/Models/WatchPredictedGlucose.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Common/Models/WatchHistoricalGlucose.swift b/Common/Models/WatchHistoricalGlucose.swift index 0e83fa3f8b..f4214625e6 100644 --- a/Common/Models/WatchHistoricalGlucose.swift +++ b/Common/Models/WatchHistoricalGlucose.swift @@ -28,7 +28,7 @@ extension WatchHistoricalGlucose: RawRepresentable { var rawValue: RawValue { return [ "d": samples.map { $0.date }, - "v": samples.map { UInt16($0.quantity.doubleValue(for: .milligramsPerDeciliter)) }, + "v": samples.map { Int16($0.quantity.doubleValue(for: .milligramsPerDeciliter)) }, "id": samples.map { $0.syncIdentifier } ] } @@ -36,7 +36,7 @@ extension WatchHistoricalGlucose: RawRepresentable { init?(rawValue: RawValue) { guard let dates = rawValue["d"] as? [Date], - let values = rawValue["v"] as? [UInt16], + let values = rawValue["v"] as? [Int16], let syncIdentifiers = rawValue["id"] as? [String], dates.count == values.count, dates.count == syncIdentifiers.count diff --git a/Common/Models/WatchPredictedGlucose.swift b/Common/Models/WatchPredictedGlucose.swift index 1236a3a30d..d801febbfa 100644 --- a/Common/Models/WatchPredictedGlucose.swift +++ b/Common/Models/WatchPredictedGlucose.swift @@ -29,7 +29,7 @@ extension WatchPredictedGlucose: RawRepresentable { var rawValue: RawValue { return [ - "v": values.map { UInt16($0.quantity.doubleValue(for: .milligramsPerDeciliter)) }, + "v": values.map { Int16($0.quantity.doubleValue(for: .milligramsPerDeciliter)) }, "d": values[0].startDate, "i": values[1].startDate.timeIntervalSince(values[0].startDate) ] @@ -37,7 +37,7 @@ extension WatchPredictedGlucose: RawRepresentable { init?(rawValue: RawValue) { guard - let values = rawValue["v"] as? [UInt16], + let values = rawValue["v"] as? [Int16], let firstDate = rawValue["d"] as? Date, let interval = rawValue["i"] as? TimeInterval else { From 604ef63885f5d2f285f16808bcb04cde14d3ca48 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Wed, 15 Aug 2018 10:35:12 -0700 Subject: [PATCH 51/51] Change the appearance of the chart to be platter style to better fit with Watch aesthetics --- .../Scenes/GlucoseChartScene.swift | 24 +++++++------------ WatchApp/Base.lproj/Interface.storyboard | 8 +++---- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift index ab774813c1..4b496314e1 100644 --- a/WatchApp Extension/Scenes/GlucoseChartScene.swift +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -19,8 +19,9 @@ import UIKit extension UIColor { static let glucoseTintColor = UIColor(red: 0 / 255, green: 176 / 255, blue: 255 / 255, alpha: 1) static let gridColor = UIColor(white: 193 / 255, alpha: 1) - static let nowColor = UIColor(white: 193 / 255, alpha: 0.5) + static let nowColor = UIColor(white: 0.2, alpha: 1) static let rangeColor = UIColor(red: 158/255, green: 215/255, blue: 245/255, alpha: 1) + static let backgroundColor = UIColor(white: 0.1, alpha: 1) } extension SKLabelNode { @@ -90,16 +91,16 @@ extension HKUnit { extension WKInterfaceDevice { enum WatchSize { - case Watch38mm - case Watch42mm + case watch38mm + case watch42mm } func watchSize() -> WatchSize { switch screenBounds.width { case 136: - return .Watch38mm + return .watch38mm default: - return .Watch42mm + return .watch42mm } } } @@ -158,23 +159,16 @@ class GlucoseChartScene: SKScene { // Use the fixed sizes specified in the storyboard, based on our guess of the model size super.init(size: { switch WKInterfaceDevice.current().watchSize() { - case .Watch38mm: + case .watch38mm: return CGSize(width: 134, height: 68) - case .Watch42mm: + case .watch42mm: return CGSize(width: 154, height: 86) } }()) anchorPoint = CGPoint(x: 0, y: 0) scaleMode = .aspectFit - backgroundColor = .clear - - let frame = SKShapeNode(rectOf: size, cornerRadius: 0) - frame.position = CGPoint(x: size.width / 2, y: size.height / 2) - frame.lineWidth = 2 - frame.fillColor = .clear - frame.strokeColor = .gridColor - addChild(frame) + backgroundColor = .backgroundColor let dashedPath = CGPath(rect: CGRect(origin: CGPoint(x: size.width / 2, y: 0), size: CGSize(width: 0, height: size.height)), transform: nil).copy(dashingWithPhase: 0, lengths: [4.0, 3.0]) let now = SKShapeNode(path: dashedPath) diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index 998a9b9ba8..3afe28add6 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -368,11 +368,11 @@ - + - - - + + +