diff --git a/.travis.yml b/.travis.yml index d695f5dfe4..ae15c2a041 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ osx_image: xcode10 # xcode_project: Loop.xcodeproj # xcode_scheme: Loop before_script: + - git config --global protocol.version 1 - set -o pipefail && xcodebuild -project Loop.xcodeproj -target Cartfile script: # Build the app target diff --git a/Cartfile b/Cartfile index f5db9d0aed..caac804a02 100644 --- a/Cartfile +++ b/Cartfile @@ -1,8 +1,7 @@ - -github "LoopKit/LoopKit" == 2.2.1 -github "LoopKit/CGMBLEKit" == 3.0 +github "LoopKit/LoopKit" "dev" +github "LoopKit/CGMBLEKit" "dev" github "i-schuetz/SwiftCharts" == 0.6.2 -github "LoopKit/dexcom-share-client-swift" == 1.0 -github "LoopKit/G4ShareSpy" == 1.0 -github "ps2/rileylink_ios" == 2.1.0 +github "LoopKit/dexcom-share-client-swift" "dev" +github "LoopKit/G4ShareSpy" "dev" +github "ps2/rileylink_ios" "dev" github "LoopKit/Amplitude-iOS" "decreepify" diff --git a/Cartfile.resolved b/Cartfile.resolved index eaad8dc7a3..1ded1a366c 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,7 +1,7 @@ github "LoopKit/Amplitude-iOS" "2137d5fd44bf630ed33e1e72d7af6d8f8612f270" -github "LoopKit/CGMBLEKit" "v3.0" -github "LoopKit/G4ShareSpy" "v1.0" -github "LoopKit/LoopKit" "v2.2.1" -github "LoopKit/dexcom-share-client-swift" "v1.0" +github "LoopKit/CGMBLEKit" "ea1267791c66e884f1013fffd36faf4555cc6eaf" +github "LoopKit/G4ShareSpy" "fed5a389e3e47e3a1953878dd21852aa5f44b360" +github "LoopKit/LoopKit" "bf4166bc77f89e22971f2030ad006967881ff082" +github "LoopKit/dexcom-share-client-swift" "b0419edf55c7f389b36cb47dd5c376bbd3d03d69" github "i-schuetz/SwiftCharts" "0.6.2" -github "ps2/rileylink_ios" "v2.1.0" +github "ps2/rileylink_ios" "46ed2c845e7c4b88c721994d8e8432e86be4c699" diff --git a/Loop/Managers/PumpManager.swift b/Common/Models/PumpManager.swift similarity index 100% rename from Loop/Managers/PumpManager.swift rename to Common/Models/PumpManager.swift diff --git a/Common/Models/PumpManagerUI.swift b/Common/Models/PumpManagerUI.swift new file mode 100644 index 0000000000..085af7eaf5 --- /dev/null +++ b/Common/Models/PumpManagerUI.swift @@ -0,0 +1,37 @@ +// +// PumpManagerUI.swift +// Loop +// +// Created by Pete Schwamb on 10/18/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit +import LoopKitUI +import MinimedKit +import MinimedKitUI + +private let managersByIdentifier: [String: PumpManagerUI.Type] = allPumpManagers.compactMap{ $0 as? PumpManagerUI.Type}.reduce(into: [:]) { (map, Type) in + map[Type.managerIdentifier] = Type +} + +typealias PumpManagerHUDViewsRawValue = [String: Any] + +func PumpManagerHUDViewsFromRawValue(_ rawValue: PumpManagerHUDViewsRawValue) -> [BaseHUDView]? { + guard let rawState = rawValue["hudProviderViews"] as? HUDProvider.HUDViewsRawState, + let managerIdentifier = rawValue["managerIdentifier"] as? String, + let manager = managersByIdentifier[managerIdentifier] + else { + return nil + } + + return manager.createHUDViews(rawValue: rawState) +} + +func PumpManagerHUDViewsRawValueFromHudProvider(_ hudProvider: HUDProvider) -> PumpManagerHUDViewsRawValue { + return [ + "managerIdentifier": hudProvider.managerIdentifier, + "hudProviderViews": hudProvider.hudViewsRawState + ] +} diff --git a/Common/Models/StatusExtensionContext.swift b/Common/Models/StatusExtensionContext.swift index f385cf30cc..c838e258eb 100644 --- a/Common/Models/StatusExtensionContext.swift +++ b/Common/Models/StatusExtensionContext.swift @@ -10,6 +10,7 @@ import Foundation import HealthKit import LoopKit +import LoopKitUI struct NetBasalContext { @@ -151,6 +152,32 @@ extension PredictedGlucoseContext: RawRepresentable { } } +struct PumpManagerHUDViewsContext: RawRepresentable { + typealias RawValue = [String: Any] + + let pumpManagerHUDViewsRawValue: PumpManagerHUDViewsRawValue + + var hudViews: [BaseHUDView]? { + return PumpManagerHUDViewsFromRawValue(pumpManagerHUDViewsRawValue) + } + + init(pumpManagerHUDViewsRawValue: PumpManagerHUDViewsRawValue) { + self.pumpManagerHUDViewsRawValue = pumpManagerHUDViewsRawValue + } + + init?(rawValue: RawValue) { + if let pumpManagerHUDViewsRawValue = rawValue["pumpManagerHUDViewsRawValue"] as? PumpManagerHUDViewsRawValue { + self.pumpManagerHUDViewsRawValue = pumpManagerHUDViewsRawValue + } else { + return nil + } + } + + var rawValue: RawValue { + return ["pumpManagerHUDViewsRawValue": pumpManagerHUDViewsRawValue] + } +} + struct StatusExtensionContext: RawRepresentable { typealias RawValue = [String: Any] private let version = 5 @@ -161,6 +188,7 @@ struct StatusExtensionContext: RawRepresentable { var batteryPercentage: Double? var reservoirCapacity: Double? var sensor: SensorDisplayableContext? + var pumpManagerHUDViewsContext: PumpManagerHUDViewsContext? init() { } @@ -184,6 +212,10 @@ struct StatusExtensionContext: RawRepresentable { if let rawValue = rawValue["sensor"] as? SensorDisplayableContext.RawValue { sensor = SensorDisplayableContext(rawValue: rawValue) } + + if let rawPumpManagerHUDViewsContext = rawValue["pumpManagerHUDViewsContext"] as? PumpManagerHUDViewsContext.RawValue { + pumpManagerHUDViewsContext = PumpManagerHUDViewsContext(rawValue: rawPumpManagerHUDViewsContext) + } } var rawValue: RawValue { @@ -197,6 +229,8 @@ struct StatusExtensionContext: RawRepresentable { raw["batteryPercentage"] = batteryPercentage raw["reservoirCapacity"] = reservoirCapacity raw["sensor"] = sensor?.rawValue + raw["pumpManagerHUDViewsContext"] = pumpManagerHUDViewsContext?.rawValue + return raw } } diff --git a/Loop Status Extension/StateColorPalette.swift b/Loop Status Extension/StateColorPalette.swift index c4e67ba9c4..e6f18b436a 100644 --- a/Loop Status Extension/StateColorPalette.swift +++ b/Loop Status Extension/StateColorPalette.swift @@ -6,6 +6,7 @@ // import LoopUI +import LoopKitUI extension StateColorPalette { static let loopStatus = StateColorPalette(unknown: .unknownColor, normal: .freshColor, warning: .agingColor, error: .staleColor) diff --git a/Loop Status Extension/StatusViewController.swift b/Loop Status Extension/StatusViewController.swift index 68d1856898..29176c2a2a 100644 --- a/Loop Status Extension/StatusViewController.swift +++ b/Loop Status Extension/StatusViewController.swift @@ -9,6 +9,7 @@ import CoreData import HealthKit import LoopKit +import LoopKitUI import LoopUI import NotificationCenter import UIKit @@ -22,8 +23,6 @@ class StatusViewController: UIViewController, NCWidgetProviding { hudView.glucoseHUD.stateColors = .cgmStatus hudView.glucoseHUD.tintColor = .glucoseTintColor hudView.basalRateHUD.tintColor = .doseTintColor - hudView.reservoirVolumeHUD.stateColors = .pumpStatus - hudView.batteryHUD.stateColors = .pumpStatus } } @IBOutlet weak var subtitleLabel: UILabel! @@ -159,7 +158,6 @@ class StatusViewController: UIViewController, NCWidgetProviding { let group = DispatchGroup() var activeInsulin: Double? - var lastReservoirValue: ReservoirValue? var glucose: [StoredGlucoseSample] = [] group.enter() @@ -173,17 +171,6 @@ class StatusViewController: UIViewController, NCWidgetProviding { group.leave() } - group.enter() - doseStore.getReservoirValues(since: .distantPast, limit: 1) { (result) in - switch result { - case .success(let values): - lastReservoirValue = values.first - case .failure: - lastReservoirValue = nil - } - group.leave() - } - charts.startDate = Calendar.current.nextDate(after: Date(timeIntervalSinceNow: .minutes(-5)), matching: DateComponents(minute: 0), matchingPolicy: .strict, direction: .backward) ?? Date() // Showing the whole history plus full prediction in the glucose plot @@ -201,13 +188,20 @@ class StatusViewController: UIViewController, NCWidgetProviding { return } - if let batteryPercentage = context.batteryPercentage { - self.hudView.batteryHUD.batteryLevel = Double(batteryPercentage) + let hudViews: [BaseHUDView] + + if let hudViewsContext = context.pumpManagerHUDViewsContext, + let contextHUDViews = hudViewsContext.hudViews + { + hudViews = contextHUDViews + } else { + hudViews = [ReservoirVolumeHUDView.instantiate(), BatteryLevelHUDView.instantiate()] } - if let reservoir = lastReservoirValue, let capacity = context.reservoirCapacity { - self.hudView.reservoirVolumeHUD.reservoirLevel = min(1, max(0, Double(reservoir.unitVolume / capacity))) - self.hudView.reservoirVolumeHUD.setReservoirVolume(volume: reservoir.unitVolume, at: reservoir.startDate) + self.hudView.removePumpManagerProvidedViews() + for view in hudViews { + view.stateColors = .pumpStatus + self.hudView.addHUDView(view) } if let netBasal = context.netBasal { diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 7176d7070b..4422013483 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -153,8 +153,6 @@ 43BFF0C51E465A2D00FF19A9 /* UIColor+HIG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0C31E4659E700FF19A9 /* UIColor+HIG.swift */; }; 43BFF0C61E465A4400FF19A9 /* UIColor+HIG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0C31E4659E700FF19A9 /* UIColor+HIG.swift */; }; 43BFF0C71E465A4F00FF19A9 /* UIColor+HIG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0C31E4659E700FF19A9 /* UIColor+HIG.swift */; }; - 43BFF0C91E465B0A00FF19A9 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0C81E465B0A00FF19A9 /* StateColorPalette.swift */; }; - 43BFF0CB1E466C0900FF19A9 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0CA1E466C0900FF19A9 /* StateColorPalette.swift */; }; 43BFF0CD1E466C8400FF19A9 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0CC1E466C8400FF19A9 /* StateColorPalette.swift */; }; 43C0944A1CACCC73001F6403 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C094491CACCC73001F6403 /* NotificationManager.swift */; }; 43C246A81D89990F0031F8D1 /* Crypto.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43C246A71D89990F0031F8D1 /* Crypto.framework */; }; @@ -177,7 +175,6 @@ 43DE925A1C5479E4001FFDE1 /* CarbEntryUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DE92581C5479E4001FFDE1 /* CarbEntryUserInfo.swift */; }; 43DE92611C555C26001FFDE1 /* AbsorptionTimeType+CarbKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DE92601C555C26001FFDE1 /* AbsorptionTimeType+CarbKit.swift */; }; 43DFB62320D4CAE7008A7BAE /* PumpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C3B6F620BBCAA30026CAFA /* PumpManager.swift */; }; - 43E0F0A51E46D1670064F7CE /* LevelHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E0F0A41E46D1670064F7CE /* LevelHUDView.swift */; }; 43E2D8C81D208D5B004DA55F /* KeychainManager+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E2D8C71D208D5B004DA55F /* KeychainManager+Loop.swift */; }; 43E2D8D41D20BF42004DA55F /* DoseMathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E2D8D31D20BF42004DA55F /* DoseMathTests.swift */; }; 43E2D8DB1D20C03B004DA55F /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; @@ -217,7 +214,6 @@ 4F11D3C220DD80B3006E072C /* WatchHistoricalGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F11D3C120DD80B3006E072C /* WatchHistoricalGlucose.swift */; }; 4F11D3C320DD84DB006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F11D3BF20DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift */; }; 4F11D3C420DD881A006E072C /* WatchHistoricalGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F11D3C120DD80B3006E072C /* WatchHistoricalGlucose.swift */; }; - 4F20AE621E6B879C00D07A06 /* ReservoirVolumeHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEC71CD84CBB003C8C80 /* ReservoirVolumeHUDView.swift */; }; 4F20AE631E6B87B100D07A06 /* ChartContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4313EDDF1D8A6BF90060FA79 /* ChartContainerView.swift */; }; 4F2C15741E0209F500E160D4 /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; 4F2C15811E0495B200E160D4 /* WatchContext+WatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F2C15801E0495B200E160D4 /* WatchContext+WatchApp.swift */; }; @@ -226,7 +222,7 @@ 4F2C15851E075B8700E160D4 /* LoopUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F75288D1DFE1DC600C322D6 /* LoopUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4F2C15931E09BF2C00E160D4 /* HUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F2C15921E09BF2C00E160D4 /* HUDView.swift */; }; 4F2C15951E09BF3C00E160D4 /* HUDView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4F2C15941E09BF3C00E160D4 /* HUDView.xib */; }; - 4F2C15971E09E94E00E160D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F2C15961E09E94E00E160D4 /* Assets.xcassets */; }; + 4F2C15971E09E94E00E160D4 /* HUDAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F2C15961E09E94E00E160D4 /* HUDAssets.xcassets */; }; 4F2C159A1E0C9E5600E160D4 /* LoopUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4F526D611DF8D9A900A04910 /* NetBasal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D601DF8D9A900A04910 /* NetBasal.swift */; }; 4F526D621DF9D95200A04910 /* NSBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430DA58D1D4AEC230097D1CA /* NSBundle.swift */; }; @@ -241,13 +237,10 @@ 4F7528941DFE1E9500C322D6 /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 4F7528951DFE1E9B00C322D6 /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 4F75289A1DFE1F6000C322D6 /* BasalRateHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEBF1CD6FCD8003C8C80 /* BasalRateHUDView.swift */; }; - 4F75289B1DFE1F6000C322D6 /* BatteryLevelHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEC91CD84DB7003C8C80 /* BatteryLevelHUDView.swift */; }; 4F75289C1DFE1F6000C322D6 /* GlucoseHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4337615E1D52F487004A3647 /* GlucoseHUDView.swift */; }; - 4F75289D1DFE1F6000C322D6 /* BaseHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEBB1CD6DE6A003C8C80 /* BaseHUDView.swift */; }; 4F75289E1DFE1F6000C322D6 /* LoopCompletionHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEBD1CD6E0CB003C8C80 /* LoopCompletionHUDView.swift */; }; 4F7528A01DFE1F9D00C322D6 /* LoopStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438DADC71CDE8F8B007697A5 /* LoopStateView.swift */; }; 4F7528A11DFE200B00C322D6 /* BasalStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B371851CE583890013C5A6 /* BasalStateView.swift */; }; - 4F7528A21DFE200B00C322D6 /* LevelMaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43FBEDD71D73843700B21F22 /* LevelMaskView.swift */; }; 4F7528A51DFE208C00C322D6 /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; 4F7528AA1DFE215100C322D6 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D5E1DF2459000A04910 /* HKUnit.swift */; }; 4F75F00220FCFE8C00B5570E /* GlucoseChartScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F75F00120FCFE8C00B5570E /* GlucoseChartScene.swift */; }; @@ -301,6 +294,8 @@ C12F21A71DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json in Resources */ = {isa = PBXBuildFile; fileRef = C12F21A61DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json */; }; C13BAD941E8009B000050CB5 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0B31E45C1BE00FF19A9 /* NumberFormatter.swift */; }; C15713821DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */; }; + C168C40621B0D53E00ADE90E /* MinimedKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C168C40521B0D53E00ADE90E /* MinimedKit.framework */; }; + C168C40821B0D53E00ADE90E /* MinimedKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C168C40721B0D53E00ADE90E /* MinimedKitUI.framework */; }; C178249A1E1999FA00D9D25C /* CaseCountable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824991E1999FA00D9D25C /* CaseCountable.swift */; }; C17824A01E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */; }; C17824A31E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json in Resources */ = {isa = PBXBuildFile; fileRef = C17824A21E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json */; }; @@ -310,6 +305,10 @@ C18C8C511D5A351900E043FB /* NightscoutDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */; }; C1C6591C1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json in Resources */ = {isa = PBXBuildFile; fileRef = C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */; }; C1C73F0D1DE3D0270022FC89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1C73F0F1DE3D0270022FC89 /* InfoPlist.strings */; }; + C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428B217806A300FAB378 /* StateColorPalette.swift */; }; + C1FB428D21791D2500FAB378 /* PumpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C3B6F620BBCAA30026CAFA /* PumpManager.swift */; }; + C1FB428F217921D600FAB378 /* PumpManagerUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428E217921D600FAB378 /* PumpManagerUI.swift */; }; + C1FB4290217922A100FAB378 /* PumpManagerUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428E217921D600FAB378 /* PumpManagerUI.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -524,11 +523,8 @@ 43785EA32122774B0057DED1 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Intents.strings; sourceTree = ""; }; 4379CFEF21112CF700AADC79 /* ShareClientUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ShareClientUI.framework; path = Carthage/Build/iOS/ShareClientUI.framework; sourceTree = SOURCE_ROOT; }; 437AFEE6203688CF008C4892 /* LoopKitUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LoopKitUI.framework; path = Carthage/Build/iOS/LoopKitUI.framework; sourceTree = ""; }; - 437CEEBB1CD6DE6A003C8C80 /* BaseHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseHUDView.swift; sourceTree = ""; }; 437CEEBD1CD6E0CB003C8C80 /* LoopCompletionHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopCompletionHUDView.swift; sourceTree = ""; }; 437CEEBF1CD6FCD8003C8C80 /* BasalRateHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasalRateHUDView.swift; sourceTree = ""; }; - 437CEEC71CD84CBB003C8C80 /* ReservoirVolumeHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReservoirVolumeHUDView.swift; sourceTree = ""; }; - 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 = ""; }; @@ -568,8 +564,6 @@ 43BFF0BB1E45C80600FF19A9 /* UIColor+Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Loop.swift"; sourceTree = ""; }; 43BFF0BE1E45C8EA00FF19A9 /* UIColor+Widget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Widget.swift"; sourceTree = ""; }; 43BFF0C31E4659E700FF19A9 /* UIColor+HIG.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+HIG.swift"; sourceTree = ""; }; - 43BFF0C81E465B0A00FF19A9 /* StateColorPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateColorPalette.swift; sourceTree = ""; }; - 43BFF0CA1E466C0900FF19A9 /* StateColorPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateColorPalette.swift; sourceTree = ""; }; 43BFF0CC1E466C8400FF19A9 /* StateColorPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateColorPalette.swift; sourceTree = ""; }; 43C094491CACCC73001F6403 /* NotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 43C246A71D89990F0031F8D1 /* Crypto.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Crypto.framework; path = Carthage/Build/iOS/Crypto.framework; sourceTree = SOURCE_ROOT; }; @@ -592,7 +586,6 @@ 43DBF0581C93F73800B3C386 /* CarbEntryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbEntryTableViewController.swift; sourceTree = ""; }; 43DE92581C5479E4001FFDE1 /* CarbEntryUserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbEntryUserInfo.swift; sourceTree = ""; }; 43DE92601C555C26001FFDE1 /* AbsorptionTimeType+CarbKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AbsorptionTimeType+CarbKit.swift"; sourceTree = ""; }; - 43E0F0A41E46D1670064F7CE /* LevelHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LevelHUDView.swift; sourceTree = ""; }; 43E2D8C71D208D5B004DA55F /* KeychainManager+Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KeychainManager+Loop.swift"; sourceTree = ""; }; 43E2D8C91D20B9E7004DA55F /* KeychainManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainManagerTests.swift; sourceTree = ""; }; 43E2D8D11D20BF42004DA55F /* DoseMathTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DoseMathTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -622,7 +615,6 @@ 43F64DD81D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleSubtitleTableViewCell.swift; sourceTree = ""; }; 43F78D251C8FC000002152D1 /* DoseMath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DoseMath.swift; sourceTree = ""; }; 43F78D4B1C914197002152D1 /* LoopKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LoopKit.framework; path = Carthage/Build/iOS/LoopKit.framework; sourceTree = SOURCE_ROOT; }; - 43FBEDD71D73843700B21F22 /* LevelMaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LevelMaskView.swift; sourceTree = ""; }; 4D3B40021D4A9DFE00BC6334 /* G4ShareSpy.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = G4ShareSpy.framework; path = Carthage/Build/iOS/G4ShareSpy.framework; sourceTree = SOURCE_ROOT; }; 4F08DE7C1E7BB6E5006741EA /* ChartAxisValueDoubleLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartAxisValueDoubleLog.swift; sourceTree = ""; }; 4F08DE7D1E7BB6E5006741EA /* ChartAxisValueDoubleUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartAxisValueDoubleUnit.swift; sourceTree = ""; }; @@ -635,7 +627,7 @@ 4F2C15801E0495B200E160D4 /* WatchContext+WatchApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WatchContext+WatchApp.swift"; sourceTree = ""; }; 4F2C15921E09BF2C00E160D4 /* HUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HUDView.swift; sourceTree = ""; }; 4F2C15941E09BF3C00E160D4 /* HUDView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HUDView.xib; sourceTree = ""; }; - 4F2C15961E09E94E00E160D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4F2C15961E09E94E00E160D4 /* HUDAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = HUDAssets.xcassets; sourceTree = ""; }; 4F526D5E1DF2459000A04910 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; 4F526D601DF8D9A900A04910 /* NetBasal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetBasal.swift; sourceTree = ""; }; 4F6663931E905FD2009E74FC /* ChartColorPalette+Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ChartColorPalette+Loop.swift"; sourceTree = ""; }; @@ -839,13 +831,18 @@ C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = far_future_high_bg_forecast.json; sourceTree = ""; }; C12F21A61DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_very_low_end_in_range.json; sourceTree = ""; }; C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MealBolusNightscoutTreatment.swift; sourceTree = ""; }; + C168C40521B0D53E00ADE90E /* MinimedKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MinimedKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C168C40721B0D53E00ADE90E /* MinimedKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MinimedKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C17824991E1999FA00D9D25C /* CaseCountable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaseCountable.swift; sourceTree = ""; }; C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseThresholdTableViewController.swift; sourceTree = ""; }; C17824A21E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_start_very_low_end_high.json; sourceTree = ""; }; C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusRecommendation.swift; sourceTree = ""; }; C18852E12082AB1A00BECC8C /* RileyLinkKitUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RileyLinkKitUI.framework; path = Carthage/Build/iOS/RileyLinkKitUI.framework; sourceTree = ""; }; C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutDataManager.swift; sourceTree = ""; }; + C1C108C22152F46D00EA5165 /* CGMBLEKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_dropping_then_rising.json; sourceTree = ""; }; + C1FB428B217806A300FAB378 /* StateColorPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateColorPalette.swift; sourceTree = ""; }; + C1FB428E217921D600FAB378 /* PumpManagerUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpManagerUI.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -911,6 +908,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C168C40621B0D53E00ADE90E /* MinimedKit.framework in Frameworks */, + C168C40821B0D53E00ADE90E /* MinimedKitUI.framework in Frameworks */, 4F7528951DFE1E9B00C322D6 /* LoopUI.framework in Frameworks */, 437AFEE520352591008C4892 /* NotificationCenter.framework in Frameworks */, ); @@ -1142,7 +1141,7 @@ 438172D81F4E9E37003C3328 /* NewPumpEvent.swift */, 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */, 43DACFFF20A2736F000F8529 /* PersistedPumpEvent.swift */, - 43BFF0CA1E466C0900FF19A9 /* StateColorPalette.swift */, + C1FB428B217806A300FAB378 /* StateColorPalette.swift */, 43F41C361D3BF32400C11ED6 /* UIAlertController.swift */, 43BFF0BB1E45C80600FF19A9 /* UIColor+Loop.swift */, 437CEEE31CDE5C0A003C8C80 /* UIImage.swift */, @@ -1204,7 +1203,6 @@ 43A567681C94880B00334FAC /* LoopDataManager.swift */, C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */, 43C094491CACCC73001F6403 /* NotificationManager.swift */, - 43C3B6F620BBCAA30026CAFA /* PumpManager.swift */, 432E73CA1D24B3D6009AD15D /* RemoteDataManager.swift */, 430C1ABC1E5568A80067F1AE /* StatusChartsManager+LoopKit.swift */, 4F70C20F1DE8FAC5006380B7 /* StatusExtensionDataManager.swift */, @@ -1251,7 +1249,7 @@ 4F75288D1DFE1DC600C322D6 /* LoopUI.h */, 4F75288E1DFE1DC600C322D6 /* Info.plist */, 4F2C15941E09BF3C00E160D4 /* HUDView.xib */, - 4F2C15961E09E94E00E160D4 /* Assets.xcassets */, + 4F2C15961E09E94E00E160D4 /* HUDAssets.xcassets */, ); path = LoopUI; sourceTree = ""; @@ -1261,19 +1259,14 @@ children = ( 437CEEBF1CD6FCD8003C8C80 /* BasalRateHUDView.swift */, 43B371851CE583890013C5A6 /* BasalStateView.swift */, - 437CEEBB1CD6DE6A003C8C80 /* BaseHUDView.swift */, - 437CEEC91CD84DB7003C8C80 /* BatteryLevelHUDView.swift */, 4313EDDF1D8A6BF90060FA79 /* ChartContainerView.swift */, 4369618F1F19C86400447E89 /* ChartPointsContextFillLayer.swift */, 4F08DE831E7BB70B006741EA /* ChartPointsScatterDownTrianglesLayer.swift */, 4F08DE841E7BB70B006741EA /* ChartPointsTouchHighlightLayerViewCache.swift */, 4337615E1D52F487004A3647 /* GlucoseHUDView.swift */, 4F2C15921E09BF2C00E160D4 /* HUDView.swift */, - 43E0F0A41E46D1670064F7CE /* LevelHUDView.swift */, - 43FBEDD71D73843700B21F22 /* LevelMaskView.swift */, 437CEEBD1CD6E0CB003C8C80 /* LoopCompletionHUDView.swift */, 438DADC71CDE8F8B007697A5 /* LoopStateView.swift */, - 437CEEC71CD84CBB003C8C80 /* ReservoirVolumeHUDView.swift */, ); path = Views; sourceTree = ""; @@ -1285,7 +1278,6 @@ 4F08DE7D1E7BB6E5006741EA /* ChartAxisValueDoubleUnit.swift */, 4FB76FCD1E8C835D00B39636 /* ChartColorPalette.swift */, 4326BA631F3A44D9007CCAD4 /* ChartLineModel.swift */, - 43BFF0C81E465B0A00FF19A9 /* StateColorPalette.swift */, ); path = Models; sourceTree = ""; @@ -1351,6 +1343,8 @@ 4FF4D0FF1E18374700846527 /* WatchContext.swift */, 4F11D3C120DD80B3006E072C /* WatchHistoricalGlucose.swift */, 4F7E8AC620E2AC0300AEA65E /* WatchPredictedGlucose.swift */, + 43C3B6F620BBCAA30026CAFA /* PumpManager.swift */, + C1FB428E217921D600FAB378 /* PumpManagerUI.swift */, ); path = Models; sourceTree = ""; @@ -1400,6 +1394,9 @@ 968DCD53F724DE56FFE51920 /* Frameworks */ = { isa = PBXGroup; children = ( + C168C40521B0D53E00ADE90E /* MinimedKit.framework */, + C168C40721B0D53E00ADE90E /* MinimedKitUI.framework */, + C1C108C22152F46D00EA5165 /* CGMBLEKitUI.framework */, 434FB6451D68F1CD007B9C70 /* Amplitude.framework */, 4344628420A7A3BE00C4BE6F /* CGMBLEKit.framework */, 438A95A71D8B9B24009D12E1 /* CGMBLEKit.framework */, @@ -1782,7 +1779,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4F2C15971E09E94E00E160D4 /* Assets.xcassets in Resources */, + 4F2C15971E09E94E00E160D4 /* HUDAssets.xcassets in Resources */, 7D70764A1FE06EE1004AC8EA /* Localizable.strings in Resources */, 7D7076451FE06EE0004AC8EA /* InfoPlist.strings in Resources */, 4F2C15951E09BF3C00E160D4 /* HUDView.xib in Resources */, @@ -1804,7 +1801,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ -f $PROJECT_DIR/.gitmodules ]; then\n echo \"Skipping checkout due to presence of .gitmodules file\"\n if [ $ACTION = \"install\" ]; then\n echo \"You're installing: Make sure to keep all submodules up-to-date and run carthage build after changes.\"\n fi\nelse\n echo \"Bootstrapping carthage dependencies\"\n /usr/local/bin/carthage bootstrap --project-directory \"$SRCROOT\" --cache-builds\nfi"; + shellScript = "if [ -f $PROJECT_DIR/.gitmodules ]; then\n echo \"Skipping checkout due to presence of .gitmodules file\"\n if [ $ACTION = \"install\" ]; then\n echo \"You're installing: Make sure to keep all submodules up-to-date and run carthage build after changes.\"\n fi\nelse\n echo \"Bootstrapping carthage dependencies\"\n unset LLVM_TARGET_TRIPLE_SUFFIX\n /usr/local/bin/carthage bootstrap --project-directory \"$SRCROOT\" --cache-builds\nfi\n"; }; 432CF88220D8BCD90066B889 /* Homebrew & Carthage Setup */ = { isa = PBXShellScriptBuildPhase; @@ -1848,7 +1845,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "carthage_frameworks_dir=${SRCROOT}/Carthage/Build/iOS\nif [ -f $PROJECT_DIR/.gitmodules && [ $ACTION != \"install\" ]; then\nfor varname in ${!SCRIPT_INPUT_FILE_*}\ndo\nexport ${varname}=${!varname/$carthage_frameworks_dir/$BUILT_PRODUCTS_DIR}\ndone\nfi\n/usr/local/bin/carthage copy-frameworks\n"; + shellScript = "carthage_frameworks_dir=${SRCROOT}/Carthage/Build/iOS\nif [ -f $PROJECT_DIR/.gitmodules ] && [ $ACTION != \"install\" ]; then\nfor varname in ${!SCRIPT_INPUT_FILE_*}\ndo\necho using $BUILT_PRODUCTS_DIR for ${varname}\nexport ${varname}=${!varname/$carthage_frameworks_dir/$BUILT_PRODUCTS_DIR}\ndone\nfi\n/usr/local/bin/carthage copy-frameworks\n"; }; 43FF3DF620A8EFE800F8E62C /* Copy Frameworks with Carthage */ = { isa = PBXShellScriptBuildPhase; @@ -1868,7 +1865,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "carthage_frameworks_dir=${SRCROOT}/Carthage/Build/iOS\nif [ -f $PROJECT_DIR/.gitmodules && [ $ACTION != \"install\" ]; then\nfor varname in ${!SCRIPT_INPUT_FILE_*}\ndo\nexport ${varname}=${!varname/$carthage_frameworks_dir/$BUILT_PRODUCTS_DIR}\ndone\nfi\n/usr/local/bin/carthage copy-frameworks"; + shellScript = "carthage_frameworks_dir=${SRCROOT}/Carthage/Build/iOS\nif [ -f $PROJECT_DIR/.gitmodules ] && [ $ACTION != \"install\" ]; then\nfor varname in ${!SCRIPT_INPUT_FILE_*}\ndo\nexport ${varname}=${!varname/$carthage_frameworks_dir/$BUILT_PRODUCTS_DIR}\ndone\nfi\n/usr/local/bin/carthage copy-frameworks\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -1887,6 +1884,7 @@ 4F2C15821E074FC600E160D4 /* NSTimeInterval.swift in Sources */, 4311FB9B1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift in Sources */, 430DA58E1D4AEC230097D1CA /* NSBundle.swift in Sources */, + C1FB428F217921D600FAB378 /* PumpManagerUI.swift in Sources */, 43C513191E864C4E001547C7 /* GlucoseRangeSchedule.swift in Sources */, 43A51E1F1EB6D62A000736CC /* CarbAbsorptionViewController.swift in Sources */, 43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */, @@ -1894,7 +1892,6 @@ 430B29932041F5B300BA9F93 /* UserDefaults+Loop.swift in Sources */, 4341F4EB1EDB92AC001C936B /* LogglyService.swift in Sources */, 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */, - 43BFF0CB1E466C0900FF19A9 /* StateColorPalette.swift in Sources */, 439A7942211F631C0041B75F /* RootNavigationController.swift in Sources */, 4F11D3C020DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */, 43F5C2DB1B92A5E1003EB13D /* SettingsTableViewController.swift in Sources */, @@ -1943,6 +1940,7 @@ 43DAD00020A2736F000F8529 /* PersistedPumpEvent.swift in Sources */, 438849EC1D29EC34003B3F23 /* AmplitudeService.swift in Sources */, 43E93FB61E469A4000EAB8DB /* NumberFormatter.swift in Sources */, + C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */, 4F08DE8F1E7BB871006741EA /* CollectionType+Loop.swift in Sources */, 435400341C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */, 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */, @@ -2076,7 +2074,9 @@ 4FB76FBA1E8C42CE00B39636 /* UIColor.swift in Sources */, 4F2C15831E0757E600E160D4 /* HKUnit.swift in Sources */, 430B29902041F57000BA9F93 /* GlucoseThreshold.swift in Sources */, + C1FB4290217922A100FAB378 /* PumpManagerUI.swift in Sources */, 434B2887206B4F07000EE07B /* WalshInsulinModel.swift in Sources */, + C1FB428D21791D2500FAB378 /* PumpManager.swift in Sources */, 43E93FB51E4675E800EAB8DB /* NumberFormatter.swift in Sources */, 43BFF0CD1E466C8400FF19A9 /* StateColorPalette.swift in Sources */, 430B29912041F57200BA9F93 /* LoopSettings.swift in Sources */, @@ -2096,7 +2096,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4F20AE621E6B879C00D07A06 /* ReservoirVolumeHUDView.swift in Sources */, 4FB76FB91E8C42B000B39636 /* CollectionType.swift in Sources */, 7D23667D21250C7E0028B67D /* LocalizedString.swift in Sources */, 4FF4D0F91E17268800846527 /* IdentifiableClass.swift in Sources */, @@ -2116,18 +2115,13 @@ 43F1C31A1F5DC87700395429 /* ChartPoint.swift in Sources */, 4F7528A11DFE200B00C322D6 /* BasalStateView.swift in Sources */, 4F20AE631E6B87B100D07A06 /* ChartContainerView.swift in Sources */, - 4F7528A21DFE200B00C322D6 /* LevelMaskView.swift in Sources */, 43BFF0C61E465A4400FF19A9 /* UIColor+HIG.swift in Sources */, 4F7528A01DFE1F9D00C322D6 /* LoopStateView.swift in Sources */, 4FB76FCE1E8C835D00B39636 /* ChartColorPalette.swift in Sources */, 4FB76FB51E8C41E200B39636 /* ChartPointsScatterDownTrianglesLayer.swift in Sources */, 4F75289A1DFE1F6000C322D6 /* BasalRateHUDView.swift in Sources */, - 4F75289B1DFE1F6000C322D6 /* BatteryLevelHUDView.swift in Sources */, 4F75289C1DFE1F6000C322D6 /* GlucoseHUDView.swift in Sources */, 4FB76FB81E8C429D00B39636 /* CGPoint.swift in Sources */, - 4F75289D1DFE1F6000C322D6 /* BaseHUDView.swift in Sources */, - 43BFF0C91E465B0A00FF19A9 /* StateColorPalette.swift in Sources */, - 43E0F0A51E46D1670064F7CE /* LevelHUDView.swift in Sources */, 4F75289E1DFE1F6000C322D6 /* LoopCompletionHUDView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Loop/Extensions/NewPumpEvent.swift b/Loop/Extensions/NewPumpEvent.swift index 52432cf229..af0e9fe9f3 100644 --- a/Loop/Extensions/NewPumpEvent.swift +++ b/Loop/Extensions/NewPumpEvent.swift @@ -10,28 +10,14 @@ import LoopKit extension NewPumpEvent { - /* - It takes a MM pump about 40s to deliver 1 Unit while bolusing - See: http://www.healthline.com/diabetesmine/ask-dmine-speed-insulin-pumps#3 - */ - private static let deliveryUnitsPerMinute = 1.5 - /// Constructs a pump event placeholder representing a bolus just enacted. /// /// - Parameters: /// - units: The units of insulin requested /// - date: The date the bolus was enacted - static func enactedBolus(units: Double, at date: Date) -> NewPumpEvent { - let dose = DoseEntry( - type: .bolus, - startDate: date, - endDate: date.addingTimeInterval(.minutes(units / NewPumpEvent.deliveryUnitsPerMinute)), - value: units, - unit: .units - ) - + static func enactedBolus(dose: DoseEntry) -> NewPumpEvent { return self.init( - date: date, + date: dose.startDate, dose: dose, isMutable: true, raw: Data(), // This can be empty, as mutable events aren't persisted diff --git a/Loop/Extensions/StateColorPalette.swift b/Loop/Extensions/StateColorPalette.swift index c4e67ba9c4..e6f18b436a 100644 --- a/Loop/Extensions/StateColorPalette.swift +++ b/Loop/Extensions/StateColorPalette.swift @@ -6,6 +6,7 @@ // import LoopUI +import LoopKitUI extension StateColorPalette { static let loopStatus = StateColorPalette(unknown: .unknownColor, normal: .freshColor, warning: .agingColor, error: .staleColor) diff --git a/Loop/Extensions/UIAlertController.swift b/Loop/Extensions/UIAlertController.swift index 0e352f9794..b841b08e4b 100644 --- a/Loop/Extensions/UIAlertController.swift +++ b/Loop/Extensions/UIAlertController.swift @@ -18,7 +18,7 @@ extension UIAlertController { - parameter handler: A closure to execute when the sheet is dismissed after selection. The closure has a single argument: - endDate: The date at which the user selected the workout to end */ - convenience init(workoutDurationSelectionHandler handler: @escaping (_ endDate: Date) -> Void) { + internal convenience init(workoutDurationSelectionHandler handler: @escaping (_ endDate: Date) -> Void) { self.init( title: NSLocalizedString("Use Workout Glucose Targets", comment: "The title of the alert controller used to select a duration for workout targets"), message: nil, @@ -51,7 +51,7 @@ extension UIAlertController { /// - cgmManagers: An array of PumpManagers /// - selectionHandler: A closure to execute when a manager is selected /// - manager: The selected manager - convenience init(pumpManagers: [PumpManagerUI.Type], selectionHandler: @escaping (_ manager: PumpManagerUI.Type) -> Void) { + internal convenience init(pumpManagers: [PumpManagerUI.Type], selectionHandler: @escaping (_ manager: PumpManagerUI.Type) -> Void) { self.init( title: NSLocalizedString("Add Pump", comment: "Action sheet title selecting Pump"), message: nil, @@ -77,7 +77,7 @@ extension UIAlertController { /// - selectionHandler: A closure to execute when either a new CGMManager or the current PumpManager is selected /// - cgmManager: The selected CGMManager type /// - pumpManager: The selected PumpManager instance - convenience init(cgmManagers: [CGMManagerUI.Type], pumpManager: CGMManager?, selectionHandler: @escaping (_ cgmManager: CGMManagerUI.Type?, _ pumpManager: CGMManager?) -> Void) { + internal convenience init(cgmManagers: [CGMManagerUI.Type], pumpManager: CGMManager?, selectionHandler: @escaping (_ cgmManager: CGMManagerUI.Type?, _ pumpManager: CGMManager?) -> Void) { self.init( title: NSLocalizedString("Add CGM", comment: "Action sheet title selecting CGM"), message: nil, @@ -105,7 +105,7 @@ extension UIAlertController { } } - convenience init(deleteCGMManagerHandler handler: @escaping (_ isDeleted: Bool) -> Void) { + internal convenience init(deleteCGMManagerHandler handler: @escaping (_ isDeleted: Bool) -> Void) { self.init( title: nil, message: NSLocalizedString("Are you sure you want to delete this CGM?", comment: "Confirmation message for deleting a CGM"), @@ -125,7 +125,7 @@ extension UIAlertController { } } - func addCancelAction(handler: ((UIAlertAction) -> Void)? = nil) { + internal func addCancelAction(handler: ((UIAlertAction) -> Void)? = nil) { let cancel = NSLocalizedString("Cancel", comment: "The title of the cancel action in an action sheet") addAction(UIAlertAction(title: cancel, style: .cancel, handler: handler)) } diff --git a/Loop/Extensions/UIImage.swift b/Loop/Extensions/UIImage.swift index 74103bfea5..4508689393 100644 --- a/Loop/Extensions/UIImage.swift +++ b/Loop/Extensions/UIImage.swift @@ -31,14 +31,6 @@ extension UIImage { return suffix } - static func batteryHUDImageWithLevel(_ level: Double?) -> UIImage? { - return UIImage(named: "battery_\(imageSuffixForLevel(level))") - } - - static func reservoirHUDImageWithLevel(_ level: Double?) -> UIImage? { - return UIImage(named: "reservoir_\(imageSuffixForLevel(level))") - } - static func preMealImage(selected: Bool) -> UIImage? { return UIImage(named: selected ? "Pre-Meal Selected" : "Pre-Meal") } diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index b4d83d3e35..b7cb3408a5 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -10,23 +10,30 @@ import HealthKit import LoopKit import LoopKitUI - final class DeviceDataManager { private let queue = DispatchQueue(label: "com.loopkit.DeviceManagerQueue", qos: .utility) var pumpManager: PumpManagerUI? { + didSet { + // If the current CGMManager is a PumpManager, we clear it out. if cgmManager is PumpManagerUI { cgmManager = nil } + pumpManagerHUDProvider = pumpManager?.hudProvider() + setupPump() + + NotificationCenter.default.post(name: .PumpManagerChanged, object: self, userInfo: nil) UserDefaults.appGroup.pumpManager = pumpManager } } + + var pumpManagerHUDProvider: HUDProvider? let logger = DiagnosticLogger.shared @@ -58,6 +65,43 @@ final class DeviceDataManager { } } + private let lockedPumpManagerStatus: Locked = Locked(nil) + + static let batteryReplacementDetectionThreshold = 0.5 + + var pumpManagerStatus: PumpManagerStatus? { + get { + return lockedPumpManagerStatus.value + } + set { + let oldValue = lockedPumpManagerStatus.value + lockedPumpManagerStatus.value = newValue + + if let status = newValue { + + loopManager.doseStore.device = status.device + + if let newBatteryValue = status.pumpBatteryChargeRemaining { + if newBatteryValue == 0 { + NotificationManager.sendPumpBatteryLowNotification() + } else { + NotificationManager.clearPumpBatteryLowNotification() + } + + if let oldBatteryValue = oldValue?.pumpBatteryChargeRemaining, newBatteryValue - oldBatteryValue >= DeviceDataManager.batteryReplacementDetectionThreshold { + AnalyticsManager.shared.pumpBatteryWasReplaced() + } + } + + // Update the pump-schedule based settings + loopManager.setScheduleTimeZone(status.timeZone) + + } else { + loopManager.doseStore.device = nil + } + } + } + /// TODO: Isolate to queue private func setupCGM() { cgmManager?.cgmManagerDelegate = self @@ -68,6 +112,12 @@ final class DeviceDataManager { private func setupPump() { pumpManager?.pumpManagerDelegate = self + + if let pumpManager = pumpManager { + self.pumpManagerStatus = pumpManager.status + self.loopManager.doseStore.device = self.pumpManagerStatus?.device + self.pumpManagerHUDProvider = pumpManager.hudProvider() + } // Proliferate PumpModel preferences to DoseStore if let pumpRecordsBasalProfileStartEvents = pumpManager?.pumpRecordsBasalProfileStartEvents { @@ -91,12 +141,13 @@ final class DeviceDataManager { init() { pumpManager = UserDefaults.appGroup.pumpManager as? PumpManagerUI + if let cgmManager = UserDefaults.appGroup.cgmManager { self.cgmManager = cgmManager } else if UserDefaults.appGroup.isCGMManagerValidPumpManager { self.cgmManager = pumpManager as? CGMManager } - + remoteDataManager.delegate = self statusExtensionManager = StatusExtensionDataManager(deviceDataManager: self) loopManager = LoopDataManager( @@ -167,28 +218,13 @@ extension DeviceDataManager: CGMManagerDelegate { extension DeviceDataManager: PumpManagerDelegate { + func pumpManager(_ pumpManager: PumpManager, didAdjustPumpClockBy adjustment: TimeInterval) { log.default("PumpManager:\(type(of: pumpManager)) did adjust pump block by \(adjustment)s") AnalyticsManager.shared.pumpTimeDidDrift(adjustment) } - func pumpManagerDidUpdatePumpBatteryChargeRemaining(_ pumpManager: PumpManager, oldValue: Double?) { - log.default("PumpManager:\(type(of: pumpManager)) did update pump battery from \(String(describing: oldValue))") - - if let newValue = pumpManager.pumpBatteryChargeRemaining { - if newValue == 0 { - NotificationManager.sendPumpBatteryLowNotification() - } else { - NotificationManager.clearPumpBatteryLowNotification() - } - - if let oldValue = oldValue, newValue - oldValue >= 0.5 { - AnalyticsManager.shared.pumpBatteryWasReplaced() - } - } - } - func pumpManagerDidUpdateState(_ pumpManager: PumpManager) { log.default("PumpManager:\(type(of: pumpManager)) did update state") @@ -214,13 +250,8 @@ extension DeviceDataManager: PumpManagerDelegate { return !(cgmManager?.providesBLEHeartbeat == true) } - func pumpManager(_ pumpManager: PumpManager, didUpdateStatus status: PumpManagerStatus) { - log.default("PumpManager:\(type(of: pumpManager)) did update status") - - loopManager.doseStore.device = status.device - // Update the pump-schedule based settings - loopManager.setScheduleTimeZone(status.timeZone) - nightscoutDataManager.upload(pumpStatus: status) + func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus) { + self.pumpManagerStatus = status } func pumpManagerWillDeactivate(_ pumpManager: PumpManager) { @@ -291,7 +322,7 @@ extension DeviceDataManager: PumpManagerDelegate { } } } - + func pumpManagerRecommendsLoop(_ pumpManager: PumpManager) { log.default("PumpManager:\(type(of: pumpManager)) recommends loop") loopManager.loop() @@ -304,6 +335,7 @@ extension DeviceDataManager: PumpManagerDelegate { func startDateToFilterNewReservoirEvents(for manager: PumpManager) -> Date { return loopManager.doseStore.lastReservoirValue?.startDate ?? .distantPast } + } @@ -338,15 +370,16 @@ extension DeviceDataManager { return } - pumpManager.enactBolus(units: units, at: startDate, willRequest: { (units, date) in - self.loopManager.addRequestedBolus(units: units, at: date, completion: nil) - }) { (error) in - if let error = error { + pumpManager.enactBolus(units: units, at: startDate, willRequest: { (dose) in + self.loopManager.addRequestedBolus(dose, completion: nil) + }) { (result) in + switch result { + case .failure(let error): self.log.error(error) NotificationManager.sendBolusFailureNotification(for: error, units: units, at: startDate) completion(error) - } else { - self.loopManager.addConfirmedBolus(units: units, at: Date()) { + case .success(let dose): + self.loopManager.addConfirmedBolus(dose) { completion(nil) } } @@ -389,8 +422,8 @@ extension DeviceDataManager: CustomDebugStringConvertible { Bundle.main.localizedNameAndVersion, "", "## DeviceDataManager", - "launchDate: \(launchDate)", - "lastError: \(String(describing: lastError))", + "* launchDate: \(launchDate)", + "* lastError: \(String(describing: lastError))", "", cgmManager != nil ? String(reflecting: cgmManager!) : "cgmManager: nil", "", @@ -402,3 +435,8 @@ extension DeviceDataManager: CustomDebugStringConvertible { ].joined(separator: "\n") } } + +extension Notification.Name { + static let PumpManagerChanged = Notification.Name(rawValue: "com.loopKit.notification.PumpManagerChanged") +} + diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 9a9347c292..8e2c60c512 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -172,7 +172,7 @@ final class LoopDataManager { fileprivate var carbsOnBoard: CarbValue? fileprivate var lastTempBasal: DoseEntry? - fileprivate var lastRequestedBolus: (units: Double, date: Date)? + fileprivate var lastRequestedBolus: DoseEntry? /// The last date at which a loop completed, from prediction to dose (if dosing is enabled) var lastLoopCompleted: Date? { @@ -451,11 +451,10 @@ extension LoopDataManager { /// Adds a bolus requested of the pump, but not confirmed. /// /// - Parameters: - /// - units: The bolus amount, in units - /// - date: The date the bolus was requested - func addRequestedBolus(units: Double, at date: Date, completion: (() -> Void)?) { + /// - dose: The DoseEntry representing the requested bolus + func addRequestedBolus(_ dose: DoseEntry, completion: (() -> Void)?) { dataAccessQueue.async { - self.lastRequestedBolus = (units: units, date: date) + self.lastRequestedBolus = dose self.notify(forChange: .bolus) completion?() @@ -465,10 +464,9 @@ extension LoopDataManager { /// Adds a bolus enacted by the pump, but not fully delivered. /// /// - Parameters: - /// - units: The bolus amount, in units - /// - date: The date the bolus was enacted - func addConfirmedBolus(units: Double, at date: Date, completion: (() -> Void)?) { - self.doseStore.addPendingPumpEvent(.enactedBolus(units: units, at: date)) { + /// - dose: The DoseEntry representing the confirmed bolus + func addConfirmedBolus(_ dose: DoseEntry, completion: (() -> Void)?) { + self.doseStore.addPendingPumpEvent(.enactedBolus(dose: dose)) { self.dataAccessQueue.async { self.lastRequestedBolus = nil self.insulinEffect = nil @@ -491,7 +489,8 @@ extension LoopDataManager { if error == nil { self.insulinEffect = nil // Expire any bolus values now represented in the insulin data - if let bolusDate = self.lastRequestedBolus?.date, bolusDate.timeIntervalSinceNow < TimeInterval(minutes: -5) { + // TODO: Ask pumpManager if dose represented in data + if let bolusEndDate = self.lastRequestedBolus?.endDate, bolusEndDate < Date() { self.lastRequestedBolus = nil } } @@ -519,7 +518,8 @@ extension LoopDataManager { self.dataAccessQueue.async { self.insulinEffect = nil // Expire any bolus values now represented in the insulin data - if areStoredValuesContinuous, let bolusDate = self.lastRequestedBolus?.date, bolusDate.timeIntervalSinceNow < TimeInterval(minutes: -5) { + // TODO: Ask pumpManager if dose represented in data + if areStoredValuesContinuous, let bolusEndDate = self.lastRequestedBolus?.endDate, bolusEndDate < Date() { self.lastRequestedBolus = nil } @@ -841,11 +841,8 @@ extension LoopDataManager { throw LoopError.missingDataError(.glucose) } - guard let pumpStatusDate = doseStore.lastReservoirValue?.startDate else { - self.predictedGlucose = nil - throw LoopError.missingDataError(.reservoir) - } - + let pumpStatusDate = doseStore.lastAddedPumpData + let startDate = Date() guard startDate.timeIntervalSince(glucose.startDate) <= settings.recencyInterval else { @@ -1179,3 +1176,9 @@ protocol LoopDataManagerDelegate: class { /// - result: The enacted basal func loopDataManager(_ manager: LoopDataManager, didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date), completion: @escaping (_ result: Result) -> Void) -> Void } + +extension DoseStore { + var lastAddedPumpData: Date { + return max(lastReservoirValue?.startDate ?? .distantPast, lastAddedPumpEvents) + } +} diff --git a/Loop/Managers/NightscoutDataManager.swift b/Loop/Managers/NightscoutDataManager.swift index 09c2a7483b..9e74600a4f 100644 --- a/Loop/Managers/NightscoutDataManager.swift +++ b/Loop/Managers/NightscoutDataManager.swift @@ -129,8 +129,43 @@ final class NightscoutDataManager { let loopVersion = Bundle.main.shortVersionString let loopStatus = LoopStatus(name: loopName, version: loopVersion, timestamp: statusTime, iob: iob, cob: cob, predicted: predicted, recommendedTempBasal: recommended, recommendedBolus: recommendedBolus, enacted: loopEnacted, failureReason: loopError) + - upload(pumpStatus: nil, loopStatus: loopStatus, deviceName: nil, firmwareVersion: nil, lastValidFrequency: nil, lastTuned: nil, uploaderStatus: getUploaderStatus()) + + let pumpStatus: NightscoutUploadKit.PumpStatus? + + if let pumpManagerStatus = deviceManager.pumpManagerStatus + { + + let battery: BatteryStatus? + + if let chargeRemaining = pumpManagerStatus.pumpBatteryChargeRemaining { + battery = BatteryStatus(percent: Int(round(chargeRemaining * 100)), voltage: nil, status: nil) + } else { + battery = nil + } + + let bolusing: Bool + if case .inProgress = pumpManagerStatus.bolusState { + bolusing = true + } else { + bolusing = false + } + + pumpStatus = NightscoutUploadKit.PumpStatus( + clock: Date(), + pumpID: pumpManagerStatus.device.localIdentifier ?? "Unknown", + iob: nil, + battery: battery, + suspended: pumpManagerStatus.basalDeliveryState == .suspended, + bolusing: bolusing, + reservoir: deviceManager.loopManager.doseStore.lastReservoirValue?.unitVolume, + secondsFromGMT: pumpManagerStatus.timeZone.secondsFromGMT()) + } else { + pumpStatus = nil + } + + upload(pumpStatus: pumpStatus, loopStatus: loopStatus, deviceName: nil, firmwareVersion: nil, uploaderStatus: getUploaderStatus()) } @@ -147,21 +182,11 @@ final class NightscoutDataManager { return UploaderStatus(name: uploaderDevice.name, timestamp: Date(), battery: battery) } - func upload(pumpStatus: PumpManagerStatus) { - upload( - pumpStatus: pumpStatus.pumpStatus, - deviceName: pumpStatus.device?.name, - firmwareVersion: pumpStatus.device?.firmwareVersion, - lastValidFrequency: pumpStatus.lastValidFrequency, - lastTuned: pumpStatus.lastTuned - ) - } - - func upload(pumpStatus: NightscoutUploadKit.PumpStatus?, deviceName: String?, firmwareVersion: String?, lastValidFrequency: Measurement?, lastTuned: Date?) { - upload(pumpStatus: pumpStatus, loopStatus: nil, deviceName: deviceName, firmwareVersion: firmwareVersion, lastValidFrequency: lastValidFrequency, lastTuned: lastTuned, uploaderStatus: nil) + func upload(pumpStatus: NightscoutUploadKit.PumpStatus?, deviceName: String?, firmwareVersion: String?) { + upload(pumpStatus: pumpStatus, loopStatus: nil, deviceName: deviceName, firmwareVersion: firmwareVersion, uploaderStatus: nil) } - private func upload(pumpStatus: NightscoutUploadKit.PumpStatus?, loopStatus: LoopStatus?, deviceName: String?, firmwareVersion: String?, lastValidFrequency: Measurement?, lastTuned: Date?, uploaderStatus: UploaderStatus?) { + private func upload(pumpStatus: NightscoutUploadKit.PumpStatus?, loopStatus: LoopStatus?, deviceName: String?, firmwareVersion: String?, uploaderStatus: UploaderStatus?) { guard let uploader = deviceManager.remoteDataManager.nightscoutService.uploader else { return @@ -176,22 +201,8 @@ final class NightscoutDataManager { let uploaderDevice = UIDevice.current - var radioAdapter: NightscoutUploadKit.RadioAdapter? = nil - - if let firmwareVersion = firmwareVersion { - radioAdapter = NightscoutUploadKit.RadioAdapter( - hardware: "RileyLink", - frequency: lastValidFrequency?.value, - name: deviceName ?? "Unknown", - lastTuned: lastTuned, - firmwareVersion: firmwareVersion, - RSSI: nil, // TODO: device.RSSI, - pumpRSSI: nil // TODO: device.pumpRSSI - ) - } - // Build DeviceStatus - let deviceStatus = DeviceStatus(device: "loop://\(uploaderDevice.name)", timestamp: Date(), pumpStatus: pumpStatus, uploaderStatus: uploaderStatus, loopStatus: loopStatus, radioAdapter: radioAdapter) + let deviceStatus = DeviceStatus(device: "loop://\(uploaderDevice.name)", timestamp: Date(), pumpStatus: pumpStatus, uploaderStatus: uploaderStatus, loopStatus: loopStatus, radioAdapter: nil) self.lastDeviceStatusUpload = Date() uploader.uploadDeviceStatus(deviceStatus) @@ -231,35 +242,3 @@ final class NightscoutDataManager { } } - -private extension PumpManagerStatus { - var batteryStatus: NightscoutUploadKit.BatteryStatus? { - return NightscoutUploadKit.BatteryStatus( - percent: battery?.percent != nil ? Int(battery!.percent! * 100) : nil, - voltage: battery?.voltage?.converted(to: .volts).value, - status: { - switch battery?.state { - case .normal?: - return .normal - case .low?: - return .low - case .none: - return nil - } - }() - ) - } - - var pumpStatus: NightscoutUploadKit.PumpStatus { - return PumpStatus( - clock: date, - pumpID: device?.localIdentifier ?? "", - iob: nil, - battery: batteryStatus, - suspended: isSuspended, - bolusing: isBolusing, - reservoir: remainingReservoir?.doubleValue(for: .internationalUnit()), - secondsFromGMT: timeZone.secondsFromGMT() - ) - } -} diff --git a/Loop/Managers/StatusExtensionDataManager.swift b/Loop/Managers/StatusExtensionDataManager.swift index ae5a1c13f8..f22b350059 100644 --- a/Loop/Managers/StatusExtensionDataManager.swift +++ b/Loop/Managers/StatusExtensionDataManager.swift @@ -18,6 +18,7 @@ final class StatusExtensionDataManager { self.deviceManager = deviceDataManager NotificationCenter.default.addObserver(self, selector: #selector(update(_:)), name: .LoopDataUpdated, object: deviceDataManager.loopManager) + NotificationCenter.default.addObserver(self, selector: #selector(update(_:)), name: .PumpManagerChanged, object: nil) } fileprivate var defaults: UserDefaults? { @@ -98,8 +99,9 @@ final class StatusExtensionDataManager { context.netBasal = NetBasalContext(rate: netBasal.rate, percentage: netBasal.percent, start: netBasal.start, end: netBasal.end) } - - context.batteryPercentage = dataManager.pumpManager?.pumpBatteryChargeRemaining + + + context.batteryPercentage = dataManager.pumpManager?.status.pumpBatteryChargeRemaining context.reservoirCapacity = dataManager.pumpManager?.pumpReservoirCapacity if let sensorInfo = dataManager.cgmManager?.sensorState { @@ -110,6 +112,10 @@ final class StatusExtensionDataManager { isLocal: sensorInfo.isLocal ) } + + if let pumpManagerHUDProvider = dataManager.pumpManagerHUDProvider { + context.pumpManagerHUDViewsContext = PumpManagerHUDViewsContext(pumpManagerHUDViewsRawValue: PumpManagerHUDViewsRawValueFromHudProvider(pumpManagerHUDProvider)) + } completionHandler(context) } diff --git a/Loop/View Controllers/BolusViewController.swift b/Loop/View Controllers/BolusViewController.swift index 793bf5b747..3a58b7f3be 100644 --- a/Loop/View Controllers/BolusViewController.swift +++ b/Loop/View Controllers/BolusViewController.swift @@ -182,7 +182,11 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex } guard bolus <= maxBolus else { - presentAlertController(withTitle: NSLocalizedString("Exceeds Maximum Bolus", comment: "The title of the alert describing a maximum bolus validation error"), message: String(format: NSLocalizedString("The maximum bolus amount is %@ Units", comment: "Body of the alert describing a maximum bolus validation error. (1: The localized max bolus value)"), bolusUnitsFormatter.string(from: maxBolus) ?? "")) + let alert = UIAlertController( + title: NSLocalizedString("Exceeds Maximum Bolus", comment: "The title of the alert describing a maximum bolus validation error"), + message: String(format: NSLocalizedString("The maximum bolus amount is %@ Units", comment: "Body of the alert describing a maximum bolus validation error. (1: The localized max bolus value)"), bolusUnitsFormatter.string(from: maxBolus) ?? ""), + preferredStyle: .alert) + present(alert, animated: true) return } diff --git a/Loop/View Controllers/CarbAbsorptionViewController.swift b/Loop/View Controllers/CarbAbsorptionViewController.swift index b6398ab177..afab2f835f 100644 --- a/Loop/View Controllers/CarbAbsorptionViewController.swift +++ b/Loop/View Controllers/CarbAbsorptionViewController.swift @@ -436,7 +436,7 @@ final class CarbAbsorptionViewController: ChartsTableViewController, Identifiabl break // Notification will trigger update case .failure(let error): self.refreshContext.update(with: .carbs) - self.presentAlertController(with: error) + self.present(UIAlertController(with: error), animated: true) } } } @@ -535,7 +535,7 @@ final class CarbAbsorptionViewController: ChartsTableViewController, Identifiabl case .failure(let error): // Ignore bolus wizard errors if error is CarbStore.CarbStoreError { - self.presentAlertController(with: error) + self.present(UIAlertController(with: error), animated: true) } } } diff --git a/Loop/View Controllers/ChartsTableViewController.swift b/Loop/View Controllers/ChartsTableViewController.swift index 3e519b2b0b..3642a25628 100644 --- a/Loop/View Controllers/ChartsTableViewController.swift +++ b/Loop/View Controllers/ChartsTableViewController.swift @@ -94,6 +94,8 @@ class ChartsTableViewController: UITableViewController, UIGestureRecognizerDeleg } ] + active = UIApplication.shared.applicationState == .active + let gestureRecognizer = UILongPressGestureRecognizer() gestureRecognizer.delegate = self gestureRecognizer.minimumPressDuration = 0.1 @@ -166,11 +168,8 @@ class ChartsTableViewController: UITableViewController, UIGestureRecognizerDeleg // References to registered notification center observers var notificationObservers: [Any] = [] - var active: Bool { - get { - return UIApplication.shared.applicationState == .active - } - set { + var active: Bool = false { + didSet { log.debug("[reloadData] for app change to active: %d", active) reloadData() } diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index 48b76fca8d..d60733f335 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -109,6 +109,16 @@ final class SettingsTableViewController: UITableViewController { } } + func configuredSetupViewController(for pumpManager: PumpManagerUI.Type) -> (UIViewController & PumpManagerSetupViewController & CompletionNotifying) { + var setupViewController = pumpManager.setupViewController() + setupViewController.setupDelegate = self + setupViewController.completionDelegate = self + setupViewController.basalSchedule = dataManager.loopManager.basalRateSchedule + setupViewController.maxBolusUnits = dataManager.loopManager.settings.maximumBolus + setupViewController.maxBasalRateUnitsPerHour = dataManager.loopManager.settings.maximumBasalRatePerHour + return setupViewController + } + // MARK: - UITableViewDataSource override func numberOfSections(in tableView: UITableView) -> Int { @@ -122,7 +132,7 @@ final class SettingsTableViewController: UITableViewController { case .pump: return PumpRow.count case .cgm: - return 1 + return CGMRow.count case .configuration: return ConfigurationRow.count case .services: @@ -160,7 +170,6 @@ final class SettingsTableViewController: UITableViewController { cell.imageView?.image = pumpManager.smallImage cell.textLabel?.text = pumpManager.localizedTitle cell.detailTextLabel?.text = nil - cell.accessoryType = .disclosureIndicator return cell } else { let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) @@ -179,9 +188,6 @@ final class SettingsTableViewController: UITableViewController { } cell.textLabel?.text = cgmManager.localizedTitle cell.detailTextLabel?.text = nil - if cgmManagerUI != nil { - cell.accessoryType = .disclosureIndicator - } return cell } else { let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) @@ -319,8 +325,9 @@ final class SettingsTableViewController: UITableViewController { case .pump: switch PumpRow(rawValue: indexPath.row)! { case .pumpSettings: - if let settings = dataManager.pumpManager?.settingsViewController() { - show(settings, sender: sender) + if var settings = dataManager.pumpManager?.settingsViewController() { + settings.completionDelegate = self + present(settings, animated: true) } else { // Add new pump let pumpManagers = allPumpManagers.compactMap({ $0 as? PumpManagerUI.Type }) @@ -328,15 +335,15 @@ final class SettingsTableViewController: UITableViewController { switch pumpManagers.count { case 1: if let PumpManagerType = pumpManagers.first { - var setupViewController = PumpManagerType.setupViewController() - setupViewController.setupDelegate = self + let setupViewController = configuredSetupViewController(for: PumpManagerType) present(setupViewController, animated: true, completion: nil) } case let x where x > 1: let alert = UIAlertController(pumpManagers: pumpManagers) { [weak self] (manager) in - var setupViewController = manager.setupViewController() - setupViewController.setupDelegate = self - self?.present(setupViewController, animated: true, completion: nil) + if let self = self { + let setupViewController = self.configuredSetupViewController(for: manager) + self.present(setupViewController, animated: true, completion: nil) + } } alert.addCancelAction { (_) in @@ -352,7 +359,9 @@ final class SettingsTableViewController: UITableViewController { case .cgm: if let cgmManager = dataManager.cgmManager as? CGMManagerUI { if let unit = dataManager.loopManager.glucoseStore.preferredUnit { - show(cgmManager.settingsViewController(for: unit), sender: sender) + var settings = cgmManager.settingsViewController(for: unit) + settings.completionDelegate = self + present(settings, animated: true) } } else if dataManager.cgmManager is PumpManagerUI { // The pump manager is providing glucose, but allow reverting the CGM @@ -406,7 +415,7 @@ final class SettingsTableViewController: UITableViewController { scheduleVC.timeZone = schedule.timeZone scheduleVC.scheduleItems = schedule.items scheduleVC.unit = schedule.unit - } else if let timeZone = dataManager.pumpManager?.pumpTimeZone { + } else if let timeZone = dataManager.pumpManager?.status.timeZone { scheduleVC.timeZone = timeZone } @@ -424,7 +433,7 @@ final class SettingsTableViewController: UITableViewController { show(scheduleVC, sender: sender) } else { - if let timeZone = dataManager.pumpManager?.pumpTimeZone { + if let timeZone = dataManager.pumpManager?.status.timeZone { scheduleVC.timeZone = timeZone } @@ -447,7 +456,7 @@ final class SettingsTableViewController: UITableViewController { show(scheduleVC, sender: sender) } else { - if let timeZone = dataManager.pumpManager?.pumpTimeZone { + if let timeZone = dataManager.pumpManager?.status.timeZone { scheduleVC.timeZone = timeZone } @@ -489,7 +498,7 @@ final class SettingsTableViewController: UITableViewController { if let profile = dataManager.loopManager.basalRateSchedule { vc.scheduleItems = profile.items vc.timeZone = profile.timeZone - } else if let timeZone = dataManager.pumpManager?.pumpTimeZone { + } else if let timeZone = dataManager.pumpManager?.status.timeZone { vc.timeZone = timeZone } @@ -550,7 +559,7 @@ final class SettingsTableViewController: UITableViewController { case .loop: break case .pump: - tableView.reloadRows(at: [indexPath], with: .fade) + tableView.reloadSections([Section.pump.rawValue], with: .fade) tableView.reloadRows(at: [[Section.cgm.rawValue, CGMRow.cgmSettings.rawValue]], with: .fade) case .cgm: tableView.reloadRows(at: [indexPath], with: .fade) @@ -568,6 +577,14 @@ final class SettingsTableViewController: UITableViewController { } } +extension SettingsTableViewController: CompletionDelegate { + func completionNotifyingDidComplete(_ object: CompletionNotifying) { + if let vc = object as? UIViewController { + vc.dismiss(animated: true, completion: nil) + } + } +} + extension SettingsTableViewController: PumpManagerSetupViewControllerDelegate { func pumpManagerSetupViewController(_ pumpManagerSetupViewController: PumpManagerSetupViewController, didSetUpPumpManager pumpManager: PumpManagerUI) { @@ -588,13 +605,6 @@ extension SettingsTableViewController: PumpManagerSetupViewControllerDelegate { dataManager.loopManager.settings.maximumBolus = maxBolusUnits tableView.reloadRows(at: [[Section.configuration.rawValue, ConfigurationRow.deliveryLimits.rawValue]], with: .none) } - - show(pumpManager.settingsViewController(), sender: nil) - dismiss(animated: true, completion: nil) - } - - func pumpManagerSetupViewControllerDidCancel(_ pumpManagerSetupViewController: PumpManagerSetupViewController) { - dismiss(animated: true, completion: nil) } } @@ -603,6 +613,7 @@ extension SettingsTableViewController: CGMManagerSetupViewControllerDelegate { fileprivate func setupCGMManager(_ CGMManagerType: CGMManagerUI.Type, indexPath: IndexPath) { if var setupViewController = CGMManagerType.setupViewController() { setupViewController.setupDelegate = self + setupViewController.completionDelegate = self present(setupViewController, animated: true, completion: nil) } else { completeCGMManagerSetup(CGMManagerType.init(rawState: [:]), indexPath: indexPath) @@ -618,11 +629,6 @@ extension SettingsTableViewController: CGMManagerSetupViewControllerDelegate { func cgmManagerSetupViewController(_ cgmManagerSetupViewController: CGMManagerSetupViewController, didSetUpCGMManager cgmManager: CGMManagerUI) { dataManager.cgmManager = cgmManager tableView.selectRow(at: IndexPath(row: CGMRow.cgmSettings.rawValue, section: Section.cgm.rawValue), animated: false, scrollPosition: .none) - show(cgmManager.settingsViewController(for: dataManager.loopManager.glucoseStore.preferredUnit ?? .milligramsPerDeciliter), sender: nil) - dismiss(animated: true, completion: nil) - } - - func cgmManagerSetupViewControllerDidCancel(_ cgmManagerSetupViewController: CGMManagerSetupViewController) { dismiss(animated: true, completion: nil) } } diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index e6730f4cd9..ed29d842c5 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -25,7 +25,6 @@ private enum BolusState { case enacting } - private extension RefreshContext { static let all: Set = [.status, .glucose, .insulin, .carbs, .targets] } @@ -42,6 +41,11 @@ final class StatusTableViewController: ChartsTableViewController { min: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 100), max: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 175) ) + + if let pumpManager = deviceManager.pumpManager { + self.basalDeliveryState = pumpManager.status.basalDeliveryState + pumpManager.addStatusObserver(self) + } let notificationCenter = NotificationCenter.default @@ -72,7 +76,13 @@ final class StatusTableViewController: ChartsTableViewController { DispatchQueue.main.async { self?.hudView?.loopCompletionHUD.loopInProgress = true } + }, + notificationCenter.addObserver(forName: .PumpManagerChanged, object: deviceManager, queue: nil) { [weak self] (notification: Notification) in + DispatchQueue.main.async { + self?.configurePumpManagerHUDViews() + } } + ] if let gestureRecognizer = charts.gestureRecognizer { @@ -125,12 +135,16 @@ final class StatusTableViewController: ChartsTableViewController { } } + onscreen = true + AnalyticsManager.shared.didDisplayStatusScreen() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + onscreen = false + if presentedViewController == nil { navigationController?.setNavigationBarHidden(false, animated: animated) } @@ -145,12 +159,17 @@ final class StatusTableViewController: ChartsTableViewController { // MARK: - State override var active: Bool { - get { - return super.active - } - set { - super.active = newValue + didSet { hudView?.loopCompletionHUD.assertTimer(active) + updateHUDActive() + } + } + + // This is similar to the visible property, but is set later, on viewDidAppear, to be + // suitable for animations that should be seen in their entirety. + var onscreen: Bool = false { + didSet { + updateHUDActive() } } @@ -167,6 +186,16 @@ final class StatusTableViewController: ChartsTableViewController { } } + private func updateHUDActive() { + deviceManager.pumpManagerHUDProvider?.visible = active && onscreen + } + + public var basalDeliveryState: PumpManagerStatus.BasalDeliveryState = .active { + didSet { + refreshContext.update(with: .status) + } + } + // Toggles the display mode based on the screen aspect ratio. Should not be updated outside of reloadData(). private var landscapeMode = false @@ -231,7 +260,6 @@ final class StatusTableViewController: ChartsTableViewController { reloading = true let reloadGroup = DispatchGroup() - var lastReservoirValue: ReservoirValue? var newRecommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? var glucoseValues: [StoredGlucoseSample]? var predictedGlucoseValues: [GlucoseValue]? @@ -344,18 +372,6 @@ final class StatusTableViewController: ChartsTableViewController { reloadGroup.leave() } - - reloadGroup.enter() - deviceManager.loopManager.doseStore.getReservoirValues(since: Date(timeIntervalSinceNow: .minutes(-30)), limit: 1) { (result) in - switch result { - case .success(let values): - lastReservoirValue = values.first - case .failure: - retryContext.update(with: .insulin) - } - - reloadGroup.leave() - } } workoutMode = deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.overrideEnabledForContext(.workout) @@ -418,31 +434,25 @@ final class StatusTableViewController: ChartsTableViewController { sensor: self.deviceManager.cgmManager?.sensorState ) } - - // Reservoir HUD - if let reservoir = lastReservoirValue { - if let capacity = self.deviceManager.pumpManager?.pumpReservoirCapacity { - hudView.reservoirVolumeHUD.reservoirLevel = min(1, max(0, reservoir.unitVolume / capacity)) - } - - hudView.reservoirVolumeHUD.setReservoirVolume(volume: reservoir.unitVolume, at: reservoir.startDate) - } - - // Battery HUD - hudView.batteryHUD.batteryLevel = self.deviceManager.pumpManager?.pumpBatteryChargeRemaining ?? UserDefaults.appGroup.statusExtensionContext?.batteryPercentage } // Show/hide the table view rows let statusRowMode: StatusRowMode? - - switch bolusState { - case .recommended?, .enacting?: - statusRowMode = nil - case .none: - if let (recommendation: tempBasal, date: date) = newRecommendedTempBasal { - statusRowMode = .recommendedTempBasal(tempBasal: tempBasal, at: date, enacting: false) - } else { - statusRowMode = .hidden + + if self.basalDeliveryState == .suspended { + statusRowMode = .pumpSuspended(resuming: false) + } else if self.basalDeliveryState == .resuming { + statusRowMode = .pumpSuspended(resuming: true) + } else { + switch bolusState { + case .recommended?, .enacting?: + statusRowMode = nil + case .none: + if let (recommendation: tempBasal, date: date) = newRecommendedTempBasal { + statusRowMode = .recommendedTempBasal(tempBasal: tempBasal, at: date, enacting: false) + } else { + statusRowMode = .hidden + } } } @@ -510,6 +520,7 @@ final class StatusTableViewController: ChartsTableViewController { case hidden case recommendedTempBasal(tempBasal: TempBasalRecommendation, at: Date, enacting: Bool) case enactingBolus + case pumpSuspended(resuming: Bool) var hasRow: Bool { switch self { @@ -577,6 +588,14 @@ final class StatusTableViewController: ChartsTableViewController { } case (.enactingBolus, .enactingBolus): break + case (.pumpSuspended(resuming: let wasResuming), .pumpSuspended(resuming: let isResuming)): + if isResuming, !wasResuming, let cell = tableView.cellForRow(at: statusIndexPath) as? TitleSubtitleTableViewCell { + let indicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray) + indicatorView.startAnimating() + cell.accessoryView = indicatorView + cell.subtitleLabel.text = nil + } + break default: self.tableView.reloadRows(at: [statusIndexPath], with: animated ? .fade : .none) } @@ -727,6 +746,10 @@ final class StatusTableViewController: ChartsTableViewController { let indicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray) indicatorView.startAnimating() cell.accessoryView = indicatorView + case .pumpSuspended: + cell.titleLabel.text = NSLocalizedString("Pump Suspended", comment: "The title of the cell indicating the pump is suspended") + cell.subtitleLabel.text = NSLocalizedString("Tap to Resume", comment: "The subtitle of the cell displaying an action to resume insulin delivery") + cell.selectionStyle = .default } } @@ -816,7 +839,8 @@ final class StatusTableViewController: ChartsTableViewController { case .status: tableView.deselectRow(at: indexPath, animated: true) - if case .recommendedTempBasal(tempBasal: let tempBasal, at: let date, enacting: let enacting) = statusRowMode, !enacting { + switch statusRowMode { + case .recommendedTempBasal(tempBasal: let tempBasal, at: let date, enacting: let enacting) where !enacting: self.updateHUDandStatusRows(statusRowMode: .recommendedTempBasal(tempBasal: tempBasal, at: date, enacting: true), newSize: nil, animated: true) self.deviceManager.loopManager.enactRecommendedTempBasal { (error) in @@ -825,7 +849,7 @@ final class StatusTableViewController: ChartsTableViewController { if let error = error { self.deviceManager.logger.addError(error, fromSource: "TempBasal") - self.presentAlertController(with: error) + self.present(UIAlertController(with: error), animated: true) } else { self.refreshContext.update(with: .status) self.log.debug("[reloadData] after manually enacting temp basal") @@ -833,6 +857,18 @@ final class StatusTableViewController: ChartsTableViewController { } } } + case .pumpSuspended(let resuming) where !resuming: + self.updateHUDandStatusRows(statusRowMode: .pumpSuspended(resuming: true) , newSize: nil, animated: true) + self.deviceManager.pumpManager?.resumeDelivery() { (error) in + DispatchQueue.main.async { + if let error = error { + let alert = UIAlertController(with: error, title: NSLocalizedString("Error Resuming", comment: "The alert title for a resume error")) + self.present(alert, animated: true, completion: nil) + } + } + } + default: + break } } case .hud: @@ -918,7 +954,7 @@ final class StatusTableViewController: ChartsTableViewController { case .failure(let error): // Ignore bolus wizard errors if error is CarbStore.CarbStoreError { - self.presentAlertController(with: error) + self.present(UIAlertController(with: error), animated: true) } else { self.deviceManager.logger.addError(error, fromSource: "Bolus") } @@ -1015,28 +1051,57 @@ final class StatusTableViewController: ChartsTableViewController { if deviceManager.cgmManager?.appURL != nil { hudView.glucoseHUD.accessibilityHint = NSLocalizedString("Launches CGM app", comment: "Glucose HUD accessibility hint") } - + + configurePumpManagerHUDViews() + hudView.loopCompletionHUD.stateColors = .loopStatus hudView.glucoseHUD.stateColors = .cgmStatus hudView.glucoseHUD.tintColor = .glucoseTintColor hudView.basalRateHUD.tintColor = .doseTintColor - hudView.reservoirVolumeHUD.stateColors = .pumpStatus - hudView.batteryHUD.stateColors = .pumpStatus refreshContext.update(with: .status) self.log.debug("[reloadData] after hudView loaded") reloadData() } } + + private func configurePumpManagerHUDViews() { + if let hudView = hudView { + hudView.removePumpManagerProvidedViews() + if var pumpManagerHUDProvider = deviceManager.pumpManagerHUDProvider + { + let views = pumpManagerHUDProvider.createHUDViews() + for view in views { + addViewToHUD(view) + } + pumpManagerHUDProvider.visible = active && onscreen + } else { + let reservoirView = ReservoirVolumeHUDView.instantiate() + let batteryView = BatteryLevelHUDView.instantiate() + for view in [reservoirView, batteryView] { + addViewToHUD(view) + } + } + } + } + + private func addViewToHUD(_ view: BaseHUDView) { + if let hudView = hudView { + let hudTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(hudViewTapped(_:))) + view.addGestureRecognizer(hudTapGestureRecognizer) + view.stateColors = .pumpStatus + hudView.addHUDView(view) + } + } @objc private func showLastError(_: Any) { // First, check whether we have a device error after the most recent completion date if let deviceError = deviceManager.lastError, deviceError.date > (hudView?.loopCompletionHUD.lastLoopCompleted ?? .distantPast) { - self.presentAlertController(with: deviceError.error) + self.present(UIAlertController(with: deviceError.error), animated: true) } else if let lastLoopError = lastLoopError { - self.presentAlertController(with: lastLoopError) + self.present(UIAlertController(with: lastLoopError), animated: true) } } @@ -1045,4 +1110,37 @@ final class StatusTableViewController: ChartsTableViewController { UIApplication.shared.open(url) } } + + @objc private func hudViewTapped(_ sender: UIGestureRecognizer) { + if let hudSubView = sender.view as? BaseHUDView, + let pumpManagerHUDProvider = deviceManager.pumpManagerHUDProvider, + let action = pumpManagerHUDProvider.didTapOnHUDView(hudSubView) + { + switch action { + case .presentViewController(let vc): + var completionNotifyingVC = vc + completionNotifyingVC.completionDelegate = self + self.present(vc, animated: true, completion: nil) + case .openAppURL(let url): + UIApplication.shared.open(url) + } + } + } +} + +extension StatusTableViewController: CompletionDelegate { + func completionNotifyingDidComplete(_ object: CompletionNotifying) { + if let vc = object as? UIViewController { + vc.dismiss(animated: true, completion: nil) + } + } +} + +extension StatusTableViewController: PumpManagerStatusObserver { + func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus) { + DispatchQueue.main.async { + self.basalDeliveryState = status.basalDeliveryState + self.reloadData(animated: true) + } + } } diff --git a/Loop/Views/SettingsImageTableViewCell.swift b/Loop/Views/SettingsImageTableViewCell.swift index e99ea9a968..1b997ae9fb 100644 --- a/Loop/Views/SettingsImageTableViewCell.swift +++ b/Loop/Views/SettingsImageTableViewCell.swift @@ -25,7 +25,7 @@ class SettingsImageTableViewCell: UITableViewCell { guard let textLabel = textLabel, let imageView = imageView else { return } - + textLabel.adjustsFontForContentSizeCategory = true textLabel.font = UIFont.preferredFont(forTextStyle: .body) textLabel.translatesAutoresizingMaskIntoConstraints = false diff --git a/LoopUI/Assets.xcassets/Contents.json b/LoopUI/HUDAssets.xcassets/Contents.json similarity index 100% rename from LoopUI/Assets.xcassets/Contents.json rename to LoopUI/HUDAssets.xcassets/Contents.json diff --git a/LoopUI/Assets.xcassets/battery/Contents.json b/LoopUI/HUDAssets.xcassets/battery/Contents.json similarity index 100% rename from LoopUI/Assets.xcassets/battery/Contents.json rename to LoopUI/HUDAssets.xcassets/battery/Contents.json diff --git a/LoopUI/Assets.xcassets/battery/battery.imageset/Contents.json b/LoopUI/HUDAssets.xcassets/battery/battery.imageset/Contents.json similarity index 100% rename from LoopUI/Assets.xcassets/battery/battery.imageset/Contents.json rename to LoopUI/HUDAssets.xcassets/battery/battery.imageset/Contents.json diff --git a/LoopUI/Assets.xcassets/battery/battery.imageset/battery.pdf b/LoopUI/HUDAssets.xcassets/battery/battery.imageset/battery.pdf similarity index 100% rename from LoopUI/Assets.xcassets/battery/battery.imageset/battery.pdf rename to LoopUI/HUDAssets.xcassets/battery/battery.imageset/battery.pdf diff --git a/LoopUI/Assets.xcassets/battery/battery_mask.imageset/Contents.json b/LoopUI/HUDAssets.xcassets/battery/battery_mask.imageset/Contents.json similarity index 100% rename from LoopUI/Assets.xcassets/battery/battery_mask.imageset/Contents.json rename to LoopUI/HUDAssets.xcassets/battery/battery_mask.imageset/Contents.json diff --git a/LoopUI/Assets.xcassets/battery/battery_mask.imageset/battery_mask.pdf b/LoopUI/HUDAssets.xcassets/battery/battery_mask.imageset/battery_mask.pdf similarity index 100% rename from LoopUI/Assets.xcassets/battery/battery_mask.imageset/battery_mask.pdf rename to LoopUI/HUDAssets.xcassets/battery/battery_mask.imageset/battery_mask.pdf diff --git a/LoopUI/Assets.xcassets/reservoir/Contents.json b/LoopUI/HUDAssets.xcassets/reservoir/Contents.json similarity index 100% rename from LoopUI/Assets.xcassets/reservoir/Contents.json rename to LoopUI/HUDAssets.xcassets/reservoir/Contents.json diff --git a/LoopUI/Assets.xcassets/reservoir/reservoir.imageset/Contents.json b/LoopUI/HUDAssets.xcassets/reservoir/reservoir.imageset/Contents.json similarity index 100% rename from LoopUI/Assets.xcassets/reservoir/reservoir.imageset/Contents.json rename to LoopUI/HUDAssets.xcassets/reservoir/reservoir.imageset/Contents.json diff --git a/LoopUI/Assets.xcassets/reservoir/reservoir.imageset/reservoir.pdf b/LoopUI/HUDAssets.xcassets/reservoir/reservoir.imageset/reservoir.pdf similarity index 100% rename from LoopUI/Assets.xcassets/reservoir/reservoir.imageset/reservoir.pdf rename to LoopUI/HUDAssets.xcassets/reservoir/reservoir.imageset/reservoir.pdf diff --git a/LoopUI/Assets.xcassets/reservoir/reservoir_mask.imageset/Contents.json b/LoopUI/HUDAssets.xcassets/reservoir/reservoir_mask.imageset/Contents.json similarity index 100% rename from LoopUI/Assets.xcassets/reservoir/reservoir_mask.imageset/Contents.json rename to LoopUI/HUDAssets.xcassets/reservoir/reservoir_mask.imageset/Contents.json diff --git a/LoopUI/Assets.xcassets/reservoir/reservoir_mask.imageset/reservoir_mask.pdf b/LoopUI/HUDAssets.xcassets/reservoir/reservoir_mask.imageset/reservoir_mask.pdf similarity index 100% rename from LoopUI/Assets.xcassets/reservoir/reservoir_mask.imageset/reservoir_mask.pdf rename to LoopUI/HUDAssets.xcassets/reservoir/reservoir_mask.imageset/reservoir_mask.pdf diff --git a/LoopUI/HUDView.xib b/LoopUI/HUDView.xib index 919a91a921..ee27795580 100644 --- a/LoopUI/HUDView.xib +++ b/LoopUI/HUDView.xib @@ -1,22 +1,19 @@ - + - - + - - @@ -24,16 +21,16 @@ - + - + @@ -64,28 +61,28 @@ - + - + - + @@ -163,130 +160,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LoopUI/Models/StateColorPalette.swift b/LoopUI/Models/StateColorPalette.swift deleted file mode 100644 index 20052f10b2..0000000000 --- a/LoopUI/Models/StateColorPalette.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// StateColorPalette.swift -// Loop -// -// Copyright © 2017 LoopKit Authors. All rights reserved. -// - -import UIKit - - -/// A collection of colors for displaying state -public struct StateColorPalette { - public let unknown: UIColor - public let normal: UIColor - public let warning: UIColor - public let error: UIColor - - public init(unknown: UIColor, normal: UIColor, warning: UIColor, error: UIColor) { - self.unknown = unknown - self.normal = normal - self.warning = warning - self.error = error - } -} diff --git a/LoopUI/Views/BasalRateHUDView.swift b/LoopUI/Views/BasalRateHUDView.swift index 0c44579a5e..0a4283d9cd 100644 --- a/LoopUI/Views/BasalRateHUDView.swift +++ b/LoopUI/Views/BasalRateHUDView.swift @@ -7,9 +7,13 @@ // import UIKit - +import LoopKitUI public final class BasalRateHUDView: BaseHUDView { + + override public var orderPriority: HUDViewOrderPriority { + return 3 + } @IBOutlet private weak var basalStateView: BasalStateView! diff --git a/LoopUI/Views/BaseHUDView.swift b/LoopUI/Views/BaseHUDView.swift deleted file mode 100644 index a0d2ad853e..0000000000 --- a/LoopUI/Views/BaseHUDView.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// HUDView.swift -// Naterade -// -// Created by Nathan Racklyeft on 5/1/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import UIKit - -public class BaseHUDView: UIView { - - @IBOutlet weak var caption: UILabel! { - didSet { - caption?.text = "—" - } - } - -} diff --git a/LoopUI/Views/BatteryLevelHUDView.swift b/LoopUI/Views/BatteryLevelHUDView.swift deleted file mode 100644 index e200c54530..0000000000 --- a/LoopUI/Views/BatteryLevelHUDView.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// BatteryLevelHUDView.swift -// Naterade -// -// Created by Nathan Racklyeft on 5/2/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import UIKit - - -public final class BatteryLevelHUDView: LevelHUDView { - - private lazy var numberFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .percent - - return formatter - }() - - public var batteryLevel: Double? { - didSet { - if let value = batteryLevel, let level = numberFormatter.string(from: value) { - caption.text = level - accessibilityValue = level - } else { - caption.text = nil - } - - level = batteryLevel - } - } - -} diff --git a/LoopUI/Views/GlucoseHUDView.swift b/LoopUI/Views/GlucoseHUDView.swift index 9a7acf26c9..802ee17de0 100644 --- a/LoopUI/Views/GlucoseHUDView.swift +++ b/LoopUI/Views/GlucoseHUDView.swift @@ -9,9 +9,13 @@ import UIKit import HealthKit import LoopKit - +import LoopKitUI public final class GlucoseHUDView: BaseHUDView { + + override public var orderPriority: HUDViewOrderPriority { + return 2 + } @IBOutlet private weak var unitLabel: UILabel! { didSet { @@ -43,10 +47,9 @@ public final class GlucoseHUDView: BaseHUDView { glucoseLabel.textColor = tintColor } - public var stateColors: StateColorPalette? { - didSet { - updateColor() - } + override public func stateColorsDidUpdate() { + super.stateColorsDidUpdate() + updateColor() } private func updateColor() { diff --git a/LoopUI/Views/HUDView.swift b/LoopUI/Views/HUDView.swift index af89b39068..3dc832c4a8 100644 --- a/LoopUI/Views/HUDView.swift +++ b/LoopUI/Views/HUDView.swift @@ -7,16 +7,17 @@ // import UIKit +import LoopKitUI public class HUDView: UIView, NibLoadable { @IBOutlet public weak var loopCompletionHUD: LoopCompletionHUDView! @IBOutlet public weak var glucoseHUD: GlucoseHUDView! @IBOutlet public weak var basalRateHUD: BasalRateHUDView! - @IBOutlet public weak var reservoirVolumeHUD: ReservoirVolumeHUDView! - @IBOutlet public weak var batteryHUD: BatteryLevelHUDView! + + private var stackView: UIStackView! func setup() { - let stackView = HUDView.nib().instantiate(withOwner: self, options: nil)[0] as! UIStackView + stackView = (HUDView.nib().instantiate(withOwner: self, options: nil)[0] as! UIStackView) self.addSubview(stackView) // Use AutoLayout to have the stack view fill its entire container. @@ -26,6 +27,25 @@ public class HUDView: UIView, NibLoadable { let heightConstraint = NSLayoutConstraint(item: stackView, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 1, constant: 0) self.addConstraints([horizontalConstraint, verticalConstraint, widthConstraint, heightConstraint]) } + + public func removePumpManagerProvidedViews() { + let standardViews: [UIView] = [loopCompletionHUD, glucoseHUD, basalRateHUD] + let pumpManagerViews = stackView.subviews.filter { !standardViews.contains($0) } + for view in pumpManagerViews { + view.removeFromSuperview() + } + } + + public func addHUDView(_ viewToAdd: BaseHUDView) { + let insertIndex = stackView.arrangedSubviews.firstIndex { (view) -> Bool in + guard let hudView = view as? BaseHUDView else { + return false + } + return viewToAdd.orderPriority <= hudView.orderPriority + } + + stackView.insertArrangedSubview(viewToAdd, at: insertIndex ?? stackView.arrangedSubviews.count) + } public override init(frame: CGRect) { super.init(frame: frame) diff --git a/LoopUI/Views/LevelHUDView.swift b/LoopUI/Views/LevelHUDView.swift deleted file mode 100644 index aec0371fd8..0000000000 --- a/LoopUI/Views/LevelHUDView.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// LevelHUDView.swift -// Loop -// -// Created by Nate Racklyeft on 2/4/17. -// Copyright © 2017 LoopKit Authors. All rights reserved. -// - -import UIKit - -public class LevelHUDView: BaseHUDView { - - @IBOutlet private weak var levelMaskView: LevelMaskView! - - override public func awakeFromNib() { - super.awakeFromNib() - - updateColor() - - accessibilityValue = LocalizedString("Unknown", comment: "Accessibility value for an unknown value") - } - - public var stateColors: StateColorPalette? { - didSet { - updateColor() - } - } - - private func updateColor() { - levelMaskView.tintColor = nil - - switch level { - case .none: - tintColor = stateColors?.unknown - case let x? where x > 0.25: - tintColor = stateColors?.normal - case let x? where x > 0.10: - tintColor = stateColors?.normal - levelMaskView.tintColor = stateColors?.warning - default: - tintColor = stateColors?.error - } - } - - internal var level: Double? { - didSet { - levelMaskView.value = level ?? 1.0 - updateColor() - } - } - -} diff --git a/LoopUI/Views/LevelMaskView.swift b/LoopUI/Views/LevelMaskView.swift deleted file mode 100644 index ec7840fd71..0000000000 --- a/LoopUI/Views/LevelMaskView.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// LevelMaskView.swift -// Loop -// -// Created by Nate Racklyeft on 8/28/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import UIKit - -// Displays a variable-height level indicator, masked by an image. -// Inspired by https://github.com/carekit-apple/CareKit/blob/master/CareKit/CareCard/OCKHeartView.h - -public class LevelMaskView: UIView { - var firstDataUpdate = true - - var value: Double = 1.0 { - didSet { - animateFill(duration: firstDataUpdate ? 0 : 1.25) - firstDataUpdate = false - } - } - - @IBInspectable var maskImage: UIImage? { - didSet { - fillView?.removeFromSuperview() - mask?.removeFromSuperview() - maskImageView?.removeFromSuperview() - - guard let maskImage = maskImage else { return } - - mask = UIView() - maskImageView = UIImageView(image: maskImage) - maskImageView!.contentMode = .center - mask!.addSubview(maskImageView!) - - clipsToBounds = true - - fillView = UIView() - fillView!.backgroundColor = tintColor - addSubview(fillView!) - } - } - - private var fillView: UIView? - - private var maskImageView: UIView? - - override public func layoutSubviews() { - super.layoutSubviews() - - guard let maskImage = maskImage else { return } - - let maskImageSize = maskImage.size - - mask?.frame = CGRect(origin: .zero, size: maskImageSize) - mask?.center = CGPoint(x: bounds.midX, y: bounds.midY) - maskImageView?.frame = mask?.bounds ?? bounds - - if (fillView?.layer.animationKeys()?.count ?? 0) == 0 { - updateFillViewFrame() - } - } - - override public func tintColorDidChange() { - super.tintColorDidChange() - - fillView?.backgroundColor = tintColor - } - - private func animateFill(duration: TimeInterval) { - UIView.animate(withDuration: duration, delay: 0, options: .beginFromCurrentState, animations: { - self.updateFillViewFrame() - }, completion: nil) - } - - private func updateFillViewFrame() { - guard let maskViewFrame = mask?.frame else { return } - - var fillViewFrame = maskViewFrame - fillViewFrame.origin.y = maskViewFrame.maxY - fillViewFrame.size.height = -CGFloat(value) * maskViewFrame.height - fillView?.frame = fillViewFrame - } -} diff --git a/LoopUI/Views/LoopCompletionHUDView.swift b/LoopUI/Views/LoopCompletionHUDView.swift index 077f699998..3fbdd1aff4 100644 --- a/LoopUI/Views/LoopCompletionHUDView.swift +++ b/LoopUI/Views/LoopCompletionHUDView.swift @@ -7,10 +7,15 @@ // import UIKit +import LoopKitUI public final class LoopCompletionHUDView: BaseHUDView { @IBOutlet private weak var loopStateView: LoopStateView! + + override public var orderPriority: HUDViewOrderPriority { + return 1 + } enum Freshness { case fresh @@ -64,10 +69,9 @@ public final class LoopCompletionHUDView: BaseHUDView { } } - public var stateColors: StateColorPalette? { - didSet { - updateTintColor() - } + override public func stateColorsDidUpdate() { + super.stateColorsDidUpdate() + updateTintColor() } private func updateTintColor() { diff --git a/LoopUI/Views/ReservoirVolumeHUDView.swift b/LoopUI/Views/ReservoirVolumeHUDView.swift deleted file mode 100644 index 6f57fe09d1..0000000000 --- a/LoopUI/Views/ReservoirVolumeHUDView.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// ReservoirVolumeHUDView.swift -// Naterade -// -// Created by Nathan Racklyeft on 5/2/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import UIKit - -public final class ReservoirVolumeHUDView: LevelHUDView { - - @IBOutlet private weak var volumeLabel: UILabel! - - override public func awakeFromNib() { - super.awakeFromNib() - - volumeLabel.isHidden = true - } - - public var reservoirLevel: Double? { - didSet { - level = reservoirLevel - - switch reservoirLevel { - case .none: - volumeLabel.isHidden = true - case let x? where x > 0.25: - volumeLabel.isHidden = true - case let x? where x > 0.10: - volumeLabel.textColor = tintColor - volumeLabel.isHidden = false - default: - volumeLabel.textColor = tintColor - volumeLabel.isHidden = false - } - } - } - - private lazy var timeFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .none - formatter.timeStyle = .short - - return formatter - }() - - private lazy var numberFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.maximumFractionDigits = 0 - - return formatter - }() - - public func setReservoirVolume(volume: Double, at date: Date) { - if let units = numberFormatter.string(from: volume) { - volumeLabel.text = String(format: LocalizedString("%@U", comment: "Format string for reservoir volume. (1: The localized volume)"), units) - let time = timeFormatter.string(from: date) - caption?.text = time - - accessibilityValue = String(format: LocalizedString("%1$@ units remaining at %2$@", comment: "Accessibility format string for (1: localized volume)(2: time)"), units, time) - } - } -}