diff --git a/Cartfile.resolved b/Cartfile.resolved index 12591cd6f4..9ebfc76862 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.1" 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/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 3f13bb5e01..e282250008 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 */; }; @@ -271,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; }; @@ -288,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; }; @@ -467,6 +475,7 @@ 438849ED1D2A1EBB003B3F23 /* MLabService.swift */, 430DA58F1D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift */, 438849E91D297CB6003B3F23 /* NightscoutService.swift */, + 438D42F81D7C88BC003244B0 /* PredictionInputEffect.swift */, 43EA28611D517E42001BC233 /* SensorDisplayable.swift */, 437CCADF1D285C7B0075D2C3 /* ServiceAuthentication.swift */, 434F54601D28859B002A9274 /* ServiceCredential.swift */, @@ -599,6 +608,7 @@ 43E344A01B9E144300C85C07 /* Extensions */ = { isa = PBXGroup; children = ( + C17884621D51A7A400405663 /* BatteryIndicator.swift */, 4331E0771C85302200FBE832 /* CGPoint.swift */, 4346D1F51C78501000ABAFE3 /* ChartPoint.swift */, 43649A621C7A347F00523D7F /* CollectionType.swift */, @@ -609,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 = ""; @@ -628,6 +638,7 @@ 4315D2861CA5CC3B00589052 /* CarbEntryEditTableViewController.swift */, 43DBF0581C93F73800B3C386 /* CarbEntryTableViewController.swift */, 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */, + 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */, 43F5173C1D713DB0000FA422 /* RadioSelectionTableViewController.swift */, 43F5C2DA1B92A5E1003EB13D /* SettingsTableViewController.swift */, 43E3449E1B9D68E900C85C07 /* StatusTableViewController.swift */, @@ -653,6 +664,7 @@ 43FBEDD71D73843700B21F22 /* LevelMaskView.swift */, 437CEEBD1CD6E0CB003C8C80 /* LoopCompletionHUDView.swift */, 438DADC71CDE8F8B007697A5 /* LoopStateView.swift */, + 438D42FA1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift */, 437CEEC71CD84CBB003C8C80 /* ReservoirVolumeHUDView.swift */, 43A5676A1C96155700334FAC /* SwitchTableViewCell.swift */, 434F54621D28DD80002A9274 /* ValidatingIndicatorView.swift */, @@ -1031,8 +1043,10 @@ 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 */, @@ -1044,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 */, @@ -1052,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/Base.lproj/Main.storyboard b/Loop/Base.lproj/Main.storyboard index 3f53ea76bf..9519d4a64d 100644 --- a/Loop/Base.lproj/Main.storyboard +++ b/Loop/Base.lproj/Main.storyboard @@ -113,7 +113,137 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -123,7 +253,7 @@ - + @@ -133,7 +263,7 @@ - + @@ -480,6 +610,7 @@ + @@ -626,7 +757,7 @@ - + @@ -659,7 +790,7 @@ - + @@ -677,7 +808,7 @@ - + @@ -695,7 +826,7 @@ - + @@ -713,7 +844,7 @@ - + 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/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 5a66f03506..7732b94ede 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -126,7 +126,7 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter } } - func enableRileyLinkHeartbeatIfNeeded() { + private func enableRileyLinkHeartbeatIfNeeded() { if transmitter != nil { rileyLinkManager.timerTickEnabled = false } else if receiverEnabled { 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/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/View Controllers/PredictionTableViewController.swift b/Loop/View Controllers/PredictionTableViewController.swift new file mode 100644 index 0000000000..59c5c0836a --- /dev/null +++ b/Loop/View Controllers/PredictionTableViewController.swift @@ -0,0 +1,315 @@ +// +// PredictionTableViewController.swift +// Loop +// +// Created by Nate Racklyeft on 9/3/16. +// Copyright © 2016 Nathan Racklyeft. All rights reserved. +// + +import UIKit +import HealthKit + +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 var active = true { + didSet { + reloadData() + } + } + + 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 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, _, _, _, _, _, error) in + if error != nil { + self.needsRefresh = true + } + + 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: animated ? .Fade : .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.subtitleLabel?.text = input.localizedDescription(forGlucoseUnit: charts.glucoseUnit) + cell.accessoryType = selected ? .Checkmark : .None + + cell.enabled = input != .retrospection || dataManager.loopManager.retrospectiveCorrectionEnabled + + 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 fe941ab70b..8d1ec70104 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -136,7 +136,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize needsRefresh = false reloading = true - tableView.reloadSections(NSIndexSet(indexesInRange: NSMakeRange(Section.Pump.rawValue, Section.count - Section.Pump.rawValue) + tableView.reloadSections(NSIndexSet(indexesInRange: NSMakeRange(Section.Sensor.rawValue, Section.count - Section.Sensor.rawValue) ), withRowAnimation: visible ? .Automatic : .None) let calendar = NSCalendar.currentCalendar() @@ -171,7 +171,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 } @@ -265,10 +265,9 @@ 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 = 3 } // MARK: - Chart Section Data @@ -368,12 +367,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize // MARK: - Pump/Sensor Section Data - private enum PumpRow: Int { - case InsulinOnBoard = 0 - - static let count = 1 - } - private enum SensorRow: Int { case State @@ -420,8 +413,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize return ChartRow.count case .Status: return StatusRow.count - case .Pump: - return PumpRow.count case .Sensor: return SensorRow.count } @@ -434,7 +425,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: @@ -492,23 +483,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 .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) @@ -536,8 +510,8 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize case .IOB, .Dose, .COB: return 85 } - case .Status, .Pump, .Sensor: - return 44 + case .Status, .Sensor: + return UITableViewAutomaticDimension } } @@ -546,12 +520,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: @@ -583,8 +552,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize if let URL = NSURL(string: "dexcomcgm://") { UIApplication.sharedApplication().openURL(URL) } - case .Pump: - break } } @@ -649,6 +616,8 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize } } } + case let vc as PredictionTableViewController: + vc.dataManager = dataManager default: break } @@ -722,7 +691,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/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/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/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() {