diff --git a/Common/Extensions/NumberFormatter.swift b/Common/Extensions/NumberFormatter.swift index fced6d67ee..e7e3cf35a4 100644 --- a/Common/Extensions/NumberFormatter.swift +++ b/Common/Extensions/NumberFormatter.swift @@ -20,7 +20,7 @@ extension NumberFormatter { return numberFormatter } - public func describingGlucose(_ value: Double, for unit: HKUnit) -> String? { + func describingGlucose(_ value: Double, for unit: HKUnit) -> String? { guard let stringValue = string(from: NSNumber(value: value)) else { return nil } @@ -35,7 +35,7 @@ extension NumberFormatter { ) } - @nonobjc public func describingGlucose(_ value: HKQuantity, for unit: HKUnit) -> String? { + @nonobjc func describingGlucose(_ value: HKQuantity, for unit: HKUnit) -> String? { return describingGlucose(value.doubleValue(for: unit), for: unit) } diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 5e4fe65a97..a61b3c68ef 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */; }; 4302F4E51D4EA75100F0FCAF /* DoseStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E41D4EA75100F0FCAF /* DoseStore.swift */; }; 43076BF31DFDBC4B0012A723 /* it.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 43076BF21DFDBC4B0012A723 /* it.lproj */; }; + 430C1ABD1E5568A80067F1AE /* StatusChartManager+LoopKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430C1ABC1E5568A80067F1AE /* StatusChartManager+LoopKit.swift */; }; 430DA58E1D4AEC230097D1CA /* NSBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430DA58D1D4AEC230097D1CA /* NSBundle.swift */; }; 430DA5901D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430DA58F1D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift */; }; 4313EDE01D8A6BF90060FA79 /* ChartContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4313EDDF1D8A6BF90060FA79 /* ChartContentView.swift */; }; @@ -103,6 +104,7 @@ 43CA93371CB98079000026B5 /* MinimedKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43CA93361CB98079000026B5 /* MinimedKit.framework */; }; 43CB2B2B1D924D450079823D /* WCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CB2B2A1D924D450079823D /* WCSession.swift */; }; 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CE7CDD1CA8B63E003CC1B0 /* Data.swift */; }; + 43CEE6E61E56AFD400CB9116 /* NightscoutUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */; }; 43DBF04C1C93B8D700B3C386 /* BolusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DBF04B1C93B8D700B3C386 /* BolusViewController.swift */; }; 43DBF0531C93EC8200B3C386 /* DeviceDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DBF0521C93EC8200B3C386 /* DeviceDataManager.swift */; }; 43DBF0591C93F73800B3C386 /* CarbEntryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DBF0581C93F73800B3C386 /* CarbEntryTableViewController.swift */; }; @@ -148,6 +150,7 @@ 43F78D4C1C914197002152D1 /* CarbKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F78D481C914197002152D1 /* CarbKit.framework */; }; 43F78D4D1C914197002152D1 /* GlucoseKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F78D491C914197002152D1 /* GlucoseKit.framework */; }; 43F78D4F1C914197002152D1 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F78D4B1C914197002152D1 /* LoopKit.framework */; }; + 43FCBBC21E51710B00343C1B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43776F9A1B8022E90074EA36 /* LaunchScreen.storyboard */; }; 4D3B40041D4A9E1A00BC6334 /* G4ShareSpy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D3B40021D4A9DFE00BC6334 /* G4ShareSpy.framework */; }; 4D5B7A4B1D457CCA00796CA9 /* GlucoseG4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5B7A4A1D457CCA00796CA9 /* GlucoseG4.swift */; }; 4F2C15741E0209F500E160D4 /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; @@ -333,6 +336,7 @@ 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 4302F4E41D4EA75100F0FCAF /* DoseStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DoseStore.swift; sourceTree = ""; }; 43076BF21DFDBC4B0012A723 /* it.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = it.lproj; sourceTree = ""; }; + 430C1ABC1E5568A80067F1AE /* StatusChartManager+LoopKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StatusChartManager+LoopKit.swift"; sourceTree = ""; }; 430DA58D1D4AEC230097D1CA /* NSBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSBundle.swift; sourceTree = ""; }; 430DA58F1D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MySentryPumpStatusMessageBody.swift; sourceTree = ""; }; 4313EDDF1D8A6BF90060FA79 /* ChartContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartContentView.swift; sourceTree = ""; }; @@ -433,6 +437,7 @@ 43CA93361CB98079000026B5 /* MinimedKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MinimedKit.framework; path = Carthage/Build/iOS/MinimedKit.framework; sourceTree = ""; }; 43CB2B2A1D924D450079823D /* WCSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WCSession.swift; sourceTree = ""; }; 43CE7CDD1CA8B63E003CC1B0 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; + 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutUploader.swift; sourceTree = ""; }; 43D533BB1CFD1DD7009E3085 /* WatchApp Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "WatchApp Extension.entitlements"; sourceTree = ""; }; 43DBF04B1C93B8D700B3C386 /* BolusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = BolusViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 43DBF0521C93EC8200B3C386 /* DeviceDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DeviceDataManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; @@ -784,6 +789,7 @@ 43CE7CDD1CA8B63E003CC1B0 /* Data.swift */, 4302F4E41D4EA75100F0FCAF /* DoseStore.swift */, C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */, + 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */, 4398973A1CD2FC2000223065 /* NSDateFormatter.swift */, 43E344A31B9E1B1C00C85C07 /* NSUserDefaults.swift */, 43BFF0CA1E466C0900FF19A9 /* StateColorPalette.swift */, @@ -850,6 +856,7 @@ 43C094491CACCC73001F6403 /* NotificationManager.swift */, 432E73CA1D24B3D6009AD15D /* RemoteDataManager.swift */, 43EB40851C82646A00472A8C /* StatusChartManager.swift */, + 430C1ABC1E5568A80067F1AE /* StatusChartManager+LoopKit.swift */, 4F70C20F1DE8FAC5006380B7 /* StatusExtensionDataManager.swift */, 4328E0341CFC0AE100E199AA /* WatchDataManager.swift */, ); @@ -1234,6 +1241,7 @@ buildActionMask = 2147483647; files = ( C1C73F081DE3D0260022FC89 /* InfoPlist.strings in Resources */, + 43FCBBC21E51710B00343C1B /* LaunchScreen.storyboard in Resources */, 43776F991B8022E90074EA36 /* Assets.xcassets in Resources */, 434F54591D28805E002A9274 /* ButtonTableViewCell.xib in Resources */, C1C73F021DE3D0250022FC89 /* Localizable.strings in Resources */, @@ -1408,8 +1416,10 @@ 435400341C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */, 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */, 43F41C371D3BF32400C11ED6 /* UIAlertController.swift in Sources */, + 430C1ABD1E5568A80067F1AE /* StatusChartManager+LoopKit.swift in Sources */, 433EA4C41D9F71C800CD78FB /* CommandResponseViewController.swift in Sources */, 434F545F1D288345002A9274 /* ShareService.swift in Sources */, + 43CEE6E61E56AFD400CB9116 /* NightscoutUploader.swift in Sources */, 4328E0331CFC091100E199AA /* WatchContext+LoopKit.swift in Sources */, 4F526D611DF8D9A900A04910 /* NetBasal.swift in Sources */, 4398973B1CD2FC2000223065 /* NSDateFormatter.swift in Sources */, diff --git a/Loop/Extensions/ChartPoint.swift b/Loop/Extensions/ChartPoint.swift index 3bb5ba888b..850274c146 100644 --- a/Loop/Extensions/ChartPoint.swift +++ b/Loop/Extensions/ChartPoint.swift @@ -101,3 +101,13 @@ extension ChartPoint { } +extension ChartPoint: TimelineValue { + public var startDate: Date { + if let dateValue = x as? ChartAxisValueDate { + return dateValue.date + } else { + return Date.distantPast + } + } +} + diff --git a/Loop/Extensions/NightscoutUploader.swift b/Loop/Extensions/NightscoutUploader.swift new file mode 100644 index 0000000000..8d33aed343 --- /dev/null +++ b/Loop/Extensions/NightscoutUploader.swift @@ -0,0 +1,51 @@ +// +// NightscoutUploader.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import CarbKit +import NightscoutUploadKit + + +extension NightscoutUploader: CarbStoreSyncDelegate { + public func carbStore(_ carbStore: CarbStore, hasEntriesNeedingUpload entries: [CarbEntry], withCompletion completionHandler: @escaping (_ uploadedObjects: [String]) -> Void) { + + let nsCarbEntries = entries.map({ MealBolusNightscoutTreatment(carbEntry: $0)}) + + upload(nsCarbEntries) { (result) in + switch result { + case .success(let ids): + // Pass new ids back + completionHandler(ids) + case .failure: + completionHandler([]) + } + } + } + + public func carbStore(_ carbStore: CarbStore, hasModifiedEntries entries: [CarbEntry], withCompletion completionHandler: @escaping (_ uploadedObjects: [String]) -> Void) { + + let nsCarbEntries = entries.map({ MealBolusNightscoutTreatment(carbEntry: $0)}) + + modifyTreatments(nsCarbEntries) { (error) in + if error != nil { + completionHandler([]) + } else { + completionHandler(entries.map { $0.externalId ?? "" } ) + } + } + } + + public func carbStore(_ carbStore: CarbStore, hasDeletedEntries ids: [String], withCompletion completionHandler: @escaping ([String]) -> Void) { + + deleteTreatmentsById(ids) { (error) in + if error != nil { + completionHandler([]) + } else { + completionHandler(ids) + } + } + } +} diff --git a/Loop/Info.plist b/Loop/Info.plist index 59af1739b9..f1bac0b228 100644 --- a/Loop/Info.plist +++ b/Loop/Info.plist @@ -2,19 +2,10 @@ - CFBundleURLTypes - - - CFBundleURLSchemes - - $(MAIN_APP_BUNDLE_IDENTIFIER) - - - + AppGroupIdentifier + $(APP_GROUP_IDENTIFIER) CFBundleDevelopmentRegion en - ITSAppUsesNonExemptEncryption - CFBundleDisplayName Loop CFBundleExecutable @@ -31,8 +22,19 @@ 1.3.0dev CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + $(MAIN_APP_BUNDLE_IDENTIFIER) + + + CFBundleVersion $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + LSApplicationQueriesSchemes dexcomcgm @@ -86,7 +88,5 @@ Glucose data retrieved from the CGM is stored securely in HealthKit. UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - AppGroupIdentifier - $(APP_GROUP_IDENTIFIER) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index bf2f03d276..f0240fa97a 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -22,7 +22,7 @@ import ShareClient import xDripG5 -final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseStoreDelegate, TransmitterDelegate, ReceiverDelegate { +final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, TransmitterDelegate, ReceiverDelegate { // MARK: - Utilities @@ -31,8 +31,8 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto /// Manages all the RileyLinks let rileyLinkManager: RileyLinkDeviceManager - /// Manages remote data (TODO: the lazy initialization isn't thread-safe) - lazy var remoteDataManager = RemoteDataManager() + /// Manages remote data + let remoteDataManager = RemoteDataManager() private var nightscoutDataManager: NightscoutDataManager! @@ -257,7 +257,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto } // Upload sensor glucose to Nightscout - remoteDataManager.nightscoutUploader?.uploadSGVFromMySentryPumpStatus(status, device: device.deviceURI) + remoteDataManager.nightscoutService.uploader?.uploadSGVFromMySentryPumpStatus(status, device: device.deviceURI) // Sentry packets are sent in groups of 3, 5s apart. Wait 11s before allowing the loop data to continue to avoid conflicting comms. DispatchQueue.global(qos: DispatchQoS.QoSClass.utility).asyncAfter(deadline: DispatchTime.now() + Double(Int64(11 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) { @@ -454,7 +454,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto // If we don't have recent pump data, or the pump was recently rewound, read new pump data before bolusing. if doseStore.lastReservoirValue == nil || doseStore.lastReservoirVolumeDrop < 0 || - doseStore.lastReservoirValue!.startDate.timeIntervalSinceNow <= TimeInterval(minutes: -5) + doseStore.lastReservoirValue!.startDate.timeIntervalSinceNow <= TimeInterval(minutes: -6) { readPumpData { (result) in switch result { @@ -528,7 +528,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto case .success(let glucoseEvents): defer { - _ = self.remoteDataManager.nightscoutUploader?.processGlucoseEvents(glucoseEvents, source: device.deviceURI) + _ = self.remoteDataManager.nightscoutService.uploader?.processGlucoseEvents(glucoseEvents, source: device.deviceURI) } self.updateEnliteSensorStatus(glucoseEvents) @@ -541,7 +541,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto return (quantity: quantity, date: e.date, isDisplayOnly: false) }) - self.glucoseStore?.addGlucoseValues(glucoseValues, device: nil, resultHandler: { (success, _, error) in + self.glucoseStore?.addGlucoseValues(glucoseValues, device: nil) { (success, _, error) in if let error = error { self.logger.addError(error, fromSource: "GlucoseStore") } @@ -549,7 +549,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto if success { NotificationCenter.default.post(name: .GlucoseUpdated, object: self) } - }) + } case .failure(let error): self.logger.addError(error, fromSource: "PumpOps") @@ -620,7 +620,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto */ private func backfillGlucoseFromShareIfNeeded(_ completion: (() -> Void)? = nil) { // We should have no G4 Share or G5 data, and a configured ShareClient and GlucoseStore. - guard latestGlucoseG4 == nil && latestGlucoseG5 == nil, let shareClient = remoteDataManager.shareClient, let glucoseStore = glucoseStore else { + guard latestGlucoseG4 == nil && latestGlucoseG5 == nil, let shareClient = remoteDataManager.shareService.client, let glucoseStore = glucoseStore else { completion?() return } @@ -747,7 +747,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto self.pumpState = nil } - remoteDataManager.nightscoutUploader?.reset() + remoteDataManager.nightscoutService.uploader?.reset() doseStore.pumpID = pumpID UserDefaults.standard.pumpID = pumpID @@ -966,69 +966,9 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto logger.addError(error, fromSource: "CarbStore") } - func carbStore(_ carbStore: CarbStore, hasEntriesNeedingUpload entries: [CarbEntry], withCompletion completionHandler: @escaping (_ uploadedObjects: [String]) -> Void) { - - guard let uploader = remoteDataManager.nightscoutUploader else { - completionHandler([]) - return - } - - let nsCarbEntries = entries.map({ MealBolusNightscoutTreatment(carbEntry: $0)}) - - uploader.upload(nsCarbEntries) { (result) in - switch result { - case .success(let ids): - // Pass new ids back - completionHandler(ids) - case .failure(let error): - self.logger.addError(error, fromSource: "NightscoutUploader") - completionHandler([]) - } - } - } - - func carbStore(_ carbStore: CarbStore, hasModifiedEntries entries: [CarbEntry], withCompletion completionHandler: @escaping (_ uploadedObjects: [String]) -> Void) { - - guard let uploader = remoteDataManager.nightscoutUploader else { - completionHandler([]) - return - } - - let nsCarbEntries = entries.map({ MealBolusNightscoutTreatment(carbEntry: $0)}) - - uploader.modifyTreatments(nsCarbEntries) { (error) in - if let error = error { - self.logger.addError(error, fromSource: "NightscoutUploader") - completionHandler([]) - } else { - completionHandler(entries.map { $0.externalId ?? "" } ) - } - } - - } - - func carbStore(_ carbStore: CarbStore, hasDeletedEntries ids: [String], withCompletion completionHandler: @escaping ([String]) -> Void) { - - guard let uploader = remoteDataManager.nightscoutUploader else { - completionHandler([]) - return - } - - uploader.deleteTreatmentsById(ids) { (error) in - if let error = error { - self.logger.addError(error, fromSource: "NightscoutUploader") - completionHandler([]) - } else { - completionHandler(ids) - } - } - completionHandler([]) - } - - // MARK: - GlucoseKit - let glucoseStore: GlucoseStore? = GlucoseStore() + let glucoseStore = GlucoseStore() // MARK: - InsulinKit @@ -1037,7 +977,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto // MARK: DoseStoreDelegate func doseStore(_ doseStore: DoseStore, hasEventsNeedingUpload pumpEvents: [PersistedPumpEvent], fromPumpID pumpID: String, withCompletion completionHandler: @escaping (_ uploadedObjects: [NSManagedObjectID]) -> Void) { - guard let uploader = remoteDataManager.nightscoutUploader, let pumpModel = pumpState?.pumpModel else { + guard let uploader = remoteDataManager.nightscoutService.uploader, let pumpModel = pumpState?.pumpModel else { completionHandler(pumpEvents.map({ $0.objectID })) return } @@ -1125,6 +1065,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto NotificationCenter.default.addObserver(self, selector: #selector(pumpStateValuesDidChange(_:)), name: .PumpStateValuesDidChange, object: pumpState) } + remoteDataManager.delegate = self statusExtensionManager = StatusExtensionDataManager(deviceDataManager: self) loopManager = LoopDataManager( deviceDataManager: self, @@ -1134,7 +1075,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto nightscoutDataManager = NightscoutDataManager(deviceDataManager: self) carbStore?.delegate = self - carbStore?.syncDelegate = self + carbStore?.syncDelegate = remoteDataManager.nightscoutService.uploader doseStore.delegate = self if UserDefaults.standard.receiverEnabled { @@ -1155,6 +1096,13 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto } +extension DeviceDataManager: RemoteDataManagerDelegate { + func remoteDataManagerdidUpdateServices(_ dataManager: RemoteDataManager) { + carbStore?.syncDelegate = dataManager.nightscoutService.uploader + } +} + + extension DeviceDataManager: CustomDebugStringConvertible { var debugDescription: String { return [ @@ -1192,4 +1140,3 @@ extension Notification.Name { /// Notification posted by the instance when loop configuration was changed static let LoopSettingsUpdated = Notification.Name(rawValue: "com.loudnate.Naterade.notification.LoopSettingsUpdated") } - diff --git a/Loop/Managers/DiagnosticLogger+LoopKit.swift b/Loop/Managers/DiagnosticLogger+LoopKit.swift index 01099258d3..e70cf3f819 100644 --- a/Loop/Managers/DiagnosticLogger+LoopKit.swift +++ b/Loop/Managers/DiagnosticLogger+LoopKit.swift @@ -26,7 +26,7 @@ extension DiagnosticLogger { addError(String(describing: message), fromSource: source) } - func addLoopStatus(startDate: Date, endDate: Date, glucose: GlucoseValue, effects: [String: [GlucoseEffect]], error: Error?, prediction: [GlucoseValue], predictionWithRetrospectiveEffect: Double, eventualBG: Double, eventualBGWithRetrospectiveEffect: Double, eventualBGWithoutMomentum: Double, recommendedTempBasal: LoopDataManager.TempBasalRecommendation?) { + func addLoopStatus(startDate: Date, endDate: Date, glucose: GlucoseValue, effects: [String: [GlucoseEffect]], error: Error?, prediction: [GlucoseValue], predictionWithRetrospectiveEffect: Double, eventualBGWithRetrospectiveEffect: Double, eventualBGWithoutMomentum: Double, recommendedTempBasal: LoopDataManager.TempBasalRecommendation?) { let dateFormatter = DateFormatter.ISO8601StrictDateFormatter() let unit = HKUnit.milligramsPerDeciliterUnit() @@ -58,7 +58,6 @@ extension DiagnosticLogger { ] }, "prediction_retrospect_delta": predictionWithRetrospectiveEffect, - "eventualBG": eventualBG, "eventualBGWithRetrospectiveEffect": eventualBGWithRetrospectiveEffect, "eventualBGWithoutMomentum": eventualBGWithoutMomentum ] diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 52fed83e18..84cd1b4e07 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -69,6 +69,7 @@ final class LoopDataManager { center.addObserver(forName: .PumpStatusUpdated, object: deviceDataManager, queue: nil) { (note) -> Void in self.dataAccessQueue.async { // Assuming insulin data is never back-dated, we don't need to remove the retrospective glucose effects + // TODO: What about a deletion? self.insulinEffect = nil self.insulinOnBoard = nil self.loop() @@ -131,7 +132,16 @@ final class LoopDataManager { private func update() throws { let updateGroup = DispatchGroup() - if glucoseChange == nil, let glucoseStore = deviceDataManager.glucoseStore { + guard let glucoseStore = deviceDataManager.glucoseStore else { + throw LoopError.configurationError("GlucoseStore") + } + + // Fetch glucose effects as far back as we want to make retroactive analysis + guard let effectStartDate = glucoseStore.latestGlucose?.startDate.addingTimeInterval(-glucoseStore.reflectionDataInterval) else { + throw LoopError.missingDataError(details: "Glucose data not available", recovery: "Check your CGM data source") + } + + if glucoseChange == nil { updateGroup.enter() glucoseStore.getRecentGlucoseChange { (values, error) in if let error = error { @@ -145,48 +155,57 @@ final class LoopDataManager { if glucoseMomentumEffect == nil { updateGroup.enter() - updateGlucoseMomentumEffect { (effects, error) in - if error == nil { - self.glucoseMomentumEffect = effects - } else { + glucoseStore.getRecentMomentumEffect { (effects, error) -> Void in + if let error = error, effects.count == 0 { + self.deviceDataManager.logger.addError(error, fromSource: "GlucoseStore") self.glucoseMomentumEffect = nil + } else { + self.glucoseMomentumEffect = effects } + updateGroup.leave() } } - if carbEffect == nil { - updateGroup.enter() - updateCarbEffect { (effects, error) in - if error == nil { - self.carbEffect = effects - } else { - self.carbEffect = nil + if let carbStore = deviceDataManager.carbStore { + if carbEffect == nil { + updateGroup.enter() + + carbStore.getGlucoseEffects(startDate: effectStartDate) { (effects, error) -> Void in + if let error = error { + self.deviceDataManager.logger.addError(error, fromSource: "CarbStore") + self.carbEffect = nil + } else { + self.carbEffect = effects + } + + updateGroup.leave() } - updateGroup.leave() } - } - if carbsOnBoardSeries == nil, let carbStore = deviceDataManager.carbStore { - updateGroup.enter() - carbStore.getCarbsOnBoardValues { (values, error) in - if let error = error { - self.deviceDataManager.logger.addError(error, fromSource: "CarbStore") - } + if carbsOnBoardSeries == nil { + updateGroup.enter() + carbStore.getCarbsOnBoardValues(startDate: effectStartDate) { (values, error) in + if let error = error { + self.deviceDataManager.logger.addError(error, fromSource: "CarbStore") + } - self.carbsOnBoardSeries = values - updateGroup.leave() + self.carbsOnBoardSeries = values + updateGroup.leave() + } } } if insulinEffect == nil { updateGroup.enter() - updateInsulinEffect { (effects, error) in - if error == nil { - self.insulinEffect = effects - } else { + deviceDataManager.doseStore.getGlucoseEffects(startDate: effectStartDate) { (effects, error) -> Void in + if let error = error { + self.deviceDataManager.logger.addError(error, fromSource: "DoseStore") self.insulinEffect = nil + } else { + self.insulinEffect = effects } + updateGroup.leave() } } @@ -196,9 +215,10 @@ final class LoopDataManager { deviceDataManager.doseStore.insulinOnBoardAtDate(Date()) { (value, error) in if let error = error { self.deviceDataManager.logger.addError(error, fromSource: "DoseStore") + self.insulinOnBoard = nil + } else { + self.insulinOnBoard = value } - - self.insulinOnBoard = value updateGroup.leave() } } @@ -213,7 +233,7 @@ final class LoopDataManager { } } - if (self.predictedGlucose == nil || self.predictedGlucoseWithoutMomentum == nil) { + if self.predictedGlucose == nil { do { try self.updatePredictedGlucoseAndRecommendedBasal() } catch let error { @@ -341,7 +361,6 @@ final class LoopDataManager { private var carbEffect: [GlucoseEffect]? { didSet { predictedGlucose = nil - predictedGlucoseWithoutMomentum = nil // Carb data may be back-dated, so re-calculate the retrospective glucose. retrospectivePredictedGlucose = nil @@ -355,7 +374,6 @@ final class LoopDataManager { } predictedGlucose = nil - predictedGlucoseWithoutMomentum = nil } } private var insulinOnBoard: InsulinValue? @@ -372,6 +390,7 @@ final class LoopDataManager { private var predictedGlucose: [GlucoseValue]? { didSet { recommendedTempBasal = nil + predictedGlucoseWithoutMomentum = nil } } private var predictedGlucoseWithoutMomentum: [GlucoseValue]? @@ -404,20 +423,6 @@ final class LoopDataManager { } } - /// The oldest date that should be used for effect calculation - private var effectStartDate: Date? { - let startDate: Date? - - if let glucoseStore = deviceDataManager.glucoseStore { - // Fetch glucose effects as far back as we want to make retroactive analysis - startDate = glucoseStore.latestGlucose?.startDate.addingTimeInterval(-glucoseStore.reflectionDataInterval) - } else { - startDate = nil - } - - return startDate - } - var lastNetBasal: NetBasal? { get { guard @@ -432,49 +437,6 @@ final class LoopDataManager { } } - private func updateCarbEffect(_ completionHandler: @escaping (_ effects: [GlucoseEffect]?, _ error: Error?) -> Void) { - guard let effectStartDate = effectStartDate else { - completionHandler(nil, LoopError.missingDataError(details: "Glucose data not available", recovery: "Check your CGM data source")) - return - } - - if let carbStore = deviceDataManager.carbStore { - carbStore.getGlucoseEffects(startDate: effectStartDate) { (effects, error) -> Void in - if let error = error { - self.deviceDataManager.logger.addError(error, fromSource: "CarbStore") - } - - completionHandler(effects, error) - } - } else { - completionHandler(nil, LoopError.missingDataError(details: "CarbStore not available", recovery: nil)) - } - } - - private func updateInsulinEffect(_ completionHandler: @escaping (_ effects: [GlucoseEffect]?, _ error: Error?) -> Void) { - deviceDataManager.doseStore.getGlucoseEffects(startDate: effectStartDate) { (effects, error) -> Void in - if let error = error { - self.deviceDataManager.logger.addError(error, fromSource: "DoseStore") - } - - completionHandler(effects, error) - } - } - - private func updateGlucoseMomentumEffect(_ completionHandler: @escaping (_ effects: [GlucoseEffect]?, _ error: Error?) -> Void) { - guard let glucoseStore = deviceDataManager.glucoseStore else { - completionHandler(nil, LoopError.missingDataError(details: "GlucoseStore not available", recovery: nil)) - return - } - glucoseStore.getRecentMomentumEffect { (effects, error) -> Void in - if let error = error { - self.deviceDataManager.logger.addError(error, fromSource: "GlucoseStore") - } - - completionHandler(effects, error) - } - } - /** Runs the glucose retrospective analysis using the latest effect data. @@ -577,7 +539,6 @@ final class LoopDataManager { predictDiff = 0 } - let eventualBG: Double = prediction.last?.quantity.doubleValue(for: unit) ?? 0 let eventualBGWithRetrospectiveEffect: Double = predictionWithRetrospectiveEffect.last?.quantity.doubleValue(for: unit) ?? 0 let eventualBGWithoutMomentum: Double = predictionWithoutMomentum.last?.quantity.doubleValue(for: unit) ?? 0 @@ -595,7 +556,6 @@ final class LoopDataManager { error: error, prediction: prediction, predictionWithRetrospectiveEffect: predictDiff, - eventualBG: eventualBG, eventualBGWithRetrospectiveEffect: eventualBGWithRetrospectiveEffect, eventualBGWithoutMomentum: eventualBGWithoutMomentum, recommendedTempBasal: recommendedTempBasal diff --git a/Loop/Managers/NightscoutDataManager.swift b/Loop/Managers/NightscoutDataManager.swift index fdc0142b73..4e704a0703 100644 --- a/Loop/Managers/NightscoutDataManager.swift +++ b/Loop/Managers/NightscoutDataManager.swift @@ -50,7 +50,7 @@ class NightscoutDataManager { func uploadLoopStatus(_ insulinOnBoard: InsulinValue? = nil, carbsOnBoard: CarbValue? = nil, predictedGlucose: [GlucoseValue]? = nil, recommendedTempBasal: LoopDataManager.TempBasalRecommendation? = nil, recommendedBolus: Double? = nil, lastTempBasal: DoseEntry? = nil, loopError: Error? = nil) { - guard deviceDataManager.remoteDataManager.nightscoutUploader != nil else { + guard deviceDataManager.remoteDataManager.nightscoutService.uploader != nil else { return } @@ -123,7 +123,7 @@ class NightscoutDataManager { func uploadDeviceStatus(_ pumpStatus: NightscoutUploadKit.PumpStatus? = nil, loopStatus: LoopStatus? = nil, includeUploaderStatus: Bool = true) { - guard let uploader = deviceDataManager.remoteDataManager.nightscoutUploader else { + guard let uploader = deviceDataManager.remoteDataManager.nightscoutService.uploader else { return } diff --git a/Loop/Managers/RemoteDataManager.swift b/Loop/Managers/RemoteDataManager.swift index 631ff5e68a..1f4939ab8f 100644 --- a/Loop/Managers/RemoteDataManager.swift +++ b/Loop/Managers/RemoteDataManager.swift @@ -13,21 +13,16 @@ import ShareClient final class RemoteDataManager { - var nightscoutUploader: NightscoutUploader? { - return nightscoutService.uploader - } + var delegate: RemoteDataManagerDelegate? var nightscoutService: NightscoutService { didSet { keychain.setNightscoutURL(nightscoutService.siteURL, secret: nightscoutService.APISecret) UIDevice.current.isBatteryMonitoringEnabled = true + delegate?.remoteDataManagerdidUpdateServices(self) } } - var shareClient: ShareClient? { - return shareService.client - } - var shareService: ShareService { didSet { try! keychain.setDexcomShareUsername(shareService.username, password: shareService.password) @@ -51,3 +46,8 @@ final class RemoteDataManager { } } } + + +protocol RemoteDataManagerDelegate: class { + func remoteDataManagerdidUpdateServices(_ dataManager: RemoteDataManager) +} diff --git a/Loop/Managers/StatusChartManager+LoopKit.swift b/Loop/Managers/StatusChartManager+LoopKit.swift new file mode 100644 index 0000000000..ac760099e0 --- /dev/null +++ b/Loop/Managers/StatusChartManager+LoopKit.swift @@ -0,0 +1,147 @@ +// +// StatusChartManager+LoopKit.swift +// Loop +// +// Created by Nate Racklyeft on 2/15/17. +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import HealthKit + +import CarbKit +import GlucoseKit +import InsulinKit +import LoopKit +import SwiftCharts + + +extension StatusChartsManager { + + private var dateFormatter: DateFormatter { + let timeFormatter = DateFormatter() + timeFormatter.dateStyle = .none + timeFormatter.timeStyle = .short + + return timeFormatter + } + + // MARK: - Glucose + + private func glucosePointsFromValues(_ glucoseValues: [GlucoseValue]) -> [ChartPoint] { + let unitString = glucoseUnit.glucoseUnitDisplayString + let formatter = dateFormatter + let glucoseFormatter = NumberFormatter.glucoseFormatter(for: glucoseUnit) + + return glucoseValues.map { + return ChartPoint( + x: ChartAxisValueDate(date: $0.startDate, formatter: formatter), + y: ChartAxisValueDoubleUnit($0.quantity.doubleValue(for: glucoseUnit), unitString: unitString, formatter: glucoseFormatter) + ) + } + } + + func setGlucoseValues(_ glucoseValues: [GlucoseValue]) { + glucosePoints = glucosePointsFromValues(glucoseValues) + } + + func setPredictedGlucoseValues(_ glucoseValues: [GlucoseValue]) { + predictedGlucosePoints = glucosePointsFromValues(glucoseValues) + } + + func setAlternatePredictedGlucoseValues(_ glucoseValues: [GlucoseValue]) { + alternatePredictedGlucosePoints = glucosePointsFromValues(glucoseValues) + } + + // MARK: - Insulin + + private var doseFormatter: NumberFormatter { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + numberFormatter.minimumFractionDigits = 2 + numberFormatter.maximumFractionDigits = 2 + + return numberFormatter + } + + func setIOBValues(_ iobValues: [InsulinValue]) { + let dateFormatter = self.dateFormatter + let doseFormatter = self.doseFormatter + + iobPoints = iobValues.map { + return ChartPoint( + x: ChartAxisValueDate(date: $0.startDate, formatter: dateFormatter), + y: ChartAxisValueDoubleUnit($0.value, unitString: "U", formatter: doseFormatter) + ) + } + } + + func setDoseEntries(_ doseEntries: [DoseEntry]) { + let dateFormatter = self.dateFormatter + let doseFormatter = self.doseFormatter + + var basalDosePoints = [ChartPoint]() + var bolusDosePoints = [ChartPoint]() + var allDosePoints = [ChartPoint]() + + for entry in doseEntries { + switch entry.unit { + case .unitsPerHour: + // TODO: Display the DateInterval + let startX = ChartAxisValueDate(date: entry.startDate, formatter: dateFormatter) + let endX = ChartAxisValueDate(date: entry.endDate, formatter: dateFormatter) + let zero = ChartAxisValueInt(0) + let value = ChartAxisValueDoubleLog(actualDouble: entry.value, unitString: "U/hour", formatter: doseFormatter) + + let valuePoints: [ChartPoint] + + if entry.value != 0 { + valuePoints = [ + ChartPoint(x: startX, y: value), + ChartPoint(x: endX, y: value) + ] + } else { + valuePoints = [] + } + + basalDosePoints += [ + ChartPoint(x: startX, y: zero) + ] + valuePoints + [ + ChartPoint(x: endX, y: zero) + ] + + allDosePoints += valuePoints + case .units: + let x = ChartAxisValueDate(date: entry.startDate, formatter: dateFormatter) + let y = ChartAxisValueDoubleLog(actualDouble: entry.value, unitString: "U", formatter: doseFormatter) + + let point = ChartPoint(x: x, y: y) + bolusDosePoints.append(point) + allDosePoints.append(point) + } + } + + self.basalDosePoints = basalDosePoints + self.bolusDosePoints = bolusDosePoints + self.allDosePoints = allDosePoints + } + + // MARK: - Carbs + + func setCOBValues(_ cobValues: [CarbValue]) { + let dateFormatter = self.dateFormatter + let integerFormatter = NumberFormatter() + integerFormatter.numberStyle = .none + integerFormatter.maximumFractionDigits = 0 + + let unit = HKUnit.gram() + let unitString = unit.unitString + + cobPoints = cobValues.map { + ChartPoint( + x: ChartAxisValueDate(date: $0.startDate, formatter: dateFormatter), + y: ChartAxisValueDoubleUnit($0.quantity.doubleValue(for: unit), unitString: unitString, formatter: integerFormatter) + ) + } + } + +} diff --git a/Loop/Managers/StatusChartManager.swift b/Loop/Managers/StatusChartManager.swift index f708e75f1f..9e1747b9d7 100644 --- a/Loop/Managers/StatusChartManager.swift +++ b/Loop/Managers/StatusChartManager.swift @@ -7,12 +7,11 @@ // import Foundation - -import CarbKit -import GlucoseKit import HealthKit -import InsulinKit + +// TODO: Remove this dependency import LoopKit + import SwiftCharts @@ -37,30 +36,13 @@ final class StatusChartsManager { return chartSettings.leading + chartSettings.trailing + (chartSettings.labelsWidthY ?? 0) + chartSettings.labelsToAxisSpacingY } - private lazy var dateFormatter: DateFormatter = { - let timeFormatter = DateFormatter() - timeFormatter.dateStyle = .none - timeFormatter.timeStyle = .short - - return timeFormatter - }() - - private lazy var doseFormatter: NumberFormatter = { - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = .decimal - numberFormatter.minimumFractionDigits = 2 - numberFormatter.maximumFractionDigits = 2 - - return numberFormatter - }() - - private lazy var integerFormatter: NumberFormatter = { + private var integerFormatter: NumberFormatter { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .none numberFormatter.maximumFractionDigits = 0 return numberFormatter - }() + } private lazy var axisLineColor = UIColor.clear @@ -72,153 +54,81 @@ final class StatusChartsManager { // MARK: - Data - var startDate = Date() - - var glucoseUnit: HKUnit = HKUnit.milligramsPerDeciliterUnit() { + /// The earliest date on the X-axis + var startDate = Date() { didSet { - if glucoseUnit != oldValue { - // Regenerate the glucose display points - let oldRange = glucoseDisplayRange - glucoseDisplayRange = oldRange - } - } - } - - var glucoseTargetRangeSchedule: GlucoseRangeSchedule? + if startDate != oldValue { + xAxisValues = nil - var glucoseValues: [GlucoseValue] = [] { - didSet { - let unitString = glucoseUnit.glucoseUnitDisplayString - let glucoseFormatter = NumberFormatter.glucoseFormatter(for: glucoseUnit) - glucosePoints = glucoseValues.map { - return ChartPoint( - x: ChartAxisValueDate(date: $0.startDate, formatter: dateFormatter), - y: ChartAxisValueDoubleUnit($0.quantity.doubleValue(for: glucoseUnit), unitString: unitString, formatter: glucoseFormatter) - ) - } - } - } - - var glucoseDisplayRange: (min: HKQuantity, max: HKQuantity)? { - didSet { - if let range = glucoseDisplayRange { - glucoseDisplayRangePoints = [ - ChartPoint(x: ChartAxisValue(scalar: 0), y: ChartAxisValueDouble(range.min.doubleValue(for: glucoseUnit))), - ChartPoint(x: ChartAxisValue(scalar: 0), y: ChartAxisValueDouble(range.max.doubleValue(for: glucoseUnit))) - ] - } else { - glucoseDisplayRangePoints = [] + updateEndDate(startDate.addingTimeInterval(TimeInterval(hours: 4))) } } } - var predictedGlucoseValues: [GlucoseValue] = [] { + /// The latest date on the X-axis + var endDate = Date() { didSet { - let unitString = glucoseUnit.glucoseUnitDisplayString - let glucoseFormatter = NumberFormatter.glucoseFormatter(for: glucoseUnit) - - predictedGlucosePoints = predictedGlucoseValues.map { - return ChartPoint( - x: ChartAxisValueDate(date: $0.startDate, formatter: dateFormatter), - y: ChartAxisValueDoubleUnit($0.quantity.doubleValue(for: glucoseUnit), unitString: unitString, formatter: glucoseFormatter) - ) + if endDate != oldValue { + xAxisValues = nil } } } - var alternatePredictedGlucoseValues: [GlucoseValue] = [] { - didSet { - let unitString = glucoseUnit.glucoseUnitDisplayString - let glucoseFormatter = NumberFormatter.glucoseFormatter(for: glucoseUnit) - - alternatePredictedGlucosePoints = alternatePredictedGlucoseValues.map { - return ChartPoint( - x: ChartAxisValueDate(date: $0.startDate, formatter: dateFormatter), - y: ChartAxisValueDoubleUnit($0.quantity.doubleValue(for: glucoseUnit), unitString: unitString, formatter: glucoseFormatter) - ) - } + /// Updates the endDate using a new candidate date + /// + /// Dates are rounded up to the next hour. + /// + /// - Parameter date: The new candidate date + private func updateEndDate(_ date: Date) { + if date > endDate { + var components = DateComponents() + components.minute = 0 + endDate = Calendar.current.nextDate(after: date, matching: components, matchingPolicy: .strict, direction: .forward) ?? date } } - var iobValues: [InsulinValue] = [] { + var glucoseUnit: HKUnit = HKUnit.milligramsPerDeciliterUnit() { didSet { - iobPoints = iobValues.map { - return ChartPoint( - x: ChartAxisValueDate(date: $0.startDate, formatter: dateFormatter), - y: ChartAxisValueDoubleUnit($0.value, unitString: "U", formatter: doseFormatter) - ) + if glucoseUnit != oldValue { + // Regenerate the glucose display points + let oldRange = glucoseDisplayRange + glucoseDisplayRange = oldRange } } } - var cobValues: [CarbValue] = [] { + var glucoseTargetRangeSchedule: GlucoseRangeSchedule? { didSet { - let unit = HKUnit.gram() - let unitString = unit.unitString - - cobPoints = cobValues.map { - ChartPoint( - x: ChartAxisValueDate(date: $0.startDate, formatter: dateFormatter), - y: ChartAxisValueDoubleUnit($0.quantity.doubleValue(for: unit), unitString: unitString, formatter: integerFormatter) - ) - } + targetGlucosePoints = [] } } - var doseEntries: [DoseEntry] = [] { + var glucoseDisplayRange: (min: HKQuantity, max: HKQuantity)? { didSet { - var basalDosePoints = [ChartPoint]() - var bolusDosePoints = [ChartPoint]() - var allDosePoints = [ChartPoint]() - - for entry in doseEntries { - switch entry.unit { - case .unitsPerHour: - // TODO: Display the DateInterval - let startX = ChartAxisValueDate(date: entry.startDate, formatter: dateFormatter) - let endX = ChartAxisValueDate(date: entry.endDate, formatter: dateFormatter) - let zero = ChartAxisValueInt(0) - let value = ChartAxisValueDoubleLog(actualDouble: entry.value, unitString: "U/hour", formatter: doseFormatter) - - basalDosePoints += [ - ChartPoint(x: startX, y: zero), - ChartPoint(x: startX, y: value), - ChartPoint(x: endX, y: value), - ChartPoint(x: endX, y: zero) - ] - - if entry.value != 0 { - allDosePoints += [ - ChartPoint(x: startX, y: value), - ChartPoint(x: endX, y: value) - ] - } - case .units: - let x = ChartAxisValueDate(date: entry.startDate, formatter: dateFormatter) - let y = ChartAxisValueDoubleLog(actualDouble: entry.value, unitString: "U", formatter: doseFormatter) - - let point = ChartPoint(x: x, y: y) - bolusDosePoints.append(point) - allDosePoints.append(point) - } + if let range = glucoseDisplayRange { + glucoseDisplayRangePoints = [ + ChartPoint(x: ChartAxisValue(scalar: 0), y: ChartAxisValueDouble(range.min.doubleValue(for: glucoseUnit))), + ChartPoint(x: ChartAxisValue(scalar: 0), y: ChartAxisValueDouble(range.max.doubleValue(for: glucoseUnit))) + ] + } else { + glucoseDisplayRangePoints = [] } - - self.basalDosePoints = basalDosePoints - self.bolusDosePoints = bolusDosePoints - self.allDosePoints = allDosePoints } } // MARK: - State - private var glucosePoints: [ChartPoint] = [] { + var glucosePoints: [ChartPoint] = [] { didSet { glucoseChart = nil - xAxisValues = nil + + if let lastDate = glucosePoints.last?.x as? ChartAxisValueDate { + updateEndDate(lastDate.date) + } } } - private var glucoseDisplayRangePoints: [ChartPoint] = [] { + var glucoseDisplayRangePoints: [ChartPoint] = [] { didSet { glucoseChart = nil } @@ -228,7 +138,10 @@ final class StatusChartsManager { var predictedGlucosePoints: [ChartPoint] = [] { didSet { glucoseChart = nil - xAxisValues = nil + + if let lastDate = predictedGlucosePoints.last?.x as? ChartAxisValueDate { + updateEndDate(lastDate.date) + } } } @@ -257,7 +170,10 @@ final class StatusChartsManager { var iobPoints: [ChartPoint] = [] { didSet { iobChart = nil - xAxisValues = nil + + if let lastDate = iobPoints.last?.x as? ChartAxisValueDate { + updateEndDate(lastDate.date) + } } } @@ -273,7 +189,10 @@ final class StatusChartsManager { var cobPoints: [ChartPoint] = [] { didSet { cobChart = nil - xAxisValues = nil + + if let lastDate = cobPoints.last?.x as? ChartAxisValueDate { + updateEndDate(lastDate.date) + } } } @@ -285,22 +204,20 @@ final class StatusChartsManager { ) } - private var basalDosePoints: [ChartPoint] = [] { - didSet { - doseChart = nil - xAxisValues = nil - } - } + var basalDosePoints: [ChartPoint] = [] + var bolusDosePoints: [ChartPoint] = [] - private var bolusDosePoints: [ChartPoint] = [] { + /// Dose points selectable when highlighting + var allDosePoints: [ChartPoint] = [] { didSet { doseChart = nil - xAxisValues = nil + + if let lastDate = allDosePoints.last?.x as? ChartAxisValueDate { + updateEndDate(lastDate.date) + } } } - private var allDosePoints: [ChartPoint] = [] - private var xAxisValues: [ChartAxisValue]? { didSet { if let xAxisValues = xAxisValues, xAxisValues.count > 1 { @@ -308,6 +225,13 @@ final class StatusChartsManager { } else { xAxisModel = nil } + + glucoseChart = nil + iobChart = nil + doseChart = nil + cobChart = nil + + targetGlucosePoints = [] } } @@ -624,7 +548,9 @@ final class StatusChartsManager { return nil } - let yAxisValues = ChartAxisValuesGenerator.generateYAxisValuesWithChartPoints(basalDosePoints + bolusDosePoints + iobDisplayRangePoints, minSegmentCount: 2, maxSegmentCount: 3, multiple: log10(2) / 2, axisValueGenerator: { ChartAxisValueDoubleLog(screenLocDouble: $0, formatter: self.integerFormatter, labelSettings: self.axisLabelSettings) }, addPaddingSegmentIfEdge: true) + let integerFormatter = self.integerFormatter + + let yAxisValues = ChartAxisValuesGenerator.generateYAxisValuesWithChartPoints(basalDosePoints + bolusDosePoints + iobDisplayRangePoints, minSegmentCount: 2, maxSegmentCount: 3, multiple: log10(2) / 2, axisValueGenerator: { ChartAxisValueDoubleLog(screenLocDouble: $0, formatter: integerFormatter, labelSettings: self.axisLabelSettings) }, addPaddingSegmentIfEdge: true) let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: axisLineColor) @@ -695,38 +621,45 @@ final class StatusChartsManager { timeFormatter.dateFormat = "h a" let points = [ - ChartPoint(x: ChartAxisValueDate(date: startDate, formatter: timeFormatter), y: ChartAxisValue(scalar: 0)), - ChartPoint(x: ChartAxisValueDate(date: startDate.addingTimeInterval(TimeInterval(hours: 4)), formatter: timeFormatter), y: ChartAxisValue(scalar: 0)), - glucosePoints.last, - predictedGlucosePoints.last, - iobPoints.last, - cobPoints.last, - basalDosePoints.last - ].flatMap { $0 } - - guard points.count > 1 else { - self.xAxisValues = [] - return - } + ChartPoint( + x: ChartAxisValueDate(date: startDate, formatter: timeFormatter), + y: ChartAxisValue(scalar: 0) + ), + ChartPoint( + x: ChartAxisValueDate(date: endDate, formatter: timeFormatter), + y: ChartAxisValue(scalar: 0) + ) + ] - let xAxisValues = ChartAxisValuesGenerator.generateXAxisValuesWithChartPoints(points, minSegmentCount: 4, maxSegmentCount: 10, multiple: TimeInterval(hours: 1), axisValueGenerator: { - ChartAxisValueDate(date: ChartAxisValueDate.dateFromScalar($0), formatter: timeFormatter, labelSettings: self.axisLabelSettings) - }, addPaddingSegmentIfEdge: false) + let xAxisValues = ChartAxisValuesGenerator.generateXAxisValuesWithChartPoints(points, + minSegmentCount: 4, + maxSegmentCount: 10, + multiple: TimeInterval(hours: 1), + axisValueGenerator: { + ChartAxisValueDate( + date: ChartAxisValueDate.dateFromScalar($0), + formatter: timeFormatter, + labelSettings: self.axisLabelSettings + ) + }, + addPaddingSegmentIfEdge: false + ) xAxisValues.first?.hidden = true xAxisValues.last?.hidden = true self.xAxisValues = xAxisValues } + /// Runs any necessary steps before rendering charts func prerender() { - glucoseChart = nil - iobChart = nil - cobChart = nil - - generateXAxisValues() + if xAxisValues == nil { + generateXAxisValues() + } - if let xAxisValues = xAxisValues, xAxisValues.count > 1, - let targets = glucoseTargetRangeSchedule { + if let xAxisValues = xAxisValues, xAxisValues.count > 1, + targetGlucosePoints.count == 0, + let targets = glucoseTargetRangeSchedule + { targetGlucosePoints = ChartPoint.pointsForGlucoseRangeSchedule(targets, xAxisValues: xAxisValues) if let override = targets.temporaryOverride { diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 059a1d396c..028ab69d6a 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -137,6 +137,8 @@ final class WatchDataManager: NSObject, WCSessionDelegate { ) deviceDataManager.loopManager.addCarbEntryAndRecommendBolus(newEntry) { (recommendation, error) in + NotificationCenter.default.post(name: .CarbEntriesDidUpdate, object: nil) + if let error = error { self.deviceDataManager.logger.addError(error, fromSource: error is CarbStore.CarbStoreError ? "CarbStore" : "Bolus") } else { diff --git a/Loop/Models/Glucose.swift b/Loop/Models/Glucose.swift index ddc1c6848a..5a57920ebe 100644 --- a/Loop/Models/Glucose.swift +++ b/Loop/Models/Glucose.swift @@ -22,7 +22,7 @@ extension Glucose: SensorDisplayable { case .ok: status = "" case .lowBattery: - status = NSLocalizedString(" Low Battery", comment: "The description of a low G5 transmitter battery with a leading space") + status = NSLocalizedString("Low Battery", comment: "The description of a low G5 transmitter battery with a leading space") case .unknown(let value): status = String(format: "%02x", value) } diff --git a/Loop/Models/ServiceAuthentication/AmplitudeService.swift b/Loop/Models/ServiceAuthentication/AmplitudeService.swift index f2712df54b..63074738c4 100644 --- a/Loop/Models/ServiceAuthentication/AmplitudeService.swift +++ b/Loop/Models/ServiceAuthentication/AmplitudeService.swift @@ -46,6 +46,7 @@ struct AmplitudeService: ServiceAuthentication { isAuthorized = true let client = Amplitude() + client.disableLocationListening() client.initializeApiKey(APIKey) self.client = client completion(true, nil) diff --git a/Loop/View Controllers/BolusViewController.swift b/Loop/View Controllers/BolusViewController.swift index 3a699f5915..876963ef60 100644 --- a/Loop/View Controllers/BolusViewController.swift +++ b/Loop/View Controllers/BolusViewController.swift @@ -69,7 +69,7 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex didSet { let amount = bolusRecommendation?.amount ?? 0 recommendedBolusAmountLabel?.text = bolusUnitsFormatter.string(from: NSNumber(value: amount)) - updateNotice(); + updateNotice() if let pendingInsulin = bolusRecommendation?.pendingInsulin { self.pendingInsulin = pendingInsulin } @@ -131,7 +131,7 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex @IBOutlet weak var noticeLabel: UILabel? { didSet { - updateNotice(); + updateNotice() } } @@ -150,13 +150,13 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex // MARK: - TableView Delegate override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if case .recommended = Rows(rawValue: indexPath.row)! { - acceptRecommendedBolus(); + if case .recommended? = Rows(rawValue: indexPath.row) { + acceptRecommendedBolus() } } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if case .recommended = Rows(rawValue: indexPath.row)! { + if case .recommended? = Rows(rawValue: indexPath.row) { cell.accessibilityCustomActions = [ UIAccessibilityCustomAction(name: NSLocalizedString("AcceptRecommendedBolus", comment: "Action to copy the recommended Bolus value to the actual Bolus Field"), target: self, selector: #selector(BolusViewController.acceptRecommendedBolus)) ] diff --git a/Loop/View Controllers/GlucoseThresholdTableViewController.swift b/Loop/View Controllers/GlucoseThresholdTableViewController.swift index e380d8c721..64607f3ac3 100644 --- a/Loop/View Controllers/GlucoseThresholdTableViewController.swift +++ b/Loop/View Controllers/GlucoseThresholdTableViewController.swift @@ -15,10 +15,10 @@ import HealthKit final class GlucoseThresholdTableViewController: TextFieldTableViewController { - public let glucoseUnits: HKUnit + public let glucoseUnit: HKUnit - init(threshold: Double?, glucoseUnits: HKUnit) { - self.glucoseUnits = glucoseUnits + init(threshold: Double?, glucoseUnit: HKUnit) { + self.glucoseUnit = glucoseUnit super.init(style: .grouped) @@ -26,10 +26,10 @@ final class GlucoseThresholdTableViewController: TextFieldTableViewController { keyboardType = .decimalPad contextHelp = NSLocalizedString("When current or forecasted BG is below miminum BG guard, Loop will not recommend a bolus, and will issue temporary basal rates of 0U/hr.", comment: "Instructions on entering minimum BG threshold") - unit = glucoseUnits.glucoseUnitDisplayString + unit = glucoseUnit.glucoseUnitDisplayString if let threshold = threshold { - value = NumberFormatter.glucoseFormatter(for: glucoseUnits).string(from: NSNumber(value: threshold)) + value = NumberFormatter.glucoseFormatter(for: glucoseUnit).string(from: NSNumber(value: threshold)) } } diff --git a/Loop/View Controllers/PredictionTableViewController.swift b/Loop/View Controllers/PredictionTableViewController.swift index a446fe1951..9b9ece90f3 100644 --- a/Loop/View Controllers/PredictionTableViewController.swift +++ b/Loop/View Controllers/PredictionTableViewController.swift @@ -142,9 +142,9 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass, U if let error = error { self.dataManager.logger.addError(error, fromSource: "GlucoseStore") self.needsRefresh = true - // TODO: Display error in the cell + self.charts.setGlucoseValues([]) } else { - self.charts.glucoseValues = values + self.charts.setGlucoseValues(values) } reloadGroup.leave() @@ -157,7 +157,7 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass, U } self.retrospectivePredictedGlucose = retrospectivePredictedGlucose - self.charts.predictedGlucoseValues = predictedGlucose ?? [] + self.charts.setPredictedGlucoseValues(predictedGlucose ?? []) reloadGroup.leave() } @@ -168,7 +168,7 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass, U self.needsRefresh = true } - self.charts.alternatePredictedGlucoseValues = predictedGlucose ?? [] + self.charts.setAlternatePredictedGlucoseValues(predictedGlucose ?? []) if let lastPoint = self.charts.alternatePredictedGlucosePoints?.last?.y { self.eventualGlucoseDescription = String(describing: lastPoint) diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index 1a033d76fc..e2a1ff0066 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -566,7 +566,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } case .minimumBGGuard: if let minBGGuard = dataManager.minimumBGGuard { - let vc = GlucoseThresholdTableViewController(threshold: minBGGuard.value, glucoseUnits: minBGGuard.unit) + let vc = GlucoseThresholdTableViewController(threshold: minBGGuard.value, glucoseUnit: minBGGuard.unit) vc.delegate = self vc.indexPath = indexPath vc.title = sender?.textLabel?.text @@ -577,7 +577,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu if let error = error { self.presentAlertController(with: error) } else if let unit = unit { - let vc = GlucoseThresholdTableViewController(threshold: nil, glucoseUnits: unit) + let vc = GlucoseThresholdTableViewController(threshold: nil, glucoseUnit: unit) vc.delegate = self vc.indexPath = indexPath vc.title = sender?.textLabel?.text @@ -730,7 +730,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu if sender.isOn { disableTransmitter() - disableReceiver(); + disableReceiver() } } @@ -840,7 +840,7 @@ extension SettingsTableViewController: TextFieldTableViewControllerDelegate { case .minimumBGGuard: if let controller = controller as? GlucoseThresholdTableViewController, let value = controller.value, let minBGGuard = valueNumberFormatter.number(from: value)?.doubleValue { - dataManager.minimumBGGuard = GlucoseThreshold(unit: controller.glucoseUnits, value: minBGGuard) + dataManager.minimumBGGuard = GlucoseThreshold(unit: controller.glucoseUnit, value: minBGGuard) } else { dataManager.minimumBGGuard = nil } diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 503a43d4e6..b38e2320cb 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -16,6 +16,21 @@ import LoopUI import SwiftCharts +private struct RefreshContext: OptionSet { + let rawValue: Int + + /// Catch-all for lastLoopCompleted, recommendedTempBasal, lastTempBasal, preferences + static let status = RefreshContext(rawValue: 1 << 0) + + static let glucose = RefreshContext(rawValue: 1 << 1) + static let insulin = RefreshContext(rawValue: 1 << 2) + static let carbs = RefreshContext(rawValue: 1 << 3) + static let targets = RefreshContext(rawValue: 1 << 4) + + static let all: RefreshContext = [.status, .glucose, .insulin, .carbs, .targets] +} + + final class StatusTableViewController: UITableViewController, UIGestureRecognizerDelegate { override func viewDidLoad() { @@ -26,9 +41,20 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize let application = UIApplication.shared notificationObservers += [ - notificationCenter.addObserver(forName: .LoopDataUpdated, object: dataManager.loopManager, queue: nil) { _ in + notificationCenter.addObserver(forName: .LoopDataUpdated, object: dataManager.loopManager, queue: nil) { note in + let context = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as! LoopDataManager.LoopUpdateContext.RawValue DispatchQueue.main.async { - self.needsRefresh = true + switch LoopDataManager.LoopUpdateContext(rawValue: context) { + case .none, .bolus?, .preferences?: + self.refreshContext.update(with: .status) + case .carbs?: + self.refreshContext.update(with: .carbs) + case .glucose?: + self.refreshContext.update(with: .glucose) + case .tempBasal?: + self.refreshContext.update(with: .insulin) + } + self.hudView.loopCompletionHUD.loopInProgress = false self.reloadData(animated: true) } @@ -40,7 +66,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize }, notificationCenter.addObserver(forName: .LoopSettingsUpdated, object: dataManager, queue: nil) { _ in DispatchQueue.main.async { - self.needsRefresh = true + self.refreshContext.update(with: .targets) self.reloadData(animated: true) } }, @@ -98,7 +124,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - needsRefresh = true + refreshContext.update(with: .status) if visible { reloadData(animated: false, to: size) } @@ -118,7 +144,20 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize } } - private var needsRefresh = true + private var refreshContext = RefreshContext.all + + private var chartStartDate: Date { + get { + return charts.startDate + } + set { + if newValue != chartStartDate { + refreshContext = .all + } + + charts.startDate = newValue + } + } private var visible = false { didSet { @@ -132,11 +171,10 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize /// /// - parameter animated: Whether the updating should be animated if possible private func reloadData(animated: Bool = false, to size: CGSize? = nil) { - if active && visible && needsRefresh { - needsRefresh = false + if active && visible && !refreshContext.isEmpty { reloading = true - // How far back should we show data? Use the screen size as a guid. + // How far back should we show data? Use the screen size as a guide. let minimumSegmentWidth: CGFloat = 50 let availableWidth = (size ?? self.tableView.bounds.size).width - self.charts.fixedHorizontalMargin let totalHours = floor(Double(availableWidth / minimumSegmentWidth)) @@ -145,7 +183,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize var components = DateComponents() components.minute = 0 let date = Date(timeIntervalSinceNow: -TimeInterval(hours: max(1, historyHours))) - charts.startDate = Calendar.current.nextDate(after: date, matching: components, matchingPolicy: .strict, direction: .backward) ?? date + chartStartDate = Calendar.current.nextDate(after: date, matching: components, matchingPolicy: .strict, direction: .backward) ?? date let reloadGroup = DispatchGroup() var newRecommendedTempBasal: LoopDataManager.TempBasalRecommendation? @@ -157,31 +195,34 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize self.charts.glucoseUnit = unit } - reloadGroup.enter() - glucoseStore.getRecentGlucoseValues(startDate: self.charts.startDate) { (values, error) -> Void in - if let error = error { - self.dataManager.logger.addError(error, fromSource: "GlucoseStore") - self.needsRefresh = true - } else { - self.charts.glucoseValues = values - } + if self.refreshContext.remove(.glucose) != nil { + reloadGroup.enter() + glucoseStore.getRecentGlucoseValues(startDate: self.chartStartDate) { (values, error) -> Void in + if let error = error { + self.dataManager.logger.addError(error, fromSource: "GlucoseStore") + self.refreshContext.update(with: .glucose) + self.charts.setGlucoseValues([]) + } else { + self.charts.setGlucoseValues(values) + } - reloadGroup.leave() + reloadGroup.leave() + } } + // For now, do this every time + _ = self.refreshContext.remove(.status) reloadGroup.enter() - self.dataManager.loopManager.getLoopStatus { (predictedGlucose, _, recommendedTempBasal, lastTempBasal, lastLoopCompleted, _, _, error) -> Void in - if error != nil { - self.needsRefresh = true - } - - self.charts.predictedGlucoseValues = predictedGlucose ?? [] + self.dataManager.loopManager.getLoopStatus { (predictedGlucose, _, recommendedTempBasal, lastTempBasal, lastLoopCompleted, _, _, _) -> Void in + self.charts.setPredictedGlucoseValues(predictedGlucose ?? []) newRecommendedTempBasal = recommendedTempBasal self.lastTempBasal = lastTempBasal self.lastLoopCompleted = lastLoopCompleted if let lastPoint = self.charts.predictedGlucosePoints.last?.y { self.eventualGlucoseDescription = String(describing: lastPoint) + } else { + self.eventualGlucoseDescription = nil } reloadGroup.leave() @@ -191,58 +232,53 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize } } - reloadGroup.enter() - dataManager.doseStore.getInsulinOnBoardValues(startDate: charts.startDate) { (values, error) -> Void in - if let error = error { - self.dataManager.logger.addError(error, fromSource: "DoseStore") - self.needsRefresh = true - } - - self.charts.iobValues = values - - if let index = values.closestIndexPriorToDate(Date()) { - self.currentIOBDescription = String(describing: self.charts.iobPoints[index].y) + if refreshContext.remove(.insulin) != nil { + reloadGroup.enter() + dataManager.doseStore.getInsulinOnBoardValues(startDate: chartStartDate) { (values, error) -> Void in + if let error = error { + self.dataManager.logger.addError(error, fromSource: "DoseStore") + self.refreshContext.update(with: .insulin) + self.charts.setIOBValues([]) + } else { + self.charts.setIOBValues(values) + } + reloadGroup.leave() } - reloadGroup.leave() - } - - reloadGroup.enter() - dataManager.doseStore.getRecentNormalizedDoseEntries(startDate: charts.startDate) { (doses, error) -> Void in - if let error = error { - self.dataManager.logger.addError(error, fromSource: "DoseStore") - self.needsRefresh = true + reloadGroup.enter() + dataManager.doseStore.getRecentNormalizedDoseEntries(startDate: chartStartDate) { (doses, error) -> Void in + if let error = error { + self.dataManager.logger.addError(error, fromSource: "DoseStore") + self.refreshContext.update(with: .insulin) + self.charts.setDoseEntries([]) + } else { + self.charts.setDoseEntries(doses) + } + reloadGroup.leave() } - self.charts.doseEntries = doses - - reloadGroup.leave() - } + reloadGroup.enter() + dataManager.doseStore.getTotalRecentUnitsDelivered { (units, _, error) in + if error != nil { + self.refreshContext.update(with: .insulin) + self.totalDelivery = nil + } else { + self.totalDelivery = units + } - reloadGroup.enter() - dataManager.doseStore.getTotalRecentUnitsDelivered { (units, _, error) in - if error != nil { - self.needsRefresh = true - } else { - self.totalDelivery = units + reloadGroup.leave() } - - reloadGroup.leave() } - if let carbStore = dataManager.carbStore { + if refreshContext.remove(.carbs) != nil, let carbStore = dataManager.carbStore { reloadGroup.enter() - carbStore.getCarbsOnBoardValues(startDate: charts.startDate) { (values, error) -> Void in + carbStore.getCarbsOnBoardValues(startDate: chartStartDate) { (values, error) -> Void in if let error = error { self.dataManager.logger.addError(error, fromSource: "CarbStore") - self.needsRefresh = true + self.refreshContext.update(with: .carbs) } - self.charts.cobValues = values - - if let index = values.closestIndexPriorToDate(Date()) { - self.currentCOBDescription = String(describing: self.charts.cobPoints[index].y) - } + self.charts.setCOBValues(values) reloadGroup.leave() } @@ -262,7 +298,9 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize hudView.loopCompletionHUD.dosingEnabled = dataManager.loopManager.dosingEnabled - charts.glucoseTargetRangeSchedule = dataManager.glucoseTargetRangeSchedule + if refreshContext.remove(.targets) != nil { + charts.glucoseTargetRangeSchedule = dataManager.glucoseTargetRangeSchedule + } workoutMode = dataManager.workoutModeEnabled @@ -275,6 +313,19 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize ) } + // Fetch the current IOB subtitle + if let index = self.charts.iobPoints.closestIndexPriorToDate(Date()) { + self.currentIOBDescription = String(describing: self.charts.iobPoints[index].y) + } else { + self.currentIOBDescription = nil + } + // Fetch the current COB subtitle + if let index = self.charts.cobPoints.closestIndexPriorToDate(Date()) { + self.currentCOBDescription = String(describing: self.charts.cobPoints[index].y) + } else { + self.currentCOBDescription = nil + } + self.charts.prerender() // Show/hide the recommended temp basal row @@ -583,7 +634,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize self.dataManager.logger.addError(error, fromSource: "TempBasal") self.presentAlertController(with: error) } else if success { - self.needsRefresh = true + self.refreshContext.update(with: .status) self.reloadData() } } @@ -650,7 +701,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize case let vc as CarbEntryTableViewController: vc.carbStore = dataManager.carbStore vc.hidesBottomBarWhenPushed = true - self.needsRefresh = true case let vc as CarbEntryEditViewController: if let carbStore = dataManager.carbStore { vc.defaultAbsorptionTimes = carbStore.defaultAbsorptionTimes @@ -702,20 +752,19 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize dataManager.loopManager.addCarbEntryAndRecommendBolus(updatedEntry) { (recommendation, error) -> Void in DispatchQueue.main.async { + self.refreshContext.update(with: .carbs) + if let error = error { // Ignore bolus wizard errors if error is CarbStore.CarbStoreError { self.presentAlertController(with: error) } else { self.dataManager.logger.addError(error, fromSource: "Bolus") - self.needsRefresh = true self.reloadData() } } else if self.active && self.visible, let bolus = recommendation?.amount, bolus > 0 { self.performSegue(withIdentifier: BolusViewController.className, sender: recommendation) - self.needsRefresh = true } else { - self.needsRefresh = true self.reloadData() } }