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