diff --git a/Cartfile.resolved b/Cartfile.resolved
index 5cf45ecb56..86f3375621 100644
--- a/Cartfile.resolved
+++ b/Cartfile.resolved
@@ -2,7 +2,7 @@ github "amplitude/Amplitude-iOS" "v3.8.5"
github "loudnate/Crypto" "13fee45175b88629aeabe60b4b4fc3daf86fa0a3"
github "mddub/G4ShareSpy" "v0.2.2"
github "loudnate/LoopKit" "v0.7.0"
-github "loudnate/SwiftCharts" "8671287afb29640f9cffced6521b1098b7aac085"
+github "loudnate/SwiftCharts" "0c58586ab36a9f358b5fff281f52b7528fe2dc5e"
github "mddub/dexcom-share-client-swift" "v0.1.3"
github "loudnate/xDripG5" "v0.6.0"
github "ps2/rileylink_ios" "v0.11.2"
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Info.plist b/Carthage/Build/iOS/SwiftCharts.framework/Info.plist
index 9542eff643..be98d53443 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Info.plist and b/Carthage/Build/iOS/SwiftCharts.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftdoc
index 4da4d1f086..f7613d6671 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftmodule
index 9598adf2a1..f07c2d1139 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftdoc
index 0994a2a3d7..b2de65033f 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftmodule
index f46efeb182..9282c513c3 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftdoc
index fc25fd072b..26b3778e52 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftmodule
index 1a2fa61cf5..a34fb834dc 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftdoc
index 745570f489..02d5829d5d 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftmodule
index 1860499412..e52c2822af 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts b/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts
index 944f529fef..9afcad7f3f 100755
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts and b/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts differ
diff --git a/DoseMathTests/Info.plist b/DoseMathTests/Info.plist
index a626cd463b..31b423aaa3 100644
--- a/DoseMathTests/Info.plist
+++ b/DoseMathTests/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 0.11.2
+ 0.12.0
CFBundleSignature
????
CFBundleVersion
diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj
index e0385baae6..aba48cb9fb 100644
--- a/Loop.xcodeproj/project.pbxproj
+++ b/Loop.xcodeproj/project.pbxproj
@@ -52,6 +52,7 @@
4354003A1C9FB81100D5819C /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DE92501C541832001FFDE1 /* UIColor.swift */; };
43649A631C7A347F00523D7F /* CollectionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43649A621C7A347F00523D7F /* CollectionType.swift */; };
436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0DA41D236A2A00104B24 /* LoopError.swift */; };
+ 436A0E7B1D7DE13400D6475D /* NSNumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0E7A1D7DE13400D6475D /* NSNumberFormatter.swift */; };
436FACEE1D0BA636004E2427 /* InsulinDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436FACED1D0BA636004E2427 /* InsulinDataSource.swift */; };
43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43776F8F1B8022E90074EA36 /* AppDelegate.swift */; };
43776F971B8022E90074EA36 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43776F951B8022E90074EA36 /* Main.storyboard */; };
@@ -67,9 +68,12 @@
437CEEC81CD84CBB003C8C80 /* ReservoirVolumeHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEC71CD84CBB003C8C80 /* ReservoirVolumeHUDView.swift */; };
437CEECA1CD84DB7003C8C80 /* BatteryLevelHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEC91CD84DB7003C8C80 /* BatteryLevelHUDView.swift */; };
437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEE31CDE5C0A003C8C80 /* UIImage.swift */; };
+ 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437D9BA21D7BC977007245E8 /* PredictionTableViewController.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 */; };
+ 438D42F91D7C88BC003244B0 /* PredictionInputEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438D42F81D7C88BC003244B0 /* PredictionInputEffect.swift */; };
+ 438D42FB1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438D42FA1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift */; };
438DADC81CDE8F8B007697A5 /* LoopStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438DADC71CDE8F8B007697A5 /* LoopStateView.swift */; };
439897351CD2F7DE00223065 /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; };
439897371CD2F80600223065 /* AnalyticsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897361CD2F80600223065 /* AnalyticsManager.swift */; };
@@ -140,6 +144,7 @@
43F78D4D1C914197002152D1 /* GlucoseKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F78D491C914197002152D1 /* GlucoseKit.framework */; };
43F78D4E1C914197002152D1 /* InsulinKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F78D4A1C914197002152D1 /* InsulinKit.framework */; };
43F78D4F1C914197002152D1 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F78D4B1C914197002152D1 /* LoopKit.framework */; };
+ 43FBEDD81D73843700B21F22 /* LevelMaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43FBEDD71D73843700B21F22 /* LevelMaskView.swift */; };
4D3B40041D4A9E1A00BC6334 /* G4ShareSpy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D3B40021D4A9DFE00BC6334 /* G4ShareSpy.framework */; };
4D5B7A4B1D457CCA00796CA9 /* GlucoseG4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5B7A4A1D457CCA00796CA9 /* GlucoseG4.swift */; };
C10428971D17BAD400DD539A /* NightscoutUploadKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */; };
@@ -270,6 +275,7 @@
435400331C9F878D00D5819C /* SetBolusUserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetBolusUserInfo.swift; sourceTree = ""; };
43649A621C7A347F00523D7F /* CollectionType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionType.swift; sourceTree = ""; };
436A0DA41D236A2A00104B24 /* LoopError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopError.swift; sourceTree = ""; };
+ 436A0E7A1D7DE13400D6475D /* NSNumberFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSNumberFormatter.swift; sourceTree = ""; };
436FACED1D0BA636004E2427 /* InsulinDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDataSource.swift; sourceTree = ""; };
43776F8C1B8022E90074EA36 /* Loop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Loop.app; sourceTree = BUILT_PRODUCTS_DIR; };
43776F8F1B8022E90074EA36 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
@@ -287,9 +293,12 @@
437CEEC91CD84DB7003C8C80 /* BatteryLevelHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryLevelHUDView.swift; sourceTree = ""; };
437CEEE31CDE5C0A003C8C80 /* UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; };
437D9BA11D7B5203007245E8 /* Loop.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Loop.xcconfig; sourceTree = ""; };
+ 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionTableViewController.swift; sourceTree = ""; };
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 = ""; };
+ 438D42F81D7C88BC003244B0 /* PredictionInputEffect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionInputEffect.swift; sourceTree = ""; };
+ 438D42FA1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionInputEffectTableViewCell.swift; sourceTree = ""; };
438DADC71CDE8F8B007697A5 /* LoopStateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopStateView.swift; sourceTree = ""; };
439897341CD2F7DE00223065 /* NSTimeInterval.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSTimeInterval.swift; sourceTree = ""; };
439897361CD2F80600223065 /* AnalyticsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AnalyticsManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
@@ -365,6 +374,7 @@
43F78D491C914197002152D1 /* GlucoseKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GlucoseKit.framework; path = Carthage/Build/iOS/GlucoseKit.framework; sourceTree = ""; };
43F78D4A1C914197002152D1 /* InsulinKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InsulinKit.framework; path = Carthage/Build/iOS/InsulinKit.framework; sourceTree = ""; };
43F78D4B1C914197002152D1 /* LoopKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LoopKit.framework; path = Carthage/Build/iOS/LoopKit.framework; sourceTree = ""; };
+ 43FBEDD71D73843700B21F22 /* LevelMaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LevelMaskView.swift; sourceTree = ""; };
4D3B40021D4A9DFE00BC6334 /* G4ShareSpy.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = G4ShareSpy.framework; path = Carthage/Build/iOS/G4ShareSpy.framework; sourceTree = ""; };
4D5B7A4A1D457CCA00796CA9 /* GlucoseG4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GlucoseG4.swift; path = Loop/Models/GlucoseG4.swift; sourceTree = SOURCE_ROOT; };
C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NightscoutUploadKit.framework; path = Carthage/Build/iOS/NightscoutUploadKit.framework; sourceTree = ""; };
@@ -465,6 +475,7 @@
438849ED1D2A1EBB003B3F23 /* MLabService.swift */,
430DA58F1D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift */,
438849E91D297CB6003B3F23 /* NightscoutService.swift */,
+ 438D42F81D7C88BC003244B0 /* PredictionInputEffect.swift */,
43EA28611D517E42001BC233 /* SensorDisplayable.swift */,
437CCADF1D285C7B0075D2C3 /* ServiceAuthentication.swift */,
434F54601D28859B002A9274 /* ServiceCredential.swift */,
@@ -597,6 +608,7 @@
43E344A01B9E144300C85C07 /* Extensions */ = {
isa = PBXGroup;
children = (
+ C17884621D51A7A400405663 /* BatteryIndicator.swift */,
4331E0771C85302200FBE832 /* CGPoint.swift */,
4346D1F51C78501000ABAFE3 /* ChartPoint.swift */,
43649A621C7A347F00523D7F /* CollectionType.swift */,
@@ -607,13 +619,13 @@
4302F4DA1D4D6E9F00F0FCAF /* NSData.swift */,
43CE7CDD1CA8B63E003CC1B0 /* NSDate.swift */,
4398973A1CD2FC2000223065 /* NSDateFormatter.swift */,
+ 436A0E7A1D7DE13400D6475D /* NSNumberFormatter.swift */,
439897341CD2F7DE00223065 /* NSTimeInterval.swift */,
43E344A31B9E1B1C00C85C07 /* NSUserDefaults.swift */,
43F41C361D3BF32400C11ED6 /* UIAlertController.swift */,
43DE92501C541832001FFDE1 /* UIColor.swift */,
437CEEE31CDE5C0A003C8C80 /* UIImage.swift */,
434FF1ED1CF27EEF000DB779 /* UITableViewCell.swift */,
- C17884621D51A7A400405663 /* BatteryIndicator.swift */,
);
path = Extensions;
sourceTree = "";
@@ -626,6 +638,7 @@
4315D2861CA5CC3B00589052 /* CarbEntryEditTableViewController.swift */,
43DBF0581C93F73800B3C386 /* CarbEntryTableViewController.swift */,
4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */,
+ 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */,
43F5173C1D713DB0000FA422 /* RadioSelectionTableViewController.swift */,
43F5C2DA1B92A5E1003EB13D /* SettingsTableViewController.swift */,
43E3449E1B9D68E900C85C07 /* StatusTableViewController.swift */,
@@ -648,8 +661,10 @@
4346D1E61C77F5FE00ABAFE3 /* ChartTableViewCell.swift */,
4337615E1D52F487004A3647 /* GlucoseHUDView.swift */,
437CEEBB1CD6DE6A003C8C80 /* HUDView.swift */,
+ 43FBEDD71D73843700B21F22 /* LevelMaskView.swift */,
437CEEBD1CD6E0CB003C8C80 /* LoopCompletionHUDView.swift */,
438DADC71CDE8F8B007697A5 /* LoopStateView.swift */,
+ 438D42FA1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift */,
437CEEC71CD84CBB003C8C80 /* ReservoirVolumeHUDView.swift */,
43A5676A1C96155700334FAC /* SwitchTableViewCell.swift */,
434F54621D28DD80002A9274 /* ValidatingIndicatorView.swift */,
@@ -1028,9 +1043,12 @@
4D5B7A4B1D457CCA00796CA9 /* GlucoseG4.swift in Sources */,
438849EC1D29EC34003B3F23 /* AmplitudeService.swift in Sources */,
435400341C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */,
+ 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */,
43F41C371D3BF32400C11ED6 /* UIAlertController.swift in Sources */,
437CEEBC1CD6DE6A003C8C80 /* HUDView.swift in Sources */,
+ 436A0E7B1D7DE13400D6475D /* NSNumberFormatter.swift in Sources */,
434F545F1D288345002A9274 /* ShareService.swift in Sources */,
+ 43FBEDD81D73843700B21F22 /* LevelMaskView.swift in Sources */,
4354003A1C9FB81100D5819C /* UIColor.swift in Sources */,
4328E0331CFC091100E199AA /* WatchContext+LoopKit.swift in Sources */,
4398973B1CD2FC2000223065 /* NSDateFormatter.swift in Sources */,
@@ -1040,6 +1058,7 @@
4302F4DB1D4D6E9F00F0FCAF /* NSData.swift in Sources */,
437CEECA1CD84DB7003C8C80 /* BatteryLevelHUDView.swift in Sources */,
43F78D261C8FC000002152D1 /* DoseMath.swift in Sources */,
+ 438D42F91D7C88BC003244B0 /* PredictionInputEffect.swift in Sources */,
4331E07A1C85650D00FBE832 /* ChartAxisValueDoubleLog.swift in Sources */,
434F54611D28859B002A9274 /* ServiceCredential.swift in Sources */,
436FACEE1D0BA636004E2427 /* InsulinDataSource.swift in Sources */,
@@ -1048,6 +1067,7 @@
43EA28601D50ED4D001BC233 /* GlucoseTrend.swift in Sources */,
4337615F1D52F487004A3647 /* GlucoseHUDView.swift in Sources */,
438849EE1D2A1EBB003B3F23 /* MLabService.swift in Sources */,
+ 438D42FB1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift in Sources */,
43F4EF1D1BA2A57600526CE1 /* DiagnosticLogger.swift in Sources */,
432E73CB1D24B3D6009AD15D /* RemoteDataManager.swift in Sources */,
438DADC81CDE8F8B007697A5 /* LoopStateView.swift in Sources */,
diff --git a/Loop/Assets.xcassets/battery/battery.imageset/Contents.json b/Loop/Assets.xcassets/battery/battery.imageset/Contents.json
new file mode 100644
index 0000000000..898b4aa559
--- /dev/null
+++ b/Loop/Assets.xcassets/battery/battery.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "battery.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/battery/battery.imageset/battery.pdf b/Loop/Assets.xcassets/battery/battery.imageset/battery.pdf
new file mode 100644
index 0000000000..ca8bffbb67
Binary files /dev/null and b/Loop/Assets.xcassets/battery/battery.imageset/battery.pdf differ
diff --git a/Loop/Assets.xcassets/battery/battery_0.imageset/Contents.json b/Loop/Assets.xcassets/battery/battery_0.imageset/Contents.json
deleted file mode 100644
index 3946374283..0000000000
--- a/Loop/Assets.xcassets/battery/battery_0.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "battery_0@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/battery/battery_0.imageset/battery_0@2x.png b/Loop/Assets.xcassets/battery/battery_0.imageset/battery_0@2x.png
deleted file mode 100644
index 09e2ebfd7d..0000000000
Binary files a/Loop/Assets.xcassets/battery/battery_0.imageset/battery_0@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/battery/battery_100.imageset/Contents.json b/Loop/Assets.xcassets/battery/battery_100.imageset/Contents.json
deleted file mode 100644
index 89e1fb18ce..0000000000
--- a/Loop/Assets.xcassets/battery/battery_100.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "battery_100@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/battery/battery_100.imageset/battery_100@2x.png b/Loop/Assets.xcassets/battery/battery_100.imageset/battery_100@2x.png
deleted file mode 100644
index 37ad1a6f6d..0000000000
Binary files a/Loop/Assets.xcassets/battery/battery_100.imageset/battery_100@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/battery/battery_25.imageset/Contents.json b/Loop/Assets.xcassets/battery/battery_25.imageset/Contents.json
deleted file mode 100644
index d55cea55b9..0000000000
--- a/Loop/Assets.xcassets/battery/battery_25.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "battery_25@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/battery/battery_25.imageset/battery_25@2x.png b/Loop/Assets.xcassets/battery/battery_25.imageset/battery_25@2x.png
deleted file mode 100644
index 0e72d46235..0000000000
Binary files a/Loop/Assets.xcassets/battery/battery_25.imageset/battery_25@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/battery/battery_50.imageset/Contents.json b/Loop/Assets.xcassets/battery/battery_50.imageset/Contents.json
deleted file mode 100644
index 08fd5a46b6..0000000000
--- a/Loop/Assets.xcassets/battery/battery_50.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "battery_50@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/battery/battery_50.imageset/battery_50@2x.png b/Loop/Assets.xcassets/battery/battery_50.imageset/battery_50@2x.png
deleted file mode 100644
index 05b2bdaf21..0000000000
Binary files a/Loop/Assets.xcassets/battery/battery_50.imageset/battery_50@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/battery/battery_75.imageset/Contents.json b/Loop/Assets.xcassets/battery/battery_75.imageset/Contents.json
deleted file mode 100644
index 8b58057f79..0000000000
--- a/Loop/Assets.xcassets/battery/battery_75.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "battery_75@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/battery/battery_75.imageset/battery_75@2x.png b/Loop/Assets.xcassets/battery/battery_75.imageset/battery_75@2x.png
deleted file mode 100644
index 8bf86a19b3..0000000000
Binary files a/Loop/Assets.xcassets/battery/battery_75.imageset/battery_75@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/battery/battery_mask.imageset/Contents.json b/Loop/Assets.xcassets/battery/battery_mask.imageset/Contents.json
new file mode 100644
index 0000000000..a089e35498
--- /dev/null
+++ b/Loop/Assets.xcassets/battery/battery_mask.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "battery_mask.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/battery/battery_mask.imageset/battery_mask.pdf b/Loop/Assets.xcassets/battery/battery_mask.imageset/battery_mask.pdf
new file mode 100644
index 0000000000..a580753697
Binary files /dev/null and b/Loop/Assets.xcassets/battery/battery_mask.imageset/battery_mask.pdf differ
diff --git a/Loop/Assets.xcassets/battery/battery_unknown.imageset/Contents.json b/Loop/Assets.xcassets/battery/battery_unknown.imageset/Contents.json
deleted file mode 100644
index 1e0bfe994d..0000000000
--- a/Loop/Assets.xcassets/battery/battery_unknown.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "battery_unknown@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/battery/battery_unknown.imageset/battery_unknown@2x.png b/Loop/Assets.xcassets/battery/battery_unknown.imageset/battery_unknown@2x.png
deleted file mode 100644
index a778ae850c..0000000000
Binary files a/Loop/Assets.xcassets/battery/battery_unknown.imageset/battery_unknown@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/bolus.imageset/bolus.pdf b/Loop/Assets.xcassets/bolus.imageset/bolus.pdf
index 7ac8d44d76..c9e69b5cfc 100644
Binary files a/Loop/Assets.xcassets/bolus.imageset/bolus.pdf and b/Loop/Assets.xcassets/bolus.imageset/bolus.pdf differ
diff --git a/Loop/Assets.xcassets/carbs.imageset/carbs.pdf b/Loop/Assets.xcassets/carbs.imageset/carbs.pdf
index 0fce5ebd16..327ec631b5 100644
Binary files a/Loop/Assets.xcassets/carbs.imageset/carbs.pdf and b/Loop/Assets.xcassets/carbs.imageset/carbs.pdf differ
diff --git a/Loop/Assets.xcassets/reservoir/reservoir.imageset/Contents.json b/Loop/Assets.xcassets/reservoir/reservoir.imageset/Contents.json
new file mode 100644
index 0000000000..aba98e7f10
--- /dev/null
+++ b/Loop/Assets.xcassets/reservoir/reservoir.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "reservoir.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/reservoir/reservoir.imageset/reservoir.pdf b/Loop/Assets.xcassets/reservoir/reservoir.imageset/reservoir.pdf
new file mode 100644
index 0000000000..9f1765ba9b
Binary files /dev/null and b/Loop/Assets.xcassets/reservoir/reservoir.imageset/reservoir.pdf differ
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_0.imageset/Contents.json b/Loop/Assets.xcassets/reservoir/reservoir_0.imageset/Contents.json
deleted file mode 100644
index 795a5ba302..0000000000
--- a/Loop/Assets.xcassets/reservoir/reservoir_0.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "reservoir_0@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_0.imageset/reservoir_0@2x.png b/Loop/Assets.xcassets/reservoir/reservoir_0.imageset/reservoir_0@2x.png
deleted file mode 100644
index 75faceec7e..0000000000
Binary files a/Loop/Assets.xcassets/reservoir/reservoir_0.imageset/reservoir_0@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_100.imageset/Contents.json b/Loop/Assets.xcassets/reservoir/reservoir_100.imageset/Contents.json
deleted file mode 100644
index bb4d75e814..0000000000
--- a/Loop/Assets.xcassets/reservoir/reservoir_100.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "reservoir_100@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_100.imageset/reservoir_100@2x.png b/Loop/Assets.xcassets/reservoir/reservoir_100.imageset/reservoir_100@2x.png
deleted file mode 100644
index d7504b87de..0000000000
Binary files a/Loop/Assets.xcassets/reservoir/reservoir_100.imageset/reservoir_100@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_25.imageset/Contents.json b/Loop/Assets.xcassets/reservoir/reservoir_25.imageset/Contents.json
deleted file mode 100644
index 3e8bde606f..0000000000
--- a/Loop/Assets.xcassets/reservoir/reservoir_25.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "reservoir_25@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_25.imageset/reservoir_25@2x.png b/Loop/Assets.xcassets/reservoir/reservoir_25.imageset/reservoir_25@2x.png
deleted file mode 100644
index d8bdee37d4..0000000000
Binary files a/Loop/Assets.xcassets/reservoir/reservoir_25.imageset/reservoir_25@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_50.imageset/Contents.json b/Loop/Assets.xcassets/reservoir/reservoir_50.imageset/Contents.json
deleted file mode 100644
index 5e0ee1539b..0000000000
--- a/Loop/Assets.xcassets/reservoir/reservoir_50.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "reservoir_50@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_50.imageset/reservoir_50@2x.png b/Loop/Assets.xcassets/reservoir/reservoir_50.imageset/reservoir_50@2x.png
deleted file mode 100644
index 4be21f0d3d..0000000000
Binary files a/Loop/Assets.xcassets/reservoir/reservoir_50.imageset/reservoir_50@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_75.imageset/Contents.json b/Loop/Assets.xcassets/reservoir/reservoir_75.imageset/Contents.json
deleted file mode 100644
index c409f7a5ca..0000000000
--- a/Loop/Assets.xcassets/reservoir/reservoir_75.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "reservoir_75@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_75.imageset/reservoir_75@2x.png b/Loop/Assets.xcassets/reservoir/reservoir_75.imageset/reservoir_75@2x.png
deleted file mode 100644
index 296b4b5927..0000000000
Binary files a/Loop/Assets.xcassets/reservoir/reservoir_75.imageset/reservoir_75@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_mask.imageset/Contents.json b/Loop/Assets.xcassets/reservoir/reservoir_mask.imageset/Contents.json
new file mode 100644
index 0000000000..98eba73895
--- /dev/null
+++ b/Loop/Assets.xcassets/reservoir/reservoir_mask.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "reservoir_mask.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_mask.imageset/reservoir_mask.pdf b/Loop/Assets.xcassets/reservoir/reservoir_mask.imageset/reservoir_mask.pdf
new file mode 100644
index 0000000000..3d53399171
Binary files /dev/null and b/Loop/Assets.xcassets/reservoir/reservoir_mask.imageset/reservoir_mask.pdf differ
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_unknown.imageset/Contents.json b/Loop/Assets.xcassets/reservoir/reservoir_unknown.imageset/Contents.json
deleted file mode 100644
index 3d3207a1e5..0000000000
--- a/Loop/Assets.xcassets/reservoir/reservoir_unknown.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "reservoir_unknown@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/reservoir/reservoir_unknown.imageset/reservoir_unknown@2x.png b/Loop/Assets.xcassets/reservoir/reservoir_unknown.imageset/reservoir_unknown@2x.png
deleted file mode 100644
index a8bbec8e99..0000000000
Binary files a/Loop/Assets.xcassets/reservoir/reservoir_unknown.imageset/reservoir_unknown@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/workout-selected.imageset/workout-selected.pdf b/Loop/Assets.xcassets/workout-selected.imageset/workout-selected.pdf
index 72e1512efe..a340acb01d 100644
Binary files a/Loop/Assets.xcassets/workout-selected.imageset/workout-selected.pdf and b/Loop/Assets.xcassets/workout-selected.imageset/workout-selected.pdf differ
diff --git a/Loop/Assets.xcassets/workout.imageset/workout.pdf b/Loop/Assets.xcassets/workout.imageset/workout.pdf
index e38a00b918..251c7b3b3c 100644
Binary files a/Loop/Assets.xcassets/workout.imageset/workout.pdf and b/Loop/Assets.xcassets/workout.imageset/workout.pdf differ
diff --git a/Loop/Base.lproj/Main.storyboard b/Loop/Base.lproj/Main.storyboard
index d05544f82c..d7f3763134 100644
--- a/Loop/Base.lproj/Main.storyboard
+++ b/Loop/Base.lproj/Main.storyboard
@@ -1,5 +1,5 @@
-
+
@@ -113,7 +113,137 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -123,7 +253,7 @@
-
+
@@ -133,7 +263,7 @@
-
+
@@ -155,7 +285,7 @@
@@ -196,23 +326,33 @@
+
@@ -220,18 +360,21 @@
-
+
+
-
+
+
-
+
+
@@ -241,14 +384,14 @@
@@ -267,8 +410,8 @@
-
-
+
+
@@ -284,26 +427,44 @@
-
@@ -311,15 +472,18 @@
+
-
-
+
+
+
-
+
+
@@ -327,22 +491,33 @@
-
+
-
+
-
-
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
@@ -351,16 +526,16 @@
-
+
-
+
-
+
@@ -448,6 +623,7 @@
+
@@ -594,7 +770,7 @@
-
+
@@ -627,7 +803,7 @@
-
+
@@ -645,7 +821,7 @@
-
+
@@ -663,7 +839,7 @@
-
+
@@ -681,14 +857,16 @@
-
+
-
-
+
+
+
-
+
+
diff --git a/Loop/Extensions/NSNumberFormatter.swift b/Loop/Extensions/NSNumberFormatter.swift
new file mode 100644
index 0000000000..99c9d3a9ec
--- /dev/null
+++ b/Loop/Extensions/NSNumberFormatter.swift
@@ -0,0 +1,23 @@
+//
+// NSNumberFormatter.swift
+// Loop
+//
+// Created by Nate Racklyeft on 9/5/16.
+// Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+extension NSNumberFormatter {
+ static func glucoseFormatter(for unit: HKUnit) -> NSNumberFormatter {
+ let numberFormatter = NSNumberFormatter()
+ numberFormatter.numberStyle = .DecimalStyle
+ numberFormatter.minimumFractionDigits = unit.preferredMinimumFractionDigits
+ numberFormatter.maximumSignificantDigits = 3
+ numberFormatter.usesSignificantDigits = true
+
+ return numberFormatter
+ }
+}
diff --git a/Loop/Extensions/NSUserDefaults.swift b/Loop/Extensions/NSUserDefaults.swift
index d33f361e0b..94e89451f7 100644
--- a/Loop/Extensions/NSUserDefaults.swift
+++ b/Loop/Extensions/NSUserDefaults.swift
@@ -28,6 +28,7 @@ extension NSUserDefaults {
case PumpID = "com.loudnate.Naterade.PumpID"
case PumpModelNumber = "com.loudnate.Naterade.PumpModelNumber"
case PumpTimeZone = "com.loudnate.Naterade.PumpTimeZone"
+ case RetrospectiveCorrectionEnabled = "com.loudnate.Loop.RetrospectiveCorrectionEnabled"
}
var basalRateSchedule: BasalRateSchedule? {
@@ -201,6 +202,15 @@ extension NSUserDefaults {
}
}
+ var retrospectiveCorrectionEnabled: Bool {
+ get {
+ return boolForKey(Key.RetrospectiveCorrectionEnabled.rawValue)
+ }
+ set {
+ setBool(newValue, forKey: Key.RetrospectiveCorrectionEnabled.rawValue)
+ }
+ }
+
var transmitterID: String? {
get {
return stringForKey(Key.G5TransmitterID.rawValue)
diff --git a/Loop/Extensions/UIColor.swift b/Loop/Extensions/UIColor.swift
index 2171d09450..37fdce010a 100644
--- a/Loop/Extensions/UIColor.swift
+++ b/Loop/Extensions/UIColor.swift
@@ -16,13 +16,13 @@ extension UIColor {
@nonobjc static let gridColor = UIColor(white: 193 / 255, alpha: 1)
- @nonobjc static let glucoseTintColor = UIColor.HIGTealBlueColor()
+ @nonobjc static let glucoseTintColor = UIColor(red: 0 / 255, green: 176 / 255, blue: 255 / 255, alpha: 1)
@nonobjc static let IOBTintColor = UIColor.HIGOrangeColor()
- @nonobjc static let COBTintColor = UIColor.HIGYellowColor()
+ @nonobjc static let COBTintColor = UIColor(red: 99 / 255, green: 218 / 255, blue: 56 / 255, alpha: 1)
- @nonobjc static let doseTintColor = UIColor.HIGGreenColor()
+ @nonobjc static let doseTintColor = UIColor.HIGOrangeColor()
@nonobjc static let freshColor = UIColor.HIGGreenColor()
@@ -42,7 +42,7 @@ extension UIColor {
}
private static func HIGYellowColor() -> UIColor {
- return UIColor(red: 1, green: 204 / 255, blue: 0 / 255, alpha: 1)
+ return UIColor(red: 1, green: 204 / 255, blue: 0, alpha: 1)
}
private static func HIGOrangeColor() -> UIColor {
diff --git a/Loop/Info.plist b/Loop/Info.plist
index c557d943b3..1588c34047 100644
--- a/Loop/Info.plist
+++ b/Loop/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.11.2
+ 0.12.0
CFBundleSignature
????
CFBundleVersion
diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift
index 5a66f03506..089138f43f 100644
--- a/Loop/Managers/DeviceDataManager.swift
+++ b/Loop/Managers/DeviceDataManager.swift
@@ -62,7 +62,7 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter
}
var sensorInfo: SensorDisplayable? {
- return latestGlucoseG5 ?? latestGlucoseG4 ?? latestPumpStatusFromMySentry
+ return latestGlucoseG5 ?? latestGlucoseG4 ?? latestGlucoseFromShare ?? latestPumpStatusFromMySentry
}
// MARK: - RileyLink
@@ -126,7 +126,7 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter
}
}
- func enableRileyLinkHeartbeatIfNeeded() {
+ private func enableRileyLinkHeartbeatIfNeeded() {
if transmitter != nil {
rileyLinkManager.timerTickEnabled = false
} else if receiverEnabled {
@@ -492,6 +492,8 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter
private var latestGlucoseG5: xDripG5.Glucose?
+ private var latestGlucoseFromShare: ShareGlucose?
+
/**
Attempts to backfill glucose data from the share servers if a G5 connection hasn't been established.
@@ -519,6 +521,8 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter
return
}
+ self.latestGlucoseFromShare = glucose.first
+
// Ignore glucose values that are up to a minute newer than our previous value, to account for possible time shifting in Share data
let newGlucose = glucose.filterDateRange(glucoseStore.latestGlucose?.startDate.dateByAddingTimeInterval(NSTimeInterval(minutes: 1)), nil).map {
return (quantity: $0.quantity, date: $0.startDate, isDisplayOnly: false)
@@ -560,7 +564,7 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter
let includeAfter = glucoseStore.latestGlucose?.startDate.dateByAddingTimeInterval(NSTimeInterval(minutes: 1))
let validGlucose = glucoseHistory.flatMap({
- $0.isValid ? $0 : nil
+ $0.isStateValid ? $0 : nil
}).filterDateRange(includeAfter, nil).map({
(quantity: $0.quantity, date: $0.startDate, isDisplayOnly: $0.isDisplayOnly)
})
diff --git a/Loop/Managers/DiagnosticLogger+LoopKit.swift b/Loop/Managers/DiagnosticLogger+LoopKit.swift
index e0604930b1..07db9eedc9 100644
--- a/Loop/Managers/DiagnosticLogger+LoopKit.swift
+++ b/Loop/Managers/DiagnosticLogger+LoopKit.swift
@@ -26,7 +26,7 @@ extension DiagnosticLogger {
addError(String(message), fromSource: source)
}
- func addLoopStatus(startDate startDate: NSDate, endDate: NSDate, glucose: GlucoseValue, effects: [String: [GlucoseEffect]], error: ErrorType?, prediction: [GlucoseValue], recommendedTempBasal: LoopDataManager.TempBasalRecommendation?) {
+ func addLoopStatus(startDate startDate: NSDate, endDate: NSDate, glucose: GlucoseValue, effects: [String: [GlucoseEffect]], error: ErrorType?, prediction: [GlucoseValue], predictionWithRetrospectiveEffect: Double, recommendedTempBasal: LoopDataManager.TempBasalRecommendation?) {
let dateFormatter = NSDateFormatter.ISO8601StrictDateFormatter()
let unit = HKUnit.milligramsPerDeciliterUnit()
@@ -50,13 +50,14 @@ extension DiagnosticLogger {
}
return input
}),
- "prediction": prediction.map({ (value) -> [String: AnyObject] in
+ "prediction": prediction.map { (value) -> [String: AnyObject] in
[
"startDate": dateFormatter.stringFromDate(value.startDate),
"value": value.quantity.doubleValueForUnit(unit),
"unit": unit.unitString
]
- })
+ },
+ "prediction_retrospect_delta": predictionWithRetrospectiveEffect
]
if let error = error {
diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift
index d63a9fe311..43911c07ec 100644
--- a/Loop/Managers/LoopDataManager.swift
+++ b/Loop/Managers/LoopDataManager.swift
@@ -8,6 +8,7 @@
import Foundation
import CarbKit
+import HealthKit
import InsulinKit
import LoopKit
import MinimedKit
@@ -31,6 +32,8 @@ final class LoopDataManager {
typealias TempBasalRecommendation = (recommendedDate: NSDate, rate: Double, duration: NSTimeInterval)
+ private typealias GlucoseChange = (GlucoseValue, GlucoseValue)
+
unowned let deviceDataManager: DeviceDataManager
var dosingEnabled: Bool {
@@ -41,43 +44,50 @@ final class LoopDataManager {
}
}
+ var retrospectiveCorrectionEnabled: Bool {
+ didSet {
+ NSUserDefaults.standardUserDefaults().retrospectiveCorrectionEnabled = retrospectiveCorrectionEnabled
+
+ notify(forChange: .Preferences)
+ }
+ }
+
init(deviceDataManager: DeviceDataManager) {
self.deviceDataManager = deviceDataManager
dosingEnabled = NSUserDefaults.standardUserDefaults().dosingEnabled
+ retrospectiveCorrectionEnabled = NSUserDefaults.standardUserDefaults().retrospectiveCorrectionEnabled
- observe()
- }
-
- // Actions
-
- private func observe() {
+ // Observe changes
let center = NSNotificationCenter.defaultCenter()
notificationObservers = [
center.addObserverForName(DeviceDataManager.GlucoseUpdatedNotification, object: deviceDataManager, queue: nil) { (note) -> Void in
dispatch_async(self.dataAccessQueue) {
self.glucoseMomentumEffect = nil
+ self.glucoseChange = nil
self.notify(forChange: .Glucose)
}
},
center.addObserverForName(DeviceDataManager.PumpStatusUpdatedNotification, object: deviceDataManager, queue: nil) { (note) -> Void in
dispatch_async(self.dataAccessQueue) {
+ // Assuming insulin data is never back-dated, we don't need to remove the retrospective glucose effects
self.insulinEffect = nil
self.insulinOnBoard = nil
self.loop()
}
+ },
+ center.addObserverForName(CarbStore.CarbEntriesDidUpdateNotification, object: nil, queue: nil) { (note) -> Void in
+ dispatch_async(self.dataAccessQueue) {
+ self.carbEffect = nil
+ self.notify(forChange: .Carbs)
+ }
}
]
-
- notificationObservers.append(center.addObserverForName(CarbStore.CarbEntriesDidUpdateNotification, object: nil, queue: nil) { (note) -> Void in
- dispatch_async(self.dataAccessQueue) {
- self.carbEffect = nil
- self.notify(forChange: .Carbs)
- }
- })
}
+ // Actions
+
private func loop() {
NSNotificationCenter.defaultCenter().postNotificationName(self.dynamicType.LoopRunningNotification, object: self)
@@ -123,9 +133,21 @@ final class LoopDataManager {
private func update() throws {
let updateGroup = dispatch_group_create()
+ if glucoseChange == nil, let glucoseStore = deviceDataManager.glucoseStore {
+ dispatch_group_enter(updateGroup)
+ glucoseStore.getRecentGlucoseChange { (values, error) in
+ if let error = error {
+ self.deviceDataManager.logger.addError(error, fromSource: "GlucoseStore")
+ }
+
+ self.glucoseChange = values
+ dispatch_group_leave(updateGroup)
+ }
+ }
+
if glucoseMomentumEffect == nil {
dispatch_group_enter(updateGroup)
- updateGlucoseMomentumEffect { (effects, error) -> Void in
+ updateGlucoseMomentumEffect { (effects, error) in
if error == nil {
self.glucoseMomentumEffect = effects
} else {
@@ -137,7 +159,7 @@ final class LoopDataManager {
if carbEffect == nil {
dispatch_group_enter(updateGroup)
- updateCarbEffect { (effects, error) -> Void in
+ updateCarbEffect { (effects, error) in
if error == nil {
self.carbEffect = effects
} else {
@@ -149,7 +171,7 @@ final class LoopDataManager {
if insulinEffect == nil {
dispatch_group_enter(updateGroup)
- updateInsulinEffect { (effects, error) -> Void in
+ updateInsulinEffect { (effects, error) in
if error == nil {
self.insulinEffect = effects
} else {
@@ -173,6 +195,14 @@ final class LoopDataManager {
dispatch_group_wait(updateGroup, DISPATCH_TIME_FOREVER)
+ if self.retrospectivePredictedGlucose == nil {
+ do {
+ try self.updateRetrospectiveGlucoseEffect()
+ } catch let error {
+ self.deviceDataManager.logger.addError(error, fromSource: "RetrospectiveGlucose")
+ }
+ }
+
if self.predictedGlucose == nil {
do {
try self.updatePredictedGlucoseAndRecommendedBasal()
@@ -198,13 +228,14 @@ final class LoopDataManager {
- parameter resultsHandler: A closure called once the values have been retrieved. The closure takes the following arguments:
- predictedGlucose: The calculated timeline of predicted glucose values
+ - retrospectivePredictedGlucose: The retrospective prediction over a recent period of glucose samples
- recommendedTempBasal: The recommended temp basal based on predicted glucose
- lastTempBasal: The last set temp basal
- lastLoopCompleted: The last date at which a loop completed, from prediction to dose (if dosing is enabled)
- insulinOnBoard Current insulin on board
- error: An error in the current state of the loop, or one that happened during the last attempt to loop.
*/
- func getLoopStatus(resultsHandler: (predictedGlucose: [GlucoseValue]?, recommendedTempBasal: TempBasalRecommendation?, lastTempBasal: DoseEntry?, lastLoopCompleted: NSDate?, insulinOnBoard: InsulinValue?, error: ErrorType?) -> Void) {
+ func getLoopStatus(resultsHandler: (predictedGlucose: [GlucoseValue]?, retrospectivePredictedGlucose: [GlucoseValue]?, recommendedTempBasal: TempBasalRecommendation?, lastTempBasal: DoseEntry?, lastLoopCompleted: NSDate?, insulinOnBoard: InsulinValue?, error: ErrorType?) -> Void) {
dispatch_async(dataAccessQueue) {
var error: ErrorType?
@@ -214,7 +245,44 @@ final class LoopDataManager {
error = updateError
}
- resultsHandler(predictedGlucose: self.predictedGlucose, recommendedTempBasal: self.recommendedTempBasal, lastTempBasal: self.lastTempBasal, lastLoopCompleted: self.lastLoopCompleted, insulinOnBoard: self.insulinOnBoard, error: error ?? self.lastLoopError)
+ resultsHandler(predictedGlucose: self.predictedGlucose, retrospectivePredictedGlucose: self.retrospectivePredictedGlucose, recommendedTempBasal: self.recommendedTempBasal, lastTempBasal: self.lastTempBasal, lastLoopCompleted: self.lastLoopCompleted, insulinOnBoard: self.insulinOnBoard, error: error ?? self.lastLoopError)
+ }
+ }
+
+ func modelPredictedGlucose(using inputs: [PredictionInputEffect], resultsHandler: (predictedGlucose: [GlucoseValue]?, error: ErrorType?) -> Void) {
+ dispatch_async(dataAccessQueue) {
+ guard let
+ glucose = self.deviceDataManager.glucoseStore?.latestGlucose
+ else {
+ resultsHandler(predictedGlucose: nil, error: LoopError.MissingDataError("Cannot predict glucose due to missing input data"))
+ return
+ }
+
+ var momentum: [GlucoseEffect] = []
+ var effects: [[GlucoseEffect]] = []
+
+ for input in inputs {
+ switch input {
+ case .carbs:
+ if let carbEffect = self.carbEffect {
+ effects.append(carbEffect)
+ }
+ case .insulin:
+ if let insulinEffect = self.insulinEffect {
+ effects.append(insulinEffect)
+ }
+ case .momentum:
+ if let momentumEffect = self.glucoseMomentumEffect {
+ momentum = momentumEffect
+ }
+ case .retrospection:
+ effects.append(self.retrospectiveGlucoseEffect)
+ }
+ }
+
+ let prediction = LoopMath.predictGlucose(glucose, momentum: momentum, effects: effects)
+
+ resultsHandler(predictedGlucose: prediction, error: nil)
}
}
@@ -225,6 +293,9 @@ final class LoopDataManager {
private var carbEffect: [GlucoseEffect]? {
didSet {
predictedGlucose = nil
+
+ // Carb data may be back-dated, so re-calculate the retrospective glucose.
+ retrospectivePredictedGlucose = nil
}
}
private var insulinEffect: [GlucoseEffect]? {
@@ -242,13 +313,28 @@ final class LoopDataManager {
predictedGlucose = nil
}
}
+ private var glucoseChange: GlucoseChange? {
+ didSet {
+ retrospectivePredictedGlucose = nil
+ }
+ }
private var predictedGlucose: [GlucoseValue]? {
didSet {
recommendedTempBasal = nil
}
}
-
+ private var retrospectivePredictedGlucose: [GlucoseValue]? {
+ didSet {
+ retrospectiveGlucoseEffect = []
+ }
+ }
+ private var retrospectiveGlucoseEffect: [GlucoseEffect] = [] {
+ didSet {
+ predictedGlucose = nil
+ }
+ }
private var recommendedTempBasal: TempBasalRecommendation?
+
private var lastTempBasal: DoseEntry?
private var lastBolus: (units: Double, date: NSDate)?
private var lastLoopError: ErrorType? {
@@ -266,11 +352,23 @@ final class LoopDataManager {
}
}
- private func updateCarbEffect(completionHandler: (effects: [GlucoseEffect]?, error: ErrorType?) -> Void) {
- let glucose = deviceDataManager.glucoseStore?.latestGlucose
+ /// The oldest date that should be used for effect calculation
+ private var effectStartDate: NSDate? {
+ let startDate: NSDate?
+ if let glucoseStore = deviceDataManager.glucoseStore {
+ // Fetch glucose effects as far back as we want to make retroactive analysis
+ startDate = glucoseStore.latestGlucose?.startDate.dateByAddingTimeInterval(-glucoseStore.reflectionDataInterval)
+ } else {
+ startDate = nil
+ }
+
+ return startDate
+ }
+
+ private func updateCarbEffect(completionHandler: (effects: [GlucoseEffect]?, error: ErrorType?) -> Void) {
if let carbStore = deviceDataManager.carbStore {
- carbStore.getGlucoseEffects(startDate: glucose?.startDate) { (effects, error) -> Void in
+ carbStore.getGlucoseEffects(startDate: effectStartDate) { (effects, error) -> Void in
if let error = error {
self.deviceDataManager.logger.addError(error, fromSource: "CarbStore")
}
@@ -283,9 +381,7 @@ final class LoopDataManager {
}
private func updateInsulinEffect(completionHandler: (effects: [GlucoseEffect]?, error: ErrorType?) -> Void) {
- let glucose = deviceDataManager.glucoseStore?.latestGlucose
-
- deviceDataManager.doseStore.getGlucoseEffects(startDate: glucose?.startDate) { (effects, error) -> Void in
+ deviceDataManager.doseStore.getGlucoseEffects(startDate: effectStartDate) { (effects, error) -> Void in
if let error = error {
self.deviceDataManager.logger.addError(error, fromSource: "DoseStore")
}
@@ -295,19 +391,60 @@ final class LoopDataManager {
}
private func updateGlucoseMomentumEffect(completionHandler: (effects: [GlucoseEffect]?, error: ErrorType?) -> Void) {
- if let glucoseStore = deviceDataManager.glucoseStore {
- glucoseStore.getRecentMomentumEffect { (effects, error) -> Void in
- if let error = error {
- self.deviceDataManager.logger.addError(error, fromSource: "GlucoseStore")
- }
-
- completionHandler(effects: effects, error: error)
- }
- } else {
+ guard let glucoseStore = deviceDataManager.glucoseStore else {
completionHandler(effects: nil, error: LoopError.MissingDataError("GlucoseStore not available"))
+ return
+ }
+ glucoseStore.getRecentMomentumEffect { (effects, error) -> Void in
+ if let error = error {
+ self.deviceDataManager.logger.addError(error, fromSource: "GlucoseStore")
+ }
+
+ completionHandler(effects: effects, error: error)
}
}
+ /**
+ Runs the glucose retrospective analysis using the latest effect data.
+
+ *This method should only be called from the `dataAccessQueue`*
+ */
+ private func updateRetrospectiveGlucoseEffect() throws {
+ guard
+ let carbEffect = self.carbEffect,
+ let insulinEffect = self.insulinEffect
+ else {
+ self.retrospectivePredictedGlucose = nil
+ throw LoopError.MissingDataError("Cannot retrospect glucose due to missing input data")
+ }
+
+ guard let change = glucoseChange else {
+ self.retrospectivePredictedGlucose = nil
+ return // Expected case for calibrations
+ }
+
+ // Run a retrospective prediction over the duration of the recorded glucose change, using the current carb and insulin effects
+ let startDate = change.0.startDate
+ let endDate = change.1.endDate.dateByAddingTimeInterval(NSTimeInterval(minutes: 5))
+ let retrospectivePrediction = LoopMath.predictGlucose(change.0, effects:
+ carbEffect.filterDateRange(startDate, endDate),
+ insulinEffect.filterDateRange(startDate, endDate)
+ )
+
+ self.retrospectivePredictedGlucose = retrospectivePrediction
+
+ guard let lastGlucose = retrospectivePrediction.last else { return }
+ let glucoseUnit = HKUnit.milligramsPerDeciliterUnit()
+ let velocityUnit = glucoseUnit.unitDividedByUnit(HKUnit.secondUnit())
+
+ let discrepancy = change.1.quantity.doubleValueForUnit(glucoseUnit) - lastGlucose.quantity.doubleValueForUnit(glucoseUnit) // mg/dL
+ let velocity = HKQuantity(unit: velocityUnit, doubleValue: discrepancy / change.1.endDate.timeIntervalSinceDate(change.0.endDate))
+ let type = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)!
+ let glucose = HKQuantitySample(type: type, quantity: change.1.quantity, startDate: change.1.startDate, endDate: change.1.endDate)
+
+ self.retrospectiveGlucoseEffect = LoopMath.decayEffect(from: glucose, atRate: velocity, for: NSTimeInterval(minutes: 60))
+ }
+
/**
Runs the glucose prediction on the latest effect data.
@@ -325,10 +462,9 @@ final class LoopDataManager {
let startDate = NSDate()
let recencyInterval = NSTimeInterval(minutes: 15)
- guard startDate.timeIntervalSinceDate(glucose.startDate) <= recencyInterval &&
- startDate.timeIntervalSinceDate(pumpStatusDate) <= recencyInterval
- else
- {
+ guard startDate.timeIntervalSinceDate(glucose.startDate) <= recencyInterval &&
+ startDate.timeIntervalSinceDate(pumpStatusDate) <= recencyInterval
+ else {
self.predictedGlucose = nil
throw LoopError.StaleDataError("Glucose Date: \(glucose.startDate) or Pump status date: \(pumpStatusDate) older than \(recencyInterval.minutes) min")
}
@@ -344,25 +480,39 @@ final class LoopDataManager {
var error: ErrorType?
+ let prediction = LoopMath.predictGlucose(glucose, momentum: momentum, effects: carbEffect, insulinEffect)
+ let predictionWithRetrospectiveEffect = LoopMath.predictGlucose(glucose, momentum: momentum, effects: carbEffect, insulinEffect, retrospectiveGlucoseEffect)
+
+ let predictDiff: Double
+
+ let unit = HKUnit.milligramsPerDeciliterUnit()
+ if let lastA = prediction.last?.quantity.doubleValueForUnit(unit),
+ let lastB = predictionWithRetrospectiveEffect.last?.quantity.doubleValueForUnit(unit)
+ {
+ predictDiff = lastB - lastA
+ } else {
+ predictDiff = 0
+ }
+
defer {
- self.deviceDataManager.logger.addLoopStatus(
+ deviceDataManager.logger.addLoopStatus(
startDate: startDate,
endDate: NSDate(),
glucose: glucose,
effects: [
"momentum": momentum,
"carbs": carbEffect,
- "insulin": insulinEffect
+ "insulin": insulinEffect,
+ "retrospective_glucose": retrospectiveGlucoseEffect
],
error: error,
prediction: prediction,
+ predictionWithRetrospectiveEffect: predictDiff,
recommendedTempBasal: recommendedTempBasal
)
}
- let prediction = LoopMath.predictGlucose(glucose, momentum: momentum, effects: carbEffect, insulinEffect)
-
- self.predictedGlucose = prediction
+ self.predictedGlucose = retrospectiveCorrectionEnabled ? predictionWithRetrospectiveEffect : prediction
guard let
maxBasal = deviceDataManager.maximumBasalRatePerHour,
@@ -374,7 +524,7 @@ final class LoopDataManager {
throw error!
}
- if let tempBasal = DoseMath.recommendTempBasalFromPredictedGlucose(prediction,
+ if let tempBasal = DoseMath.recommendTempBasalFromPredictedGlucose(predictionWithRetrospectiveEffect,
lastTempBasal: lastTempBasal,
maxBasalRate: maxBasal,
glucoseTargetRange: glucoseTargetRange,
diff --git a/Loop/Managers/NightscoutDataManager.swift b/Loop/Managers/NightscoutDataManager.swift
index cbfc2840f3..69258a8e90 100644
--- a/Loop/Managers/NightscoutDataManager.swift
+++ b/Loop/Managers/NightscoutDataManager.swift
@@ -34,17 +34,15 @@ class NightscoutDataManager {
return
}
- deviceDataManager.loopManager.getLoopStatus { (predictedGlucose, recommendedTempBasal, lastTempBasal, lastLoopCompleted, insulinOnBoard, loopError) in
+ deviceDataManager.loopManager.getLoopStatus { (predictedGlucose, _, recommendedTempBasal, lastTempBasal, _, insulinOnBoard, loopError) in
self.deviceDataManager.loopManager.getRecommendedBolus { (bolusUnits, getBolusError) in
- if getBolusError != nil {
- self.deviceDataManager.logger.addError(getBolusError!, fromSource: "NightscoutDataManager")
+ if let getBolusError = getBolusError {
+ self.deviceDataManager.logger.addError(getBolusError, fromSource: "NightscoutDataManager")
}
self.uploadLoopStatus(insulinOnBoard, predictedGlucose: predictedGlucose, recommendedTempBasal: recommendedTempBasal, recommendedBolus: bolusUnits, lastTempBasal: lastTempBasal, loopError: loopError ?? getBolusError)
}
}
-
-
}
private var lastTempBasalUploaded: DoseEntry?
diff --git a/Loop/Managers/StatusChartManager.swift b/Loop/Managers/StatusChartManager.swift
index aa886a3227..82ae6942c9 100644
--- a/Loop/Managers/StatusChartManager.swift
+++ b/Loop/Managers/StatusChartManager.swift
@@ -86,6 +86,19 @@ final class StatusChartsManager {
}
}
+ var glucoseDisplayRange: (min: HKQuantity, max: HKQuantity)? {
+ didSet {
+ if let range = glucoseDisplayRange {
+ glucoseDisplayRangePoints = [
+ ChartPoint(x: ChartAxisValue(scalar: 0), y: ChartAxisValueDouble(range.min.doubleValueForUnit(glucoseUnit))),
+ ChartPoint(x: ChartAxisValue(scalar: 0), y: ChartAxisValueDouble(range.max.doubleValueForUnit(glucoseUnit)))
+ ]
+ } else {
+ glucoseDisplayRangePoints = []
+ }
+ }
+ }
+
var predictedGlucoseValues: [GlucoseValue] = [] {
didSet {
let unitString = glucoseUnit.glucoseUnitDisplayString
@@ -99,6 +112,19 @@ final class StatusChartsManager {
}
}
+ var alternatePredictedGlucoseValues: [GlucoseValue] = [] {
+ didSet {
+ let unitString = glucoseUnit.glucoseUnitDisplayString
+
+ alternatePredictedGlucosePoints = alternatePredictedGlucoseValues.map {
+ return ChartPoint(
+ x: ChartAxisValueDate(date: $0.startDate, formatter: dateFormatter),
+ y: ChartAxisValueDoubleUnit($0.quantity.doubleValueForUnit(glucoseUnit), unitString: unitString, formatter: integerFormatter)
+ )
+ }
+ }
+ }
+
var IOBValues: [InsulinValue] = [] {
didSet {
IOBPoints = IOBValues.map {
@@ -157,6 +183,12 @@ final class StatusChartsManager {
}
}
+ private var glucoseDisplayRangePoints: [ChartPoint] = [] {
+ didSet {
+ glucoseChart = nil
+ }
+ }
+
private var predictedGlucosePoints: [ChartPoint] = [] {
didSet {
glucoseChart = nil
@@ -164,6 +196,8 @@ final class StatusChartsManager {
}
}
+ private var alternatePredictedGlucosePoints: [ChartPoint]?
+
private var targetGlucosePoints: [ChartPoint] = [] {
didSet {
glucoseChart = nil
@@ -250,9 +284,9 @@ final class StatusChartsManager {
return nil
}
- let allPoints = glucosePoints + predictedGlucosePoints
+ let points = glucosePoints + predictedGlucosePoints + targetGlucosePoints + targetOverridePoints + glucoseDisplayRangePoints
- let yAxisValues = ChartAxisValuesGenerator.generateYAxisValuesWithChartPoints(allPoints + targetGlucosePoints + targetOverridePoints,
+ let yAxisValues = ChartAxisValuesGenerator.generateYAxisValuesWithChartPoints(points,
minSegmentCount: 2,
maxSegmentCount: 4,
multiple: glucoseUnit.glucoseUnitYAxisSegmentSize,
@@ -293,17 +327,38 @@ final class StatusChartsManager {
let circles = ChartPointsScatterCirclesLayer(xAxis: xAxis, yAxis: yAxis, innerFrame: innerFrame, chartPoints: glucosePoints, displayDelay: 0, itemSize: CGSize(width: 4, height: 4), itemFillColor: UIColor.glucoseTintColor)
+ var alternatePrediction: ChartLayer?
+
+ if let altPoints = alternatePredictedGlucosePoints where altPoints.count > 1 {
+ // TODO: Bug in ChartPointsLineLayer requires a non-zero animation to draw the dash pattern
+ let lineModel = ChartLineModel(chartPoints: altPoints, lineColor: UIColor.glucoseTintColor, lineWidth: 2, animDuration: 0.0001, animDelay: 0, dashPattern: [6, 5])
+
+ alternatePrediction = ChartPointsLineLayer(xAxis: xAxis, yAxis: yAxis, innerFrame: innerFrame, lineModels: [lineModel])
+ }
+
var prediction: ChartLayer?
if predictedGlucosePoints.count > 1 {
- prediction = ChartPointsScatterCirclesLayer(xAxis: xAxis, yAxis: yAxis, innerFrame: innerFrame, chartPoints: predictedGlucosePoints, displayDelay: 0, itemSize: CGSize(width: 2, height: 2), itemFillColor: UIColor.glucoseTintColor.colorWithAlphaComponent(0.75))
+ let lineColor = (alternatePrediction == nil) ? UIColor.glucoseTintColor : UIColor.secondaryLabelColor
+
+ // TODO: Bug in ChartPointsLineLayer requires a non-zero animation to draw the dash pattern
+ let lineModel = ChartLineModel(
+ chartPoints: predictedGlucosePoints,
+ lineColor: lineColor,
+ lineWidth: 1,
+ animDuration: 0.0001,
+ animDelay: 0,
+ dashPattern: [6, 5]
+ )
+
+ prediction = ChartPointsLineLayer(xAxis: xAxis, yAxis: yAxis, innerFrame: innerFrame, lineModels: [lineModel])
}
glucoseChartCache = ChartPointsTouchHighlightLayerViewCache(
xAxis: xAxis,
yAxis: yAxis,
innerFrame: innerFrame,
- chartPoints: allPoints,
+ chartPoints: glucosePoints + (alternatePredictedGlucosePoints ?? predictedGlucosePoints),
tintColor: UIColor.glucoseTintColor,
labelCenterY: chartSettings.top,
gestureRecognizer: panGestureRecognizer
@@ -318,6 +373,7 @@ final class StatusChartsManager {
yAxis,
glucoseChartCache?.highlightLayer,
prediction,
+ alternatePrediction,
circles
]
diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift
index 157102cd73..2bc90c1e29 100644
--- a/Loop/Managers/WatchDataManager.swift
+++ b/Loop/Managers/WatchDataManager.swift
@@ -105,8 +105,7 @@ final class WatchDataManager: NSObject, WCSessionDelegate {
let reservoir = deviceDataManager.doseStore.lastReservoirValue
let maxBolus = deviceDataManager.maximumBolus
- deviceDataManager.loopManager.getLoopStatus { (predictedGlucose, recommendedTempBasal, lastTempBasal, lastLoopCompleted, insulinOnBoard, error) in
-
+ deviceDataManager.loopManager.getLoopStatus { (predictedGlucose, _, recommendedTempBasal, lastTempBasal, lastLoopCompleted, _, error) in
let eventualGlucose = predictedGlucose?.last
self.deviceDataManager.loopManager.getRecommendedBolus { (units, error) in
diff --git a/Loop/Models/Glucose.swift b/Loop/Models/Glucose.swift
index c0c2db1544..72aec6e4e8 100644
--- a/Loop/Models/Glucose.swift
+++ b/Loop/Models/Glucose.swift
@@ -11,6 +11,10 @@ import xDripG5
extension Glucose: SensorDisplayable {
+ var isStateValid: Bool {
+ return state == .OK && status == .OK
+ }
+
var stateDescription: String {
let status: String
switch self.status {
diff --git a/Loop/Models/GlucoseG4.swift b/Loop/Models/GlucoseG4.swift
index 3510a43eb7..e87623fc51 100644
--- a/Loop/Models/GlucoseG4.swift
+++ b/Loop/Models/GlucoseG4.swift
@@ -12,13 +12,6 @@ import HealthKit
import LoopKit
-extension GlucoseG4 {
- var isValid: Bool {
- return glucose >= 20
- }
-}
-
-
extension GlucoseG4: GlucoseValue {
public var quantity: HKQuantity {
return HKQuantity(unit: HKUnit.milligramsPerDeciliterUnit(), doubleValue: Double(glucose))
@@ -31,11 +24,15 @@ extension GlucoseG4: GlucoseValue {
extension GlucoseG4: SensorDisplayable {
+ var isStateValid: Bool {
+ return glucose >= 20
+ }
+
var stateDescription: String {
- if isValid {
- return "✓"
+ if isStateValid {
+ return NSLocalizedString("OK", comment: "Sensor state description for the valid state")
} else {
- return String(format: "%02x", glucose)
+ return NSLocalizedString("Needs Attention", comment: "Sensor state description for the non-valid state")
}
}
diff --git a/Loop/Models/MySentryPumpStatusMessageBody.swift b/Loop/Models/MySentryPumpStatusMessageBody.swift
index 631995cfba..f1535c6816 100644
--- a/Loop/Models/MySentryPumpStatusMessageBody.swift
+++ b/Loop/Models/MySentryPumpStatusMessageBody.swift
@@ -11,14 +11,12 @@ import MinimedKit
extension MySentryPumpStatusMessageBody: SensorDisplayable {
- var stateDescription: String {
+ var isStateValid: Bool {
switch glucose {
- case .Active:
- return "✓"
- case .Off:
- return ""
+ case .Active, .Off:
+ return true
default:
- return String(glucose)
+ return false
}
}
diff --git a/Loop/Models/PredictionInputEffect.swift b/Loop/Models/PredictionInputEffect.swift
new file mode 100644
index 0000000000..779f63b645
--- /dev/null
+++ b/Loop/Models/PredictionInputEffect.swift
@@ -0,0 +1,44 @@
+//
+// PredictionInputEffect.swift
+// Loop
+//
+// Created by Nate Racklyeft on 9/4/16.
+// Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+enum PredictionInputEffect {
+ case carbs
+ case insulin
+ case momentum
+ case retrospection
+
+ var localizedTitle: String {
+ switch self {
+ case .carbs:
+ return NSLocalizedString("Carbohydrates", comment: "Title of the prediction input effect for carbohydrates")
+ case .insulin:
+ return NSLocalizedString("Insulin", comment: "Title of the prediction input effect for insulin")
+ case .momentum:
+ return NSLocalizedString("Glucose Momentum", comment: "Title of the prediction input effect for glucose momentum")
+ case .retrospection:
+ return NSLocalizedString("Retrospective Correction", comment: "Title of the prediction input effect for retrospective correction")
+ }
+ }
+
+ func localizedDescription(forGlucoseUnit unit: HKUnit) -> String {
+ switch self {
+ case .carbs:
+ return String(format: NSLocalizedString("Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)", comment: "Description of the prediction input effect for carbohydrates. (1: The glucose unit string)"), unit.glucoseUnitDisplayString)
+ case .insulin:
+ return String(format: NSLocalizedString("Insulin Absorbed (U) × Insulin Sensitivity (%1$@/U)", comment: "Description of the prediction input effect for insulin"), unit.glucoseUnitDisplayString)
+ case .momentum:
+ return NSLocalizedString("15 min glucose regression coefficient (b₁), continued with decay over 30 min", comment: "Description of the prediction input effect for glucose momentum")
+ case .retrospection:
+ return NSLocalizedString("30 mim comparison of glucose prediction vs actual, continued with decay over 60 min", comment: "Description of the prediction input effect for retrospective correction")
+ }
+ }
+}
diff --git a/Loop/Models/SensorDisplayable.swift b/Loop/Models/SensorDisplayable.swift
index bdcb516a51..7cf7723110 100644
--- a/Loop/Models/SensorDisplayable.swift
+++ b/Loop/Models/SensorDisplayable.swift
@@ -6,11 +6,27 @@
// Copyright © 2016 Nathan Racklyeft. All rights reserved.
//
+import Foundation
+
protocol SensorDisplayable {
- // Describes the state of the sensor in the current localization
+ /// Returns whether the current state is valid
+ var isStateValid: Bool { get }
+
+ /// Describes the state of the sensor in the current localization
var stateDescription: String { get }
/// Enumerates the trend of the sensor values
var trendType: GlucoseTrend? { get }
}
+
+
+extension SensorDisplayable {
+ var stateDescription: String {
+ if isStateValid {
+ return NSLocalizedString("OK", comment: "Sensor state description for the valid state")
+ } else {
+ return NSLocalizedString("Needs Attention", comment: "Sensor state description for the non-valid state")
+ }
+ }
+}
diff --git a/Loop/Models/ShareGlucose+GlucoseKit.swift b/Loop/Models/ShareGlucose+GlucoseKit.swift
index ef93b5694d..8699c1e9a6 100644
--- a/Loop/Models/ShareGlucose+GlucoseKit.swift
+++ b/Loop/Models/ShareGlucose+GlucoseKit.swift
@@ -21,3 +21,14 @@ extension ShareGlucose: GlucoseValue {
return HKQuantity(unit: HKUnit.milligramsPerDeciliterUnit(), doubleValue: Double(glucose))
}
}
+
+
+extension ShareGlucose: SensorDisplayable {
+ var isStateValid: Bool {
+ return glucose >= 20
+ }
+
+ var trendType: GlucoseTrend? {
+ return GlucoseTrend(rawValue: Int(trend))
+ }
+}
diff --git a/Loop/View Controllers/PredictionTableViewController.swift b/Loop/View Controllers/PredictionTableViewController.swift
new file mode 100644
index 0000000000..a6577dfc8d
--- /dev/null
+++ b/Loop/View Controllers/PredictionTableViewController.swift
@@ -0,0 +1,338 @@
+//
+// PredictionTableViewController.swift
+// Loop
+//
+// Created by Nate Racklyeft on 9/3/16.
+// Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import UIKit
+import HealthKit
+import LoopKit
+
+
+class PredictionTableViewController: UITableViewController, IdentifiableClass {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ tableView.cellLayoutMarginsFollowReadableWidth = true
+
+ let notificationCenter = NSNotificationCenter.defaultCenter()
+ let mainQueue = NSOperationQueue.mainQueue()
+ let application = UIApplication.sharedApplication()
+
+ notificationObservers += [
+ notificationCenter.addObserverForName(LoopDataManager.LoopDataUpdatedNotification, object: dataManager.loopManager, queue: nil) { note in
+ guard let rawContext = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as? Int where LoopDataManager.LoopUpdateContext(rawValue: rawContext) != .Preferences else {
+ return
+ }
+
+ dispatch_async(dispatch_get_main_queue()) {
+ self.needsRefresh = true
+ self.reloadData(animated: true)
+ }
+ },
+ notificationCenter.addObserverForName(UIApplicationWillResignActiveNotification, object: application, queue: mainQueue) { _ in
+ self.active = false
+ },
+ notificationCenter.addObserverForName(UIApplicationDidBecomeActiveNotification, object: application, queue: mainQueue) { _ in
+ self.active = true
+ }
+ ]
+ }
+
+ deinit {
+ for observer in notificationObservers {
+ NSNotificationCenter.defaultCenter().removeObserver(observer)
+ }
+ }
+
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+
+ visible = true
+ }
+
+ override func viewDidAppear(animated: Bool) {
+ super.viewDidAppear(animated)
+
+ AnalyticsManager.sharedManager.didDisplayStatusScreen()
+ }
+
+ override func viewWillDisappear(animated: Bool) {
+ super.viewWillDisappear(animated)
+
+ visible = false
+ }
+
+ override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
+ super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
+
+ if visible {
+ coordinator.animateAlongsideTransition({ (_) -> Void in
+ self.tableView.beginUpdates()
+ self.tableView.reloadSections(NSIndexSet(index: Section.charts.rawValue), withRowAnimation: .Fade)
+ self.tableView.endUpdates()
+ }, completion: nil)
+ } else {
+ needsRefresh = true
+ }
+ }
+
+ // MARK: - State
+
+ // References to registered notification center observers
+ private var notificationObservers: [AnyObject] = []
+
+ var dataManager: DeviceDataManager!
+
+ private lazy var charts: StatusChartsManager = {
+ let charts = StatusChartsManager()
+
+ charts.glucoseDisplayRange = (
+ min: HKQuantity(unit: HKUnit.milligramsPerDeciliterUnit(), doubleValue: 60),
+ max: HKQuantity(unit: HKUnit.milligramsPerDeciliterUnit(), doubleValue: 200)
+ )
+
+ return charts
+ }()
+
+ private var retrospectivePredictedGlucose: [GlucoseValue]?
+
+ private var active = true {
+ didSet {
+ reloadData()
+ }
+ }
+
+ private var needsRefresh = true
+
+ private var visible = false {
+ didSet {
+ reloadData()
+ }
+ }
+
+ private var reloading = false
+
+ private func reloadData(animated animated: Bool = false) {
+ if active && visible && needsRefresh {
+ needsRefresh = false
+ reloading = true
+
+ let calendar = NSCalendar.currentCalendar()
+ let components = NSDateComponents()
+ components.minute = 0
+ let date = NSDate(timeIntervalSinceNow: -NSTimeInterval(hours: 1))
+ charts.startDate = calendar.nextDateAfterDate(date, matchingComponents: components, options: [.MatchStrictly, .SearchBackwards]) ?? date
+
+ let reloadGroup = dispatch_group_create()
+ var glucoseUnit: HKUnit?
+
+ if let glucoseStore = dataManager.glucoseStore {
+ dispatch_group_enter(reloadGroup)
+ glucoseStore.getRecentGlucoseValues(startDate: charts.startDate) { (values, error) -> Void in
+ if let error = error {
+ self.dataManager.logger.addError(error, fromSource: "GlucoseStore")
+ self.needsRefresh = true
+ // TODO: Display error in the cell
+ } else {
+ self.charts.glucoseValues = values
+ }
+
+ dispatch_group_leave(reloadGroup)
+ }
+
+ dispatch_group_enter(reloadGroup)
+ glucoseStore.preferredUnit { (unit, error) in
+ glucoseUnit = unit
+
+ dispatch_group_leave(reloadGroup)
+ }
+ }
+
+ dispatch_group_enter(reloadGroup)
+ dataManager.loopManager.getLoopStatus { (predictedGlucose, retrospectivePredictedGlucose, _, _, _, _, error) in
+ if error != nil {
+ self.needsRefresh = true
+ }
+
+ self.retrospectivePredictedGlucose = retrospectivePredictedGlucose
+ self.charts.predictedGlucoseValues = predictedGlucose ?? []
+
+ dispatch_group_leave(reloadGroup)
+ }
+
+ dispatch_group_enter(reloadGroup)
+ dataManager.loopManager.modelPredictedGlucose(using: selectedInputs.flatMap { $0.selected ? $0.input : nil }) { (predictedGlucose, error) in
+ if error != nil {
+ self.needsRefresh = true
+ }
+
+ self.charts.alternatePredictedGlucoseValues = predictedGlucose ?? []
+
+ dispatch_group_leave(reloadGroup)
+ }
+
+ charts.glucoseTargetRangeSchedule = dataManager.glucoseTargetRangeSchedule
+
+ dispatch_group_notify(reloadGroup, dispatch_get_main_queue()) {
+ if let unit = glucoseUnit {
+ self.charts.glucoseUnit = unit
+ }
+
+ self.charts.prerender()
+
+ self.tableView.reloadSections(NSIndexSet(indexesInRange: NSMakeRange(Section.charts.rawValue, 1)),
+ withRowAnimation: .None
+ )
+
+ self.reloading = false
+ }
+ }
+ }
+
+ // MARK: - UITableViewDataSource
+
+ private enum Section: Int {
+ case charts
+ case inputs
+ case settings
+
+ static let count = 3
+ }
+
+ private lazy var selectedInputs: [(input: PredictionInputEffect, selected: Bool)] = [
+ (.carbs, true), (.insulin, true), (.momentum, true), (.retrospection, true)
+ ]
+
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ return Section.count
+ }
+
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch Section(rawValue: section)! {
+ case .charts:
+ return 1
+ case .inputs:
+ return selectedInputs.count
+ case .settings:
+ return 1
+ }
+ }
+
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ switch Section(rawValue: indexPath.section)! {
+ case .charts:
+ let cell = tableView.dequeueReusableCellWithIdentifier(ChartTableViewCell.className, forIndexPath: indexPath) as! ChartTableViewCell
+
+ let frame = CGRect(origin: .zero, size: CGSize(width: tableView.bounds.width, height: cell.placeholderView!.bounds.height))
+
+ cell.contentView.layoutMargins.left = tableView.separatorInset.left
+
+ if let chart = charts.glucoseChartWithFrame(frame) {
+ cell.chartView = chart.view
+ } else {
+ cell.chartView = nil
+ // TODO: Display empty state
+ }
+
+ cell.selectionStyle = .None
+
+ return cell
+ case .inputs:
+ let cell = tableView.dequeueReusableCellWithIdentifier(PredictionInputEffectTableViewCell.className, forIndexPath: indexPath) as! PredictionInputEffectTableViewCell
+
+ let (input, selected) = selectedInputs[indexPath.row]
+
+ cell.titleLabel?.text = input.localizedTitle
+ cell.accessoryType = selected ? .Checkmark : .None
+ cell.enabled = input != .retrospection || dataManager.loopManager.retrospectiveCorrectionEnabled
+
+ var subtitleText = input.localizedDescription(forGlucoseUnit: charts.glucoseUnit)
+
+ if input == .retrospection,
+ let startGlucose = retrospectivePredictedGlucose?.first,
+ let endGlucose = retrospectivePredictedGlucose?.last,
+ let currentGlucose = self.dataManager.glucoseStore?.latestGlucose
+ {
+ let formatter = NSNumberFormatter.glucoseFormatter(for: charts.glucoseUnit)
+ let values = [startGlucose, endGlucose, currentGlucose].map { formatter.stringFromNumber($0.quantity.doubleValueForUnit(charts.glucoseUnit)) ?? "?" }
+
+ let retro = String(
+ format: NSLocalizedString("Last comparison: %1$@ → %2$@ vs %3$@", comment: "Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose)"),
+ values[0], values[1], values[2]
+ )
+
+ subtitleText = String(format: "%@\n%@", subtitleText, retro)
+ }
+
+ cell.subtitleLabel?.text = subtitleText
+
+ cell.contentView.layoutMargins.left = tableView.separatorInset.left
+
+ return cell
+ case .settings:
+ let cell = tableView.dequeueReusableCellWithIdentifier(SwitchTableViewCell.className, forIndexPath: indexPath) as! SwitchTableViewCell
+
+ cell.titleLabel?.text = NSLocalizedString("Enable Retrospective Correction", comment: "Title of the switch which toggles retrospective correction effects")
+ cell.subtitleLabel?.text = NSLocalizedString("This will more aggresively increase or decrease basal delivery when glucose movement doesn't match the carbohydrate and insulin-based model.", comment: "The description of the switch which toggles retrospective correction effects")
+ cell.`switch`?.on = dataManager.loopManager.retrospectiveCorrectionEnabled
+ cell.`switch`?.addTarget(self, action: #selector(retrospectiveCorrectionSwitchChanged(_:)), forControlEvents: .ValueChanged)
+
+ cell.contentView.layoutMargins.left = tableView.separatorInset.left
+
+ return cell
+ }
+ }
+
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch Section(rawValue: section)! {
+ case .settings:
+ return NSLocalizedString("Algorithm Settings", comment: "The title of the section containing algorithm settings")
+ default:
+ return nil
+ }
+ }
+
+ // MARK: - UITableViewDelegate
+
+ override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
+ switch Section(rawValue: indexPath.section)! {
+ case .charts:
+ return 220
+ case .inputs, .settings:
+ return 60
+ }
+ }
+
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ guard Section(rawValue: indexPath.section) == .inputs else { return }
+
+ let (input, selected) = selectedInputs[indexPath.row]
+
+ if let cell = tableView.cellForRowAtIndexPath(indexPath) {
+ cell.accessoryType = !selected ? .Checkmark : .None
+ }
+
+ selectedInputs[indexPath.row] = (input, !selected)
+
+ tableView.deselectRowAtIndexPath(indexPath, animated: true)
+
+ needsRefresh = true
+ reloadData()
+ }
+
+ // MARK: - Actions
+
+ @objc private func retrospectiveCorrectionSwitchChanged(sender: UISwitch) {
+ dataManager.loopManager.retrospectiveCorrectionEnabled = sender.on
+
+ if let row = selectedInputs.indexOf({ $0.input == PredictionInputEffect.retrospection }),
+ let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: Section.inputs.rawValue)) as? PredictionInputEffectTableViewCell
+ {
+ cell.enabled = self.dataManager.loopManager.retrospectiveCorrectionEnabled
+ }
+ }
+}
diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift
index 492b313bc8..d2e526434a 100644
--- a/Loop/View Controllers/StatusTableViewController.swift
+++ b/Loop/View Controllers/StatusTableViewController.swift
@@ -58,8 +58,11 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
// Toolbar
toolbarItems![0].accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button")
+ toolbarItems![0].tintColor = UIColor.COBTintColor
toolbarItems![2].accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button")
+ toolbarItems![2].tintColor = UIColor.doseTintColor
toolbarItems![6].accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button")
+ toolbarItems![6].tintColor = UIColor.secondaryLabelColor
}
deinit {
@@ -133,9 +136,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
needsRefresh = false
reloading = true
- tableView.reloadSections(NSIndexSet(indexesInRange: NSMakeRange(Section.Pump.rawValue, Section.count - Section.Pump.rawValue)
- ), withRowAnimation: visible ? .Automatic : .None)
-
let calendar = NSCalendar.currentCalendar()
let components = NSDateComponents()
components.minute = 0
@@ -168,7 +168,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
}
dispatch_group_enter(reloadGroup)
- dataManager.loopManager.getLoopStatus { (predictedGlucose, recommendedTempBasal, lastTempBasal, lastLoopCompleted, insulinOnBoard, error) -> Void in
+ dataManager.loopManager.getLoopStatus { (predictedGlucose, _, recommendedTempBasal, lastTempBasal, lastLoopCompleted, _, error) -> Void in
if error != nil {
self.needsRefresh = true
}
@@ -222,15 +222,17 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
}
}
- reservoirVolume = dataManager.doseStore.lastReservoirValue?.unitVolume
+ if let reservoir = dataManager.doseStore.lastReservoirValue {
+ if let capacity = dataManager.pumpState?.pumpModel?.reservoirCapacity {
+ reservoirVolumeHUD.reservoirLevel = min(1, max(0, Double(reservoir.unitVolume / Double(capacity))))
+ }
- if let capacity = dataManager.pumpState?.pumpModel?.reservoirCapacity,
- resVol = reservoirVolume {
- reservoirLevel = min(1, max(0, Double(resVol / Double(capacity))))
+ reservoirVolumeHUD.reservoirVolume = reservoir.unitVolume
+ reservoirVolumeHUD.lastUpdated = reservoir.startDate
}
if let status = dataManager.latestPumpStatusFromMySentry {
- batteryLevel = Double(status.batteryRemainingPercent) / 100
+ batteryLevelHUD.batteryLevel = Double(status.batteryRemainingPercent) / 100
}
loopCompletionHUD.dosingEnabled = dataManager.loopManager.dosingEnabled
@@ -260,10 +262,8 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
private enum Section: Int {
case Charts = 0
case Status
- case Pump
- case Sensor
- static let count = 4
+ static let count = 2
}
// MARK: - Chart Section Data
@@ -333,24 +333,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
}
}
- private var reservoirLevel: Double? {
- didSet {
- reservoirVolumeHUD.reservoirLevel = reservoirLevel
- }
- }
-
- private var reservoirVolume: Double? {
- didSet {
- reservoirVolumeHUD.reservoirVolume = reservoirVolume
- }
- }
-
- private var batteryLevel: Double? {
- didSet {
- batteryLevelHUD.batteryLevel = batteryLevel
- }
- }
-
private var settingTempBasal: Bool = false {
didSet {
if let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: StatusRow.RecommendedBasal.rawValue, inSection: Section.Status.rawValue)) {
@@ -381,39 +363,10 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
// MARK: - Pump/Sensor Section Data
- private enum PumpRow: Int {
- case Date = 0
- case InsulinOnBoard
-
- static let count = 2
- }
-
- private enum SensorRow: Int {
- case State
-
- static let count = 1
- }
-
private lazy var emptyValueString: String = NSLocalizedString("––",
comment: "The detail value of a numeric cell with no value"
)
- private lazy var dateComponentsFormatter: NSDateComponentsFormatter = {
- let formatter = NSDateComponentsFormatter()
- formatter.unitsStyle = .Short
-
- return formatter
- }()
-
- private lazy var numberFormatter = NSNumberFormatter()
-
- private lazy var dateFormatter: NSDateFormatter = {
- let formatter = NSDateFormatter()
- formatter.dateStyle = .MediumStyle
- formatter.timeStyle = .MediumStyle
- return formatter
- }()
-
private lazy var timeFormatter: NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.dateStyle = .NoStyle
@@ -434,10 +387,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
return ChartRow.count
case .Status:
return StatusRow.count
- case .Pump:
- return PumpRow.count
- case .Sensor:
- return SensorRow.count
}
}
@@ -448,7 +397,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
switch Section(rawValue: indexPath.section)! {
case .Charts:
let cell = tableView.dequeueReusableCellWithIdentifier(ChartTableViewCell.className, forIndexPath: indexPath) as! ChartTableViewCell
- let frame = cell.contentView.frame
+ let frame = cell.contentView.bounds
switch ChartRow(rawValue: indexPath.row)! {
case .Glucose:
@@ -506,43 +455,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
}
}
- return cell
- case .Pump:
- let cell = tableView.dequeueReusableCellWithIdentifier(UITableViewCell.className, forIndexPath: indexPath)
- cell.selectionStyle = .None
-
- switch PumpRow(rawValue: indexPath.row)! {
- case .Date:
- cell.textLabel?.text = NSLocalizedString("Last MySentry", comment: "The title of the cell containing the last updated mysentry status packet date")
-
- if let date = dataManager.latestPumpStatusFromMySentry?.pumpDateComponents.date {
- cell.detailTextLabel?.text = dateFormatter.stringFromDate(date)
- } else {
- cell.detailTextLabel?.text = emptyValueString
- }
- case .InsulinOnBoard:
- cell.textLabel?.text = NSLocalizedString("Bolus Insulin on Board", comment: "The title of the cell containing the estimated amount of active bolus insulin in the body")
-
- if let iob = dataManager.latestPumpStatusFromMySentry?.iob {
- let numberValue = NSNumber(double: iob).descriptionWithLocale(locale)
- cell.detailTextLabel?.text = "\(numberValue) Units"
- } else {
- cell.detailTextLabel?.text = emptyValueString
- }
- }
-
- return cell
- case .Sensor:
- let cell = tableView.dequeueReusableCellWithIdentifier(UITableViewCell.className, forIndexPath: indexPath)
- cell.selectionStyle = .None
-
- switch SensorRow(rawValue: indexPath.row)! {
- case .State:
- cell.textLabel?.text = NSLocalizedString("Sensor State", comment: "The title of the cell containing the current sensor state")
-
- cell.detailTextLabel?.text = dataManager.sensorInfo?.stateDescription ?? emptyValueString
- }
-
return cell
}
}
@@ -558,8 +470,8 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
case .IOB, .Dose, .COB:
return 85
}
- case .Status, .Pump, .Sensor:
- return 44
+ case .Status:
+ return UITableViewAutomaticDimension
}
}
@@ -568,12 +480,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
case .Charts:
switch ChartRow(rawValue: indexPath.row)! {
case .Glucose:
- if let URL = NSURL(string: "dexcomcgm://") where UIApplication.sharedApplication().canOpenURL(URL) {
- UIApplication.sharedApplication().openURL(URL)
- }
- else if let URL = NSURL(string: "dexcomshare://") where UIApplication.sharedApplication().canOpenURL(URL) {
- UIApplication.sharedApplication().openURL(URL)
- }
+ performSegueWithIdentifier(PredictionTableViewController.className, sender: indexPath)
case .IOB, .Dose:
performSegueWithIdentifier(InsulinDeliveryTableViewController.className, sender: indexPath)
case .COB:
@@ -601,12 +508,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
}
}
}
- case .Sensor:
- if let URL = NSURL(string: "dexcomcgm://") {
- UIApplication.sharedApplication().openURL(URL)
- }
- case .Pump:
- break
}
}
@@ -671,6 +572,8 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
}
}
}
+ case let vc as PredictionTableViewController:
+ vc.dataManager = dataManager
default:
break
}
@@ -723,6 +626,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
let item = UIBarButtonItem(image: UIImage.workoutImage(selected: selected), style: .Plain, target: self, action: #selector(toggleWorkoutMode(_:)))
item.accessibilityLabel = NSLocalizedString("Workout Mode", comment: "The label of the workout mode toggle button")
item.accessibilityHint = selected ? NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") : NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled")
+ item.tintColor = UIColor.glucoseTintColor
return item
}
@@ -743,7 +647,21 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
@IBOutlet var loopCompletionHUD: LoopCompletionHUDView!
- @IBOutlet var glucoseHUD: GlucoseHUDView!
+ @IBOutlet var glucoseHUD: GlucoseHUDView! {
+ didSet {
+ let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openCGMApp(_:)))
+ glucoseHUD.addGestureRecognizer(tapGestureRecognizer)
+ }
+ }
+
+ @objc private func openCGMApp(_: AnyObject) {
+ if let URL = NSURL(string: "dexcomcgm://") where UIApplication.sharedApplication().canOpenURL(URL) {
+ UIApplication.sharedApplication().openURL(URL)
+ }
+ else if let URL = NSURL(string: "dexcomshare://") where UIApplication.sharedApplication().canOpenURL(URL) {
+ UIApplication.sharedApplication().openURL(URL)
+ }
+ }
@IBOutlet var basalRateHUD: BasalRateHUDView!
diff --git a/Loop/Views/BatteryLevelHUDView.swift b/Loop/Views/BatteryLevelHUDView.swift
index 38879fc816..a78efd4db9 100644
--- a/Loop/Views/BatteryLevelHUDView.swift
+++ b/Loop/Views/BatteryLevelHUDView.swift
@@ -11,7 +11,13 @@ import UIKit
final class BatteryLevelHUDView: HUDView {
- @IBOutlet private var imageView: UIImageView!
+ @IBOutlet private var levelMaskView: LevelMaskView!
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ tintColor = .unknownColor
+ }
private lazy var numberFormatter: NSNumberFormatter = {
let formatter = NSNumberFormatter()
@@ -29,7 +35,18 @@ final class BatteryLevelHUDView: HUDView {
caption.text = nil
}
- imageView.image = UIImage.batteryHUDImageWithLevel(batteryLevel)
+ switch batteryLevel {
+ case .None:
+ tintColor = .unknownColor
+ case let x? where x > 0.25:
+ tintColor = .secondaryLabelColor
+ case let x? where x > 0.10:
+ tintColor = .agingColor
+ default:
+ tintColor = .staleColor
+ }
+
+ levelMaskView.value = batteryLevel ?? 1.0
}
}
diff --git a/Loop/Views/ChartTableViewCell.swift b/Loop/Views/ChartTableViewCell.swift
index f7bd127272..fb53098f22 100644
--- a/Loop/Views/ChartTableViewCell.swift
+++ b/Loop/Views/ChartTableViewCell.swift
@@ -11,6 +11,14 @@ import UIKit
final class ChartTableViewCell: UITableViewCell {
+ @IBOutlet var placeholderView: UIView?
+
+ @IBOutlet var subtitleLabel: UILabel? {
+ didSet {
+ subtitleLabel?.textColor = UIColor.secondaryLabelColor
+ }
+ }
+
var chartView: UIView? {
didSet {
if let view = oldValue {
diff --git a/Loop/Views/GlucoseHUDView.swift b/Loop/Views/GlucoseHUDView.swift
index c7fc0ea271..67b50840b1 100644
--- a/Loop/Views/GlucoseHUDView.swift
+++ b/Loop/Views/GlucoseHUDView.swift
@@ -15,24 +15,32 @@ final class GlucoseHUDView: HUDView {
@IBOutlet private var unitLabel: UILabel! {
didSet {
- unitLabel?.text = "–"
- unitLabel?.textColor = .glucoseTintColor
+ unitLabel.text = "–"
+ unitLabel.textColor = .glucoseTintColor
}
}
@IBOutlet private var glucoseLabel: UILabel! {
didSet {
- glucoseLabel?.text = "–"
- glucoseLabel?.textColor = .glucoseTintColor
+ glucoseLabel.text = "–"
+ glucoseLabel.textColor = .glucoseTintColor
+ }
+ }
+
+ @IBOutlet private var alertLabel: UILabel! {
+ didSet {
+ alertLabel.alpha = 0
+ alertLabel.backgroundColor = UIColor.agingColor
+ alertLabel.textColor = UIColor.whiteColor()
+ alertLabel.layer.cornerRadius = 9
+ alertLabel.clipsToBounds = true
}
}
func set(glucoseValue: GlucoseValue, for unit: HKUnit, from sensor: SensorDisplayable?) {
caption?.text = timeFormatter.stringFromDate(glucoseValue.startDate)
- let numberFormatter = NSNumberFormatter()
- numberFormatter.numberStyle = .DecimalStyle
- numberFormatter.minimumFractionDigits = unit.preferredMinimumFractionDigits
+ let numberFormatter = NSNumberFormatter.glucoseFormatter(for: unit)
glucoseLabel.text = numberFormatter.stringFromNumber(glucoseValue.quantity.doubleValueForUnit(unit))
var unitStrings = [unit.glucoseUnitDisplayString]
@@ -42,6 +50,10 @@ final class GlucoseHUDView: HUDView {
}
unitLabel.text = unitStrings.joinWithSeparator(" ")
+
+ UIView.animateWithDuration(0.25) {
+ self.alertLabel.alpha = sensor?.isStateValid == true ? 0 : 1
+ }
}
private lazy var timeFormatter: NSDateFormatter = {
diff --git a/Loop/Views/LevelMaskView.swift b/Loop/Views/LevelMaskView.swift
new file mode 100644
index 0000000000..2b35bc7934
--- /dev/null
+++ b/Loop/Views/LevelMaskView.swift
@@ -0,0 +1,83 @@
+//
+// LevelMaskView.swift
+// Loop
+//
+// Created by Nate Racklyeft on 8/28/16.
+// Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import UIKit
+
+// Displays a variable-height level indicator, masked by an image.
+// Inspired by https://github.com/carekit-apple/CareKit/blob/master/CareKit/CareCard/OCKHeartView.h
+
+class LevelMaskView: UIView {
+
+ var value: Double = 1.0 {
+ didSet {
+ animateFill()
+ }
+ }
+
+ @IBInspectable var maskImage: UIImage? {
+ didSet {
+ fillView?.removeFromSuperview()
+ maskView?.removeFromSuperview()
+ maskImageView?.removeFromSuperview()
+
+ guard let maskImage = maskImage else { return }
+
+ maskView = UIView()
+ maskImageView = UIImageView(image: maskImage)
+ maskImageView!.contentMode = .Center
+ maskView!.addSubview(maskImageView!)
+
+ clipsToBounds = true
+
+ fillView = UIView()
+ fillView!.backgroundColor = tintColor
+ addSubview(fillView!)
+ }
+ }
+
+ private var fillView: UIView?
+
+ private var maskImageView: UIView?
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+
+ guard let maskImage = maskImage else { return }
+
+ let maskImageSize = maskImage.size
+
+ maskView?.frame = CGRect(origin: .zero, size: maskImageSize)
+ maskView?.center = CGPoint(x: bounds.midX, y: bounds.midY)
+ maskImageView?.frame = maskView?.bounds ?? bounds
+
+ if (fillView?.layer.animationKeys()?.count ?? 0) == 0 {
+ updateFillViewFrame()
+ }
+ }
+
+ override func tintColorDidChange() {
+ super.tintColorDidChange()
+
+ fillView?.backgroundColor = tintColor
+ }
+
+ private func animateFill() {
+ UIView.animateWithDuration(1.25, delay: 0, options: .BeginFromCurrentState, animations: {
+ self.updateFillViewFrame()
+ }, completion: nil)
+ }
+
+ private func updateFillViewFrame() {
+ guard let maskViewFrame = maskView?.frame else { return }
+
+ var fillViewFrame = maskViewFrame
+ fillViewFrame.origin.y = maskViewFrame.maxY
+ fillViewFrame.size.height = -CGFloat(value) * maskViewFrame.height
+ fillView?.frame = fillViewFrame
+ }
+}
diff --git a/Loop/Views/PredictionInputEffectTableViewCell.swift b/Loop/Views/PredictionInputEffectTableViewCell.swift
new file mode 100644
index 0000000000..b5e5c0ee8a
--- /dev/null
+++ b/Loop/Views/PredictionInputEffectTableViewCell.swift
@@ -0,0 +1,29 @@
+//
+// PredictionInputEffectTableViewCell.swift
+// Loop
+//
+// Created by Nate Racklyeft on 9/4/16.
+// Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import UIKit
+
+class PredictionInputEffectTableViewCell: UITableViewCell {
+
+ @IBOutlet weak var titleLabel: UILabel!
+
+ @IBOutlet weak var subtitleLabel: UILabel!
+
+ var enabled: Bool = true {
+ didSet {
+ if enabled {
+ titleLabel.textColor = UIColor.darkTextColor()
+ subtitleLabel.textColor = UIColor.darkTextColor()
+ } else {
+ titleLabel.textColor = UIColor.secondaryLabelColor
+ subtitleLabel.textColor = UIColor.secondaryLabelColor
+ }
+ }
+ }
+
+}
diff --git a/Loop/Views/ReservoirVolumeHUDView.swift b/Loop/Views/ReservoirVolumeHUDView.swift
index af23563112..67b549de0b 100644
--- a/Loop/Views/ReservoirVolumeHUDView.swift
+++ b/Loop/Views/ReservoirVolumeHUDView.swift
@@ -10,12 +10,60 @@ import UIKit
final class ReservoirVolumeHUDView: HUDView {
- @IBOutlet private var imageView: UIImageView!
+ @IBOutlet private var levelMaskView: LevelMaskView!
+
+ @IBOutlet private var volumeLabel: UILabel!
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ tintColor = .unknownColor
+ volumeLabel.hidden = true
+ }
+
+ var reservoirLevel: Double? {
+ didSet {
+ levelMaskView.value = reservoirLevel ?? 1.0
+
+ switch reservoirLevel {
+ case .None:
+ tintColor = .unknownColor
+ volumeLabel.hidden = true
+ case let x? where x > 0.25:
+ tintColor = .secondaryLabelColor
+ volumeLabel.hidden = true
+ case let x? where x > 0.10:
+ tintColor = .agingColor
+ volumeLabel.textColor = tintColor
+ volumeLabel.hidden = false
+ default:
+ tintColor = .staleColor
+ volumeLabel.textColor = tintColor
+ volumeLabel.hidden = false
+ }
+ }
+ }
+
+ var lastUpdated: NSDate? {
+ didSet {
+ if let date = lastUpdated {
+ caption?.text = timeFormatter.stringFromDate(date)
+ }
+ }
+ }
+
+ private lazy var timeFormatter: NSDateFormatter = {
+ let formatter = NSDateFormatter()
+ formatter.dateStyle = .NoStyle
+ formatter.timeStyle = .ShortStyle
+
+ return formatter
+ }()
private lazy var numberFormatter: NSNumberFormatter = {
let formatter = NSNumberFormatter()
formatter.numberStyle = .DecimalStyle
- formatter.minimumFractionDigits = 1
+ formatter.maximumFractionDigits = 0
return formatter
}()
@@ -23,15 +71,9 @@ final class ReservoirVolumeHUDView: HUDView {
var reservoirVolume: Double? {
didSet {
if let volume = reservoirVolume, units = numberFormatter.stringFromNumber(volume) {
- caption.text = "\(units) U"
+ volumeLabel.text = String(format: NSLocalizedString("%@U", comment: "Format string for reservoir volume. (1: The localized volume)"), units)
}
}
}
- var reservoirLevel: Double? {
- didSet {
- imageView.image = UIImage.reservoirHUDImageWithLevel(reservoirLevel)
- }
- }
-
}
diff --git a/Loop/Views/SwitchTableViewCell.swift b/Loop/Views/SwitchTableViewCell.swift
index c8a362f578..bd8a8df84f 100644
--- a/Loop/Views/SwitchTableViewCell.swift
+++ b/Loop/Views/SwitchTableViewCell.swift
@@ -13,6 +13,8 @@ final class SwitchTableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
+ @IBOutlet weak var subtitleLabel: UILabel?
+
@IBOutlet var `switch`: UISwitch?
override func prepareForReuse() {
diff --git a/LoopTests/Info.plist b/LoopTests/Info.plist
index a626cd463b..31b423aaa3 100644
--- a/LoopTests/Info.plist
+++ b/LoopTests/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 0.11.2
+ 0.12.0
CFBundleSignature
????
CFBundleVersion
diff --git a/WatchApp Extension/Info.plist b/WatchApp Extension/Info.plist
index fcb94af9e9..3c9421e03f 100644
--- a/WatchApp Extension/Info.plist
+++ b/WatchApp Extension/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
XPC!
CFBundleShortVersionString
- 0.11.2
+ 0.12.0
CFBundleSignature
????
CFBundleVersion
diff --git a/WatchApp/Info.plist b/WatchApp/Info.plist
index aa375f08a1..8872c77a19 100644
--- a/WatchApp/Info.plist
+++ b/WatchApp/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.11.2
+ 0.12.0
CFBundleSignature
????
CFBundleVersion