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..855f22bd6e --- /dev/null +++ b/Common/Models/WatchDatedRange.swift @@ -0,0 +1,54 @@ +// +// WatchDatedRange.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..f4214625e6 --- /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 { Int16($0.quantity.doubleValue(for: .milligramsPerDeciliter)) }, + "id": samples.map { $0.syncIdentifier } + ] + } + + init?(rawValue: RawValue) { + guard + let dates = rawValue["d"] as? [Date], + let values = rawValue["v"] as? [Int16], + let syncIdentifiers = rawValue["id"] as? [String], + dates.count == values.count, + dates.count == syncIdentifiers.count + else { + return nil + } + + self.samples = (0.. 1 else { + return nil + } + self.values = values + } +} + + +extension WatchPredictedGlucose: RawRepresentable { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + + return [ + "v": values.map { Int16($0.quantity.doubleValue(for: .milligramsPerDeciliter)) }, + "d": values[0].startDate, + "i": values[1].startDate.timeIntervalSince(values[0].startDate) + ] + } + + init?(rawValue: RawValue) { + guard + let values = rawValue["v"] as? [Int16], + let firstDate = rawValue["d"] as? Date, + let interval = rawValue["i"] as? TimeInterval + else { + return nil + } + + self.values = values.enumerated().map { tuple in + PredictedGlucoseValue(startDate: firstDate + Double(tuple.0) * interval, + quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(tuple.1))) + } + } +} diff --git a/Loop Status Extension/Base.lproj/MainInterface.storyboard b/Loop Status Extension/Base.lproj/MainInterface.storyboard index 154bdf9b27..5b5eb74e56 100644 --- a/Loop Status Extension/Base.lproj/MainInterface.storyboard +++ b/Loop Status Extension/Base.lproj/MainInterface.storyboard @@ -1,11 +1,11 @@ - + - + diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index d3d4d73d20..595058300e 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -44,7 +44,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 */; }; @@ -115,7 +115,6 @@ 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */; }; 438172D91F4E9E37003C3328 /* NewPumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438172D81F4E9E37003C3328 /* NewPumpEvent.swift */; }; 43846AD51D8FA67800799272 /* Base.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 43846AD41D8FA67800799272 /* Base.lproj */; }; - 43846ADB1D91057000799272 /* ContextUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43846ADA1D91057000799272 /* ContextUpdatable.swift */; }; 438849EA1D297CB6003B3F23 /* NightscoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438849E91D297CB6003B3F23 /* NightscoutService.swift */; }; 438849EC1D29EC34003B3F23 /* AmplitudeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438849EB1D29EC34003B3F23 /* AmplitudeService.swift */; }; 438849EE1D2A1EBB003B3F23 /* MLabService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438849ED1D2A1EBB003B3F23 /* MLabService.swift */; }; @@ -210,6 +209,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 */; }; @@ -230,6 +233,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 */; }; @@ -242,6 +246,13 @@ 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 */; }; + 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 */; }; @@ -257,10 +268,14 @@ 4FB76FCE1E8C835D00B39636 /* ChartColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB76FCD1E8C835D00B39636 /* ChartColorPalette.swift */; }; 4FC8C8011DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC8C8001DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift */; }; 4FC8C8021DEB943800A1452E /* NSUserDefaults+StatusExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC8C8001DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift */; }; + 4FDDD23720DC51DF00D04B16 /* LoopDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */; }; + 4FF0F75E20E1E5D100FC6291 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431E73471FF95A900069B5F7 /* PersistenceController.swift */; }; + 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 */; }; 7D7076351FE06EDE004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076371FE06EDE004AC8EA /* Localizable.strings */; }; 7D70763A1FE06EDF004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D70763C1FE06EDF004AC8EA /* InfoPlist.strings */; }; 7D70763F1FE06EDF004AC8EA /* ckcomplication.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076411FE06EDF004AC8EA /* ckcomplication.strings */; }; @@ -447,7 +462,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 = ""; }; @@ -517,7 +532,6 @@ 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionTableViewController.swift; sourceTree = ""; }; 438172D81F4E9E37003C3328 /* NewPumpEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPumpEvent.swift; sourceTree = ""; }; 43846AD41D8FA67800799272 /* Base.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base.lproj; sourceTree = ""; }; - 43846ADA1D91057000799272 /* ContextUpdatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextUpdatable.swift; sourceTree = ""; }; 438849E91D297CB6003B3F23 /* NightscoutService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutService.swift; sourceTree = ""; }; 438849EB1D29EC34003B3F23 /* AmplitudeService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmplitudeService.swift; sourceTree = ""; }; 438849ED1D2A1EBB003B3F23 /* MLabService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MLabService.swift; sourceTree = ""; }; @@ -616,6 +630,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 = ""; }; @@ -631,13 +647,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 = ""; }; + 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 = ""; }; + 4F82654F20E69F9A0031A8F5 /* HUDInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDInterfaceController.swift; sourceTree = ""; }; 4FB76FC51E8C57B100B39636 /* StatusChartsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusChartsManager.swift; sourceTree = ""; }; 4FB76FCD1E8C835D00B39636 /* ChartColorPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartColorPalette.swift; sourceTree = ""; }; 4FC8C8001DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSUserDefaults+StatusExtension.swift"; sourceTree = ""; }; + 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopDataManager.swift; sourceTree = ""; }; 4FF4D0FF1E18374700846527 /* WatchContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchContext.swift; sourceTree = ""; }; + 4FFEDFBE20E5CF22000BFC58 /* ChartHUDController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartHUDController.swift; sourceTree = ""; }; 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 = ""; }; 7D68AAAB1FE2DB0A00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/MainInterface.strings; sourceTree = ""; }; @@ -771,9 +795,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 = ""; @@ -783,6 +808,8 @@ children = ( 4344629120A7C19800C4BE6F /* ButtonGroup.swift */, 4328E0221CFBE2C500E199AA /* CLKComplicationTemplate.swift */, + 4F7E8AC420E2AB9600AEA65E /* Date.swift */, + 4F73F5FB20E2E7FA00E8D82C /* GlucoseStore.swift */, 4328E0201CFBE2C500E199AA /* IdentifiableClass.swift */, 4328E0231CFBE2C500E199AA /* NSUserDefaults.swift */, 4328E0241CFBE2C500E199AA /* UIColor.swift */, @@ -928,6 +955,8 @@ 43A9438F1B926B7B0051FA24 /* Assets.xcassets */, 4328E0121CFBE1B700E199AA /* Controllers */, 4328E01F1CFBE2B100E199AA /* Extensions */, + 4FE3475F20D5D7FA00A86D03 /* Managers */, + 4F75F0052100146B00B5570E /* Scenes */, 43A943831B926B7B0051FA24 /* Supporting Files */, ); path = "WatchApp Extension"; @@ -1141,6 +1170,14 @@ path = Models; sourceTree = ""; }; + 4F75F0052100146B00B5570E /* Scenes */ = { + isa = PBXGroup; + children = ( + 4F75F00120FCFE8C00B5570E /* GlucoseChartScene.swift */, + ); + path = Scenes; + sourceTree = ""; + }; 4FB76FC31E8C575900B39636 /* Managers */ = { isa = PBXGroup; children = ( @@ -1159,6 +1196,14 @@ path = Extensions; sourceTree = ""; }; + 4FE3475F20D5D7FA00A86D03 /* Managers */ = { + isa = PBXGroup; + children = ( + 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */, + ); + path = Managers; + sourceTree = ""; + }; 4FF4D0FA1E1834BD00846527 /* Common */ = { isa = PBXGroup; children = ( @@ -1174,6 +1219,7 @@ 43673E2E1F37BDA10058AC7C /* Insulin */, 435400301C9F744E00D5819C /* BolusSuggestionUserInfo.swift */, 43DE92581C5479E4001FFDE1 /* CarbEntryUserInfo.swift */, + 4F11D3BF20DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift */, 4309786D1E73DAD100BEBC82 /* CGM.swift */, 894B91CC1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift */, 430B298D2041F56500BA9F93 /* GlucoseThreshold.swift */, @@ -1181,6 +1227,9 @@ 435400331C9F878D00D5819C /* SetBolusUserInfo.swift */, 4F70C2111DE900EA006380B7 /* StatusExtensionContext.swift */, 4FF4D0FF1E18374700846527 /* WatchContext.swift */, + 4F11D3C120DD80B3006E072C /* WatchHistoricalGlucose.swift */, + 4F7E8AC620E2AC0300AEA65E /* WatchPredictedGlucose.swift */, + 4F7E8AC820E2AC3700AEA65E /* WatchDatedRange.swift */, ); path = Models; sourceTree = ""; @@ -1704,6 +1753,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 */, @@ -1740,6 +1790,7 @@ 4309786E1E73DAD100BEBC82 /* CGM.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 */, @@ -1767,8 +1818,10 @@ 43CEE6E61E56AFD400CB9116 /* NightscoutUploader.swift in Sources */, 4328E0331CFC091100E199AA /* WatchContext+LoopKit.swift in Sources */, 4F526D611DF8D9A900A04910 /* NetBasal.swift in Sources */, + 4F7E8ACB20E2ACB500AEA65E /* WatchPredictedGlucose.swift in Sources */, 43C3B6EC20B650A80026CAFA /* SettingsImageTableViewCell.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 */, @@ -1805,27 +1858,38 @@ 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 */, 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 */, 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 df3329f72a..cf66c4bd26 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 deviceManager: 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.deviceManager.sensorInfo?.trendType?.rawValue context.cgm = self.deviceManager.cgm @@ -135,17 +141,61 @@ final class WatchDataManager: NSObject, WCSessionDelegate { startDate: override.start, endDate: override.end ) + + let endDate = override.end ?? .distantFuture + if endDate > Date() { + context.temporaryOverride = WatchDatedRange( + startDate: override.start, + endDate: endDate, + minValue: override.value.minValue, + maxValue: override.value.maxValue + ) + } } let configuredOverrideContexts = self.deviceManager.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 + } + + // 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)) } if let trend = self.deviceManager.sensorInfo?.trendType { context.glucoseTrendRawValue = trend.rawValue } + _ = updateGroup.wait(timeout: .distantFuture) completion(context) } } @@ -193,23 +243,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 = deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.setOverride(overrideContext, from: overrideUserInfo.startDate, until: overrideUserInfo.effectiveEndDate) - - if overrideSuccess == false { - lastActiveOverrideContext = previousActiveOverrideContext - } - - replyHandler([:]) + _ = deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.setOverride(overrideUserInfo.context.correspondingOverrideContext, from: overrideUserInfo.startDate, until: overrideUserInfo.effectiveEndDate) } else { - lastActiveOverrideContext = nil deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.clearOverride() + } + replyHandler([:]) + case GlucoseBackfillRequestUserInfo.name?: + if let userInfo = GlucoseBackfillRequestUserInfo(rawValue: message), + let manager = deviceManager.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..8386b11f20 100644 --- a/WatchApp Extension/Controllers/StatusInterfaceController.swift +++ b/WatchApp Extension/Controllers/ActionHUDController.swift @@ -1,5 +1,5 @@ // -// StatusInterfaceController.swift +// ActionHUDController.swift // Loop // // Created by Nathan Racklyeft on 5/29/16. @@ -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..2a896c429a --- /dev/null +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -0,0 +1,160 @@ +// +// ChartHUDController.swift +// Loop +// +// Created by Bharat Mediratta on 6/26/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import WatchKit +import WatchConnectivity +import CGMBLEKit +import LoopKit +import SpriteKit + +final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { + @IBOutlet weak var basalLabel: WKInterfaceLabel! + @IBOutlet weak var iobLabel: WKInterfaceLabel! + @IBOutlet weak var cobLabel: WKInterfaceLabel! + @IBOutlet weak var glucoseScene: WKInterfaceSKScene! + @IBAction func setChartWindow1Hour() { + scene.visibleHours = 1 + } + @IBAction func setChartWindow2Hours() { + scene.visibleHours = 2 + } + @IBAction func setChartWindow3Hours() { + scene.visibleHours = 3 + } + private let scene = GlucoseChartScene() + + override init() { + super.init() + + loopManager = ExtensionDelegate.shared().loopManager + NotificationCenter.default.addObserver(forName: .GlucoseSamplesDidChange, object: loopManager?.glucoseStore, queue: nil) { _ in + DispatchQueue.main.async { + self.updateGlucoseChart() + } + } + + glucoseScene.presentScene(scene) + } + + 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. + // + // TODO: possibly because I'm not calling super.awake()? investigate that. + DispatchQueue.main.async { + self.didAppear() + } + } + } + + override func didAppear() { + super.didAppear() + } + + override func willActivate() { + crownSequencer.delegate = self + crownSequencer.focus() + + super.willActivate() + + loopManager?.glucoseStore.maybeRequestGlucoseBackfill() + glucoseScene.isPaused = false + } + + 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 + } + + scene.predictedGlucose = activeContext.predictedGlucose?.values + scene.targetRanges = activeContext.targetRanges + scene.temporaryOverride = activeContext.temporaryOverride + scene.unit = activeContext.preferredGlucoseUnit + + loopManager?.glucoseStore.getCachedGlucoseSamples(start: .EarliestGlucoseCutoff) { (samples) in + DispatchQueue.main.async { + self.scene.historicalGlucose = samples + self.scene.updateNodes(animated: false) + } + } + } + + // MARK: WKCrownDelegate + var crownAccumulator = 0.0 + + func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) { + crownAccumulator += rotationalDelta + if abs(crownAccumulator) >= 0.25 { + scene.visibleBg += Int(sign(crownAccumulator)) + crownAccumulator = 0 + } + } +} 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..45a34b59f7 --- /dev/null +++ b/WatchApp Extension/Managers/LoopDataManager.swift @@ -0,0 +1,31 @@ +// +// LoopDataManager.swift +// WatchApp Extension +// +// Created by Bharat Mediratta on 6/21/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation +import HealthKit +import LoopKit + +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(4)) + } +} + +extension Notification.Name { + static let ContextUpdated = Notification.Name(rawValue: "com.loopkit.notification.ContextUpdated") +} diff --git a/WatchApp Extension/Scenes/GlucoseChartScene.swift b/WatchApp Extension/Scenes/GlucoseChartScene.swift new file mode 100644 index 0000000000..4b496314e1 --- /dev/null +++ b/WatchApp Extension/Scenes/GlucoseChartScene.swift @@ -0,0 +1,298 @@ +// +// GlucoseChartScene.swift +// WatchApp Extension +// +// Created by Bharat Mediratta on 7/16/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation +import SpriteKit +import HealthKit +import LoopKit +import WatchKit +import UIKit + + +// 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: 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 { + static func basic(at position: CGPoint) -> SKLabelNode { + let basic = SKLabelNode(text: "--") + basic.fontSize = 12 + basic.fontName = "HelveticaNeue" + basic.fontColor = .white + basic.alpha = 0.8 + basic.verticalAlignmentMode = .top + basic.horizontalAlignmentMode = .left + basic.position = position + return basic + } +} + +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 + let xScale: CGFloat + let yScale: CGFloat + + func point(_ x: Date, _ y: Double) -> CGPoint { + 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) + 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) + } +} + +extension HKUnit { + var highWatermarkRange: [Double] { + if unitString == "mg/dL" { + return [150.0, 200.0, 250.0, 300.0, 350.0, 400.0] + } else { + return [8.0, 11.0, 14.0, 17.0, 20.0, 23.0] + } + } + + var lowWatermark: Double { + if unitString == "mg/dL" { + return 50.0 + } else { + return 3.0 + } + } +} + +extension WKInterfaceDevice { + enum WatchSize { + case watch38mm + case watch42mm + } + + func watchSize() -> WatchSize { + switch screenBounds.width { + case 136: + return .watch38mm + default: + return .watch42mm + } + } +} + +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? + var historicalGlucose: [SampleValue]? + var predictedGlucose: [SampleValue]? + var targetRanges: [WatchDatedRange]? + + var visibleBg: Int = 1 { + didSet { + if let range = unit?.highWatermarkRange, (0.. SKSpriteNode { + if nodes[hashValue] == nil { + nodes[hashValue] = SKSpriteNode(color: .clear, size: CGSize(width: 0, height: 0)) + addChild(nodes[hashValue]!) + } + return nodes[hashValue]! + } + + func updateNodes(animated: Bool) { + dispatchPrecondition(condition: .onQueue(.main)) + + guard let unit = unit else { + return + } + + let window = TimeInterval(hours: Double(visibleHours)) + let scaler = Scaler(startDate: Date() - window, + glucoseMin: unit.lowWatermark, + xScale: size.width / CGFloat(window * 2), + yScale: size.height / CGFloat(unit.highWatermarkRange[visibleBg] - unit.lowWatermark)) + + + let numberFormatter = NumberFormatter.glucoseFormatter(for: unit) + minBGLabel.text = numberFormatter.string(from: unit.lowWatermark) + maxBGLabel.text = numberFormatter.string(from: unit.highWatermarkRange[visibleBg]) + hoursLabel.text = "\(Int(visibleHours))h" + + // 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.4 : 0.6) + sprite.move(to: scaler.rect(for: range), animated: animated) + 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, 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) + 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) + inactiveNodes.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) + let sprite = getSprite(forHash: $0.hashValue) + sprite.color = .glucoseTintColor + sprite.move(to: CGRect(origin: origin, size: size), animated: animated) + inactiveNodes.removeValue(forKey: $0.hashValue) + } + + predictedPathNode?.removeFromParent() + if let predictedGlucose = predictedGlucose, predictedGlucose.count > 2 { + let predictedPath = CGMutablePath() + predictedPath.addLines(between: predictedGlucose.map { + scaler.point($0.startDate, $0.quantity.doubleValue(for: unit)) + }) + + predictedPathNode = SKShapeNode(path: predictedPath.copy(dashingWithPhase: 11, lengths: [5, 3])) + 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), + SKAction.fadeIn(withDuration: 0.75) + ]), withKey: "move") + } + } + + // Any inactive nodes can be safely removed + inactiveNodes.forEach { hash, node in + node.removeFromParent() + nodes.removeValue(forKey: hash) + } + + isPaused = false + } +} 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/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 0000000000..3db2e8a0a2 Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-38mm.png differ 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 0000000000..63f7579ca6 Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph-42mm.png differ diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/1-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/1-hour-graph-38mm.png new file mode 100644 index 0000000000..ab36b57710 Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/1-hour-graph-38mm.png differ 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 0000000000..c8f39f9905 Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/1-hour-graph-42mm.png differ diff --git a/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json new file mode 100644 index 0000000000..e835ece7e8 --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/1-hour-graph.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "watch", + "filename" : "1-hour-graph-38mm.png", + "screen-width" : "<=145", + "scale" : "2x" + }, + { + "idiom" : "watch", + "filename" : "1-hour-graph-42mm.png", + "screen-width" : ">145", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.png new file mode 100644 index 0000000000..6f60342c72 Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-38mm.png differ diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.png b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.png new file mode 100644 index 0000000000..a09880e901 Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph-42mm.png differ 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 0000000000..f4ff6ee730 Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/2-hour-graph-38mm.png differ 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 0000000000..c43ed36fab Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/2-hour-graph-42mm.png differ diff --git a/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/Contents.json new file mode 100644 index 0000000000..b2b2284ab7 --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/2-hour-graph.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "watch", + "filename" : "2-hour-graph-38mm.png", + "screen-width" : "<=145", + "scale" : "2x" + }, + { + "idiom" : "watch", + "filename" : "2-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/3-hour-graph-38mm.png b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.png new file mode 100644 index 0000000000..d76ad7aa01 Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-38mm.png differ 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 0000000000..c7474e6a2d Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph-42mm.png differ 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 0000000000..6f18861f4c Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/3-hour-graph-38mm.png differ 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 0000000000..b4bd9938b5 Binary files /dev/null and b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/3-hour-graph-42mm.png differ diff --git a/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json new file mode 100644 index 0000000000..17343c647c --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/3-hour-graph.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "watch", + "filename" : "3-hour-graph-38mm.png", + "screen-width" : "<=145", + "scale" : "2x" + }, + { + "idiom" : "watch", + "filename" : "3-hour-graph-42mm.png", + "screen-width" : ">145", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/Graph menu icons/Contents.json b/WatchApp/Assets.xcassets/Graph menu icons/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/WatchApp/Assets.xcassets/Graph menu icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index f2b392d588..3afe28add6 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,96 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -359,7 +445,7 @@ - +