From cd0231c5fea1fade4ed13a4f683494f04527f2bf Mon Sep 17 00:00:00 2001 From: Trpl7ca <52380252+Trpl7ca@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:14:04 -0800 Subject: [PATCH] Delete patches/combined_nid_and_la_V0101m.patch --- patches/combined_nid_and_la_V0101m.patch | 3104 ---------------------- 1 file changed, 3104 deletions(-) delete mode 100644 patches/combined_nid_and_la_V0101m.patch diff --git a/patches/combined_nid_and_la_V0101m.patch b/patches/combined_nid_and_la_V0101m.patch deleted file mode 100644 index 3eb8aa12f..000000000 --- a/patches/combined_nid_and_la_V0101m.patch +++ /dev/null @@ -1,3104 +0,0 @@ -Submodule Loop 20c313c..f556dc8: -diff --git a/Loop/Loop Widget Extension/Bootstrap/Bootstrap.swift b/Loop/Loop Widget Extension/Bootstrap/Bootstrap.swift -new file mode 100644 -index 00000000..00823471 ---- /dev/null -+++ b/Loop/Loop Widget Extension/Bootstrap/Bootstrap.swift -@@ -0,0 +1,11 @@ -+// -+// Bootstrap.swift -+// Loop Widget Extension -+// -+// Created by Bastiaan Verhaar on 25/06/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import Foundation -+ -+class Bootstrap{} -diff --git a/Loop/Loop Widget Extension/Helpers/LocalizedString.swift b/Loop/Loop Widget Extension/Helpers/LocalizedString.swift -new file mode 100644 -index 00000000..15818175 ---- /dev/null -+++ b/Loop/Loop Widget Extension/Helpers/LocalizedString.swift -@@ -0,0 +1,21 @@ -+// -+// LocalizedString.swift -+// Loop Widget Extension -+// -+// Created by Bastiaan Verhaar on 25/06/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import Foundation -+ -+private class FrameworkBundle { -+ static let main = Bundle(for: Bootstrap.self) -+} -+ -+func LocalizedString(_ key: String, tableName: String? = nil, value: String? = nil, comment: String) -> String { -+ if let value = value { -+ return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, value: value, comment: comment) -+ } else { -+ return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, comment: comment) -+ } -+} -diff --git a/Loop/Loop Widget Extension/Live Activity/BasalViewActivity.swift b/Loop/Loop Widget Extension/Live Activity/BasalViewActivity.swift -new file mode 100644 -index 00000000..915335c5 ---- /dev/null -+++ b/Loop/Loop Widget Extension/Live Activity/BasalViewActivity.swift -@@ -0,0 +1,46 @@ -+// -+// BasalView.swift -+// Loop -+// -+// Created by Noah Brauner on 8/15/22. -+// Copyright © 2022 LoopKit Authors. All rights reserved. -+// -+ -+import SwiftUI -+ -+struct BasalViewActivity: View { -+ let percent: Double -+ let rate: Double -+ -+ var body: some View { -+ VStack(spacing: 1) { -+ BasalRateView(percent: percent) -+ .overlay( -+ BasalRateView(percent: percent) -+ .stroke(Color("insulin"), lineWidth: 2) -+ ) -+ .foregroundColor(Color("insulin").opacity(0.5)) -+ .frame(width: 44, height: 22) -+ -+ if let rateString = decimalFormatter.string(from: NSNumber(value: rate)) { -+ Text("\(rateString)U") -+ .font(.subheadline) -+ } -+ else { -+ Text("-U") -+ .font(.subheadline) -+ } -+ } -+ } -+ -+ private let decimalFormatter: NumberFormatter = { -+ let formatter = NumberFormatter() -+ formatter.numberStyle = .decimal -+ formatter.minimumFractionDigits = 1 -+ formatter.minimumIntegerDigits = 1 -+ formatter.positiveFormat = "+0.0##" -+ formatter.negativeFormat = "-0.0##" -+ -+ return formatter -+ }() -+} -diff --git a/Loop/Loop Widget Extension/Live Activity/ChartView.swift b/Loop/Loop Widget Extension/Live Activity/ChartView.swift -new file mode 100644 -index 00000000..b65e02c9 ---- /dev/null -+++ b/Loop/Loop Widget Extension/Live Activity/ChartView.swift -@@ -0,0 +1,159 @@ -+// -+// ChartValues.swift -+// Loop Widget Extension -+// -+// Created by Bastiaan Verhaar on 25/06/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import Foundation -+import SwiftUI -+import Charts -+ -+struct ChartView: View { -+ private let glucoseSampleData: [ChartValues] -+ private let predicatedData: [ChartValues] -+ private let glucoseRanges: [GlucoseRangeValue] -+ private let preset: Preset? -+ private let yAxisMarks: [Double] -+ -+ init(glucoseSamples: [GlucoseSampleAttributes], predicatedGlucose: [Double], predicatedStartDate: Date?, predicatedInterval: TimeInterval?, useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) { -+ self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit) -+ self.predicatedData = ChartValues.convert( -+ data: predicatedGlucose, -+ startDate: predicatedStartDate ?? Date.now, -+ interval: predicatedInterval ?? .minutes(5), -+ useLimits: useLimits, -+ lowerLimit: lowerLimit, -+ upperLimit: upperLimit -+ ) -+ self.preset = preset -+ self.glucoseRanges = glucoseRanges -+ self.yAxisMarks = yAxisMarks -+ } -+ -+ init(glucoseSamples: [GlucoseSampleAttributes], useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) { -+ self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit) -+ self.predicatedData = [] -+ self.preset = preset -+ self.glucoseRanges = glucoseRanges -+ self.yAxisMarks = yAxisMarks -+ } -+ -+ var body: some View { -+ ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){ -+ Chart { -+ if let preset = self.preset, predicatedData.count > 0, preset.endDate > Date.now.addingTimeInterval(.hours(-6)) { -+ RectangleMark( -+ xStart: .value("Start", preset.startDate), -+ xEnd: .value("End", preset.endDate), -+ yStart: .value("Preset override", preset.minValue), -+ yEnd: .value("Preset override", preset.maxValue) -+ ) -+ .foregroundStyle(.primary) -+ .opacity(0.6) -+ } -+ -+ ForEach(glucoseRanges) { item in -+ RectangleMark( -+ xStart: .value("Start", item.startDate), -+ xEnd: .value("End", item.endDate), -+ yStart: .value("Glucose range", item.minValue), -+ yEnd: .value("Glucose range", item.maxValue) -+ ) -+ .foregroundStyle(.primary) -+ .opacity(0.3) -+ } -+ -+ ForEach(glucoseSampleData) { item in -+ PointMark (x: .value("Date", item.x), -+ y: .value("Glucose level", item.y) -+ ) -+ .symbolSize(20) -+ .foregroundStyle(by: .value("Color", item.color)) -+ } -+ -+ ForEach(predicatedData) { item in -+ LineMark (x: .value("Date", item.x), -+ y: .value("Glucose level", item.y) -+ ) -+ .lineStyle(StrokeStyle(lineWidth: 3, dash: [2, 3])) -+ } -+ } -+ .chartForegroundStyleScale([ -+ "Good": .green, -+ "High": .orange, -+ "Low": .red, -+ "Default": .blue -+ ]) -+ .chartPlotStyle { plotContent in -+ plotContent.background(.cyan.opacity(0.15)) -+ } -+ .chartLegend(.hidden) -+ .chartYScale(domain: [yAxisMarks.first ?? 0, yAxisMarks.last ?? 0]) -+ .chartYAxis { -+ AxisMarks(values: yAxisMarks) -+ } -+ .chartYAxis { -+ AxisMarks(position: .leading) { _ in -+ AxisValueLabel().foregroundStyle(Color.primary) -+ AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])) -+ .foregroundStyle(Color.primary) -+ } -+ } -+ .chartXAxis { -+ AxisMarks(position: .automatic, values: .stride(by: .hour)) { _ in -+ AxisValueLabel(format: .dateTime.hour(.twoDigits(amPM: .narrow)), anchor: .top) -+ .foregroundStyle(Color.primary) -+ AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])) -+ .foregroundStyle(Color.primary) -+ } -+ } -+ -+ if let preset = self.preset, preset.endDate > Date.now { -+ Text(preset.title) -+ .font(.footnote) -+ .padding(.trailing, 5) -+ .padding(.top, 2) -+ } -+ } -+ } -+} -+ -+struct ChartValues: Identifiable { -+ public let id: UUID -+ public let x: Date -+ public let y: Double -+ public let color: String -+ -+ init(x: Date, y: Double, color: String) { -+ self.id = UUID() -+ self.x = x -+ self.y = y -+ self.color = color -+ } -+ -+ static func convert(data: [Double], startDate: Date, interval: TimeInterval, useLimits: Bool, lowerLimit: Double, upperLimit: Double) -> [ChartValues] { -+ let twoHours = Date.now.addingTimeInterval(.hours(4)) -+ -+ return data.enumerated().filter { (index, item) in -+ return startDate.addingTimeInterval(interval * Double(index)) < twoHours -+ }.map { (index, item) in -+ return ChartValues( -+ x: startDate.addingTimeInterval(interval * Double(index)), -+ y: item, -+ color: !useLimits ? "Default" : item < lowerLimit ? "Low" : item > upperLimit ? "High" : "Good" -+ ) -+ } -+ } -+ -+ static func convert(data: [GlucoseSampleAttributes], useLimits: Bool, lowerLimit: Double, upperLimit: Double) -> [ChartValues] { -+ return data.map { item in -+ return ChartValues( -+ x: item.x, -+ y: item.y, -+ color: !useLimits ? "Default" : item.y < lowerLimit ? "Low" : item.y > upperLimit ? "High" : "Good" -+ ) -+ } -+ } -+} -diff --git a/Loop/Loop Widget Extension/Live Activity/GlucoseLiveActivityConfiguration.swift b/Loop/Loop Widget Extension/Live Activity/GlucoseLiveActivityConfiguration.swift -new file mode 100644 -index 00000000..4d5ed5ef ---- /dev/null -+++ b/Loop/Loop Widget Extension/Live Activity/GlucoseLiveActivityConfiguration.swift -@@ -0,0 +1,298 @@ -+// -+// LiveActivityConfiguration.swift -+// Loop Widget Extension -+// -+// Created by Bastiaan Verhaar on 23/06/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import ActivityKit -+import LoopKit -+import SwiftUI -+import LoopCore -+import WidgetKit -+import Charts -+import HealthKit -+ -+@available(iOS 16.2, *) -+struct GlucoseLiveActivityConfiguration: Widget { -+ private let timeFormatter: DateFormatter = { -+ let dateFormatter = DateFormatter() -+ dateFormatter.dateStyle = .none -+ dateFormatter.timeStyle = .short -+ -+ return dateFormatter -+ }() -+ -+ var body: some WidgetConfiguration { -+ ActivityConfiguration(for: GlucoseActivityAttributes.self) { context in -+ // Create the presentation that appears on the Lock Screen and as a -+ // banner on the Home Screen of devices that don't support the Dynamic Island. -+ ZStack { -+ VStack { -+ if context.attributes.mode == .large { -+ HStack(spacing: 15) { -+ loopIcon(context) -+ if context.attributes.addPredictiveLine { -+ ChartView( -+ glucoseSamples: context.state.glucoseSamples, -+ predicatedGlucose: context.state.predicatedGlucose, -+ predicatedStartDate: context.state.predicatedStartDate, -+ predicatedInterval: context.state.predicatedInterval, -+ useLimits: context.attributes.useLimits, -+ lowerLimit: context.state.isMmol ? context.attributes.lowerLimitChartMmol : context.attributes.lowerLimitChartMg, -+ upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg, -+ glucoseRanges: context.state.glucoseRanges, -+ preset: context.state.preset, -+ yAxisMarks: context.state.yAxisMarks -+ ) -+ .frame(height: 85) -+ } else { -+ ChartView( -+ glucoseSamples: context.state.glucoseSamples, -+ useLimits: context.attributes.useLimits, -+ lowerLimit: context.state.isMmol ? context.attributes.lowerLimitChartMmol : context.attributes.lowerLimitChartMg, -+ upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg, -+ glucoseRanges: context.state.glucoseRanges, -+ preset: context.state.preset, -+ yAxisMarks: context.state.yAxisMarks -+ ) -+ .frame(height: 85) -+ } -+ } -+ } -+ -+ HStack { -+ bottomSpacer(border: false) -+ -+ let endIndex = context.state.bottomRow.endIndex - 1 -+ ForEach(Array(context.state.bottomRow.enumerated()), id: \.element) { (index, item) in -+ switch (item.type) { -+ case .generic: -+ bottomItemGeneric( -+ title: item.label, -+ value: item.value, -+ unit: LocalizedString(item.unit, comment: "No comment") -+ ) -+ -+ case .basal: -+ BasalViewActivity(percent: item.percentage, rate: item.rate) -+ -+ case .currentBg: -+ bottomItemCurrentBG( -+ value: item.value, -+ trend: item.trend, -+ context: context -+ ) -+ -+ case .loopCircle: -+ bottomItemLoopCircle(context: context) -+ } -+ -+ if index != endIndex { -+ bottomSpacer(border: true) -+ } -+ } -+ -+ bottomSpacer(border: false) -+ } -+ } -+ if context.state.ended { -+ VStack { -+ Spacer() -+ HStack { -+ Spacer() -+ Text(NSLocalizedString("Open the app to update the widget", comment: "No comment")) -+ Spacer() -+ } -+ Spacer() -+ } -+ .background(.ultraThinMaterial.opacity(0.8)) -+ .padding(.all, -15) -+ } -+ } -+ .privacySensitive() -+ .padding(.all, 15) -+ .background(BackgroundStyle.background.opacity(0.4)) -+ .activityBackgroundTint(Color.clear) -+ } dynamicIsland: { context in -+ let glucoseFormatter = NumberFormatter.glucoseFormatter(for: context.state.isMmol ? HKUnit.millimolesPerLiter : HKUnit.milligramsPerDeciliter) -+ -+ return DynamicIsland { -+ DynamicIslandExpandedRegion(.leading) { -+ HStack(alignment: .center) { -+ loopIcon(context) -+ .frame(width: 40, height: 40, alignment: .trailing) -+ Spacer() -+ Text("\(glucoseFormatter.string(from: context.state.currentGlucose) ?? "??")\(getArrowImage(context.state.trendType))") -+ .foregroundStyle(getGlucoseColor(context.state.currentGlucose, context: context)) -+ .font(.headline) -+ .fontWeight(.heavy) -+ } -+ } -+ DynamicIslandExpandedRegion(.trailing) { -+ HStack{ -+ Text(context.state.delta) -+ .foregroundStyle(Color(white: 0.9)) -+ .font(.headline) -+ Text(context.state.isMmol ? HKUnit.millimolesPerLiter.localizedShortUnitString : HKUnit.milligramsPerDeciliter.localizedShortUnitString) -+ .foregroundStyle(Color(white: 0.7)) -+ .font(.subheadline) -+ } -+ } -+ DynamicIslandExpandedRegion(.bottom) { -+ if context.attributes.addPredictiveLine { -+ ChartView( -+ glucoseSamples: context.state.glucoseSamples, -+ predicatedGlucose: context.state.predicatedGlucose, -+ predicatedStartDate: context.state.predicatedStartDate, -+ predicatedInterval: context.state.predicatedInterval, -+ useLimits: context.attributes.useLimits, -+ lowerLimit: context.state.isMmol ? context.attributes.lowerLimitChartMmol : context.attributes.lowerLimitChartMg, -+ upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg, -+ glucoseRanges: context.state.glucoseRanges, -+ preset: context.state.preset, -+ yAxisMarks: context.state.yAxisMarks -+ ) -+ .frame(height: 75) -+ } else { -+ ChartView( -+ glucoseSamples: context.state.glucoseSamples, -+ useLimits: context.attributes.useLimits, -+ lowerLimit: context.state.isMmol ? context.attributes.lowerLimitChartMmol : context.attributes.lowerLimitChartMg, -+ upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg, -+ glucoseRanges: context.state.glucoseRanges, -+ preset: context.state.preset, -+ yAxisMarks: context.state.yAxisMarks -+ ) -+ .frame(height: 75) -+ } -+ } -+ } compactLeading: { -+ Text("\(glucoseFormatter.string(from: context.state.currentGlucose) ?? "??")\(getArrowImage(context.state.trendType))") -+ .foregroundStyle(getGlucoseColor(context.state.currentGlucose, context: context)) -+ .minimumScaleFactor(0.1) -+ } compactTrailing: { -+ Text(context.state.delta) -+ .foregroundStyle(Color(white: 0.9)) -+ .minimumScaleFactor(0.1) -+ } minimal: { -+ Text(glucoseFormatter.string(from: context.state.currentGlucose) ?? "??") -+ .foregroundStyle(getGlucoseColor(context.state.currentGlucose, context: context)) -+ .minimumScaleFactor(0.1) -+ } -+ } -+ } -+ -+ @ViewBuilder -+ private func loopIcon(_ context: ActivityViewContext) -> some View { -+ Circle() -+ .trim(from: context.state.isCloseLoop ? 0 : 0.2, to: 1) -+ .stroke(getLoopColor(context.state.lastCompleted), lineWidth: 8) -+ .rotationEffect(Angle(degrees: -126)) -+ .frame(width: 36, height: 36) -+ } -+ -+ @ViewBuilder -+ private func bottomItemGeneric(title: String, value: String, unit: String) -> some View { -+ VStack(alignment: .center) { -+ Text("\(value)\(unit)") -+ .font(.headline) -+ .foregroundStyle(.primary) -+ .fontWeight(.heavy) -+ .font(Font.body.leading(.tight)) -+ Text(title) -+ .font(.subheadline) -+ } -+ } -+ -+ @ViewBuilder -+ private func bottomItemCurrentBG(value: String, trend: GlucoseTrend?, context: ActivityViewContext) -> some View { -+ VStack(alignment: .center) { -+ HStack { -+ Text(value + getArrowImage(trend)) -+ .font(.title) -+ .foregroundStyle(!context.attributes.useLimits ? .primary : getGlucoseColor(context.state.currentGlucose, context: context)) -+ .fontWeight(.heavy) -+ .font(Font.body.leading(.tight)) -+ } -+ } -+ } -+ -+ @ViewBuilder -+ private func bottomItemLoopCircle(context: ActivityViewContext) -> some View { -+ VStack(alignment: .center) { -+ loopIcon(context) -+ } -+ } -+ -+ @ViewBuilder -+ private func bottomSpacer(border: Bool) -> some View { -+ Spacer() -+ if (border) { -+ Divider() -+ .background(.secondary) -+ Spacer() -+ } -+ -+ } -+ -+ private func getArrowImage(_ trendType: GlucoseTrend?) -> String { -+ switch trendType { -+ case .upUpUp: -+ return "\u{2191}\u{2191}" // ↑↑ -+ case .upUp: -+ return "\u{2191}" // ↑ -+ case .up: -+ return "\u{2197}" // ↗ -+ case .flat: -+ return "\u{2192}" // → -+ case .down: -+ return "\u{2198}" // ↘ -+ case .downDown: -+ return "\u{2193}" // ↓ -+ case .downDownDown: -+ return "\u{2193}\u{2193}" // ↓↓ -+ case .none: -+ return "" -+ } -+ } -+ -+ private func getLoopColor(_ age: Date?) -> Color { -+ var freshness: LoopCompletionFreshness = .stale -+ if let age = age { -+ freshness = LoopCompletionFreshness(age: abs(min(0, age.timeIntervalSinceNow))) -+ } -+ -+ switch freshness { -+ case .fresh: -+ return Color("fresh") -+ case .aging: -+ return Color("warning") -+ case .stale: -+ return .red -+ } -+ } -+ -+ private func getGlucoseColor(_ value: Double, context: ActivityViewContext) -> Color { -+ guard context.attributes.useLimits else { -+ return .primary -+ } -+ -+ if -+ context.state.isMmol && value < context.attributes.lowerLimitChartMmol || -+ !context.state.isMmol && value < context.attributes.lowerLimitChartMg -+ { -+ return .red -+ } -+ -+ if -+ context.state.isMmol && value > context.attributes.upperLimitChartMmol || -+ !context.state.isMmol && value > context.attributes.upperLimitChartMg -+ { -+ return .orange -+ } -+ -+ return .green -+ } -+} -diff --git a/Loop/Loop Widget Extension/LoopWidgets.swift b/Loop/Loop Widget Extension/LoopWidgets.swift -index 26f92edb..684bf073 100644 ---- a/Loop/Loop Widget Extension/LoopWidgets.swift -+++ b/Loop/Loop Widget Extension/LoopWidgets.swift -@@ -14,5 +14,6 @@ struct LoopWidgets: WidgetBundle { - @WidgetBundleBuilder - var body: some Widget { - SystemStatusWidget() -+ GlucoseLiveActivityConfiguration() - } - } -diff --git a/Loop/Loop.xcodeproj/project.pbxproj b/Loop/Loop.xcodeproj/project.pbxproj -index 11819516..7e00af68 100644 ---- a/Loop/Loop.xcodeproj/project.pbxproj -+++ b/Loop/Loop.xcodeproj/project.pbxproj -@@ -7,6 +7,7 @@ - objects = { - - /* Begin PBXBuildFile section */ -+ 120490CB2CBFB25A006BDF0A /* NegativeInsulinDamperSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 120490CA2CBFB25A006BDF0A /* NegativeInsulinDamperSelectionView.swift */; }; - 1419606428D9550400BA86E0 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 437AFEE6203688CF008C4892 /* LoopKitUI.framework */; }; - 1419606928D9554E00BA86E0 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 437AFEE6203688CF008C4892 /* LoopKitUI.framework */; }; - 1419606A28D955BC00BA86E0 /* MockKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C101947127DD473C004E7EB8 /* MockKitUI.framework */; }; -@@ -401,6 +402,21 @@ - B4E96D5D248A82A2002DABAD /* StatusBarHUDView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B4E96D5C248A82A2002DABAD /* StatusBarHUDView.xib */; }; - B4F3D25124AF890C0095CE44 /* BluetoothStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4F3D25024AF890C0095CE44 /* BluetoothStateManager.swift */; }; - B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4FEEF7C24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift */; }; -+ B82181F62C93628300478A91 /* LiveActivityManagementViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82181F52C93628300478A91 /* LiveActivityManagementViewModel.swift */; }; -+ B82182002C93716A00478A91 /* ChartAxisGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82181F92C936AD600478A91 /* ChartAxisGenerator.swift */; }; -+ B851FFC52C37221800D738C1 /* LiveActivityManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B851FFC42C37221800D738C1 /* LiveActivityManagementView.swift */; }; -+ B851FFCA2C3731DE00D738C1 /* LiveActivitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B851FFC62C37271E00D738C1 /* LiveActivitySettings.swift */; }; -+ B851FFCB2C3731DE00D738C1 /* LiveActivitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B851FFC62C37271E00D738C1 /* LiveActivitySettings.swift */; }; -+ B87539C92C2B06CE0085A975 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87539C82C2B06CE0085A975 /* LocalizedString.swift */; }; -+ B87539CB2C2B08430085A975 /* Bootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87539CA2C2B08430085A975 /* Bootstrap.swift */; }; -+ B87539CD2C2B46950085A975 /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87539CC2C2B46950085A975 /* ChartView.swift */; }; -+ B87539CF2C2DCB770085A975 /* BasalViewActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87539CE2C2DCB770085A975 /* BasalViewActivity.swift */; }; -+ B87D411D2C28A69600120877 /* GlucoseLiveActivityConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87D411C2C28A69600120877 /* GlucoseLiveActivityConfiguration.swift */; }; -+ B87D411F2C28A85F00120877 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B87D411E2C28A85F00120877 /* ActivityKit.framework */; }; -+ B8A937B82C29BA5900E38645 /* GlucoseActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A937B72C29BA5900E38645 /* GlucoseActivityManager.swift */; }; -+ B8A937C32C29C3B400E38645 /* GlucoseActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A937C02C29C29300E38645 /* GlucoseActivityAttributes.swift */; }; -+ B8A937C42C29C43B00E38645 /* GlucoseActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A937C02C29C29300E38645 /* GlucoseActivityAttributes.swift */; }; -+ B8FD0B522C39CA3E003FB72B /* LiveActivityBottomRowManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FD0B512C39CA3E003FB72B /* LiveActivityBottomRowManagerView.swift */; }; - C1004DF22981F5B700B8CF94 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1004DF02981F5B700B8CF94 /* InfoPlist.strings */; }; - C1004DF52981F5B700B8CF94 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1004DF32981F5B700B8CF94 /* Localizable.strings */; }; - C1004DF82981F5B700B8CF94 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1004DF62981F5B700B8CF94 /* InfoPlist.strings */; }; -@@ -729,6 +745,16 @@ - name = "Embed App Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; -+ B82181FF2C9370F800478A91 /* Embed Frameworks */ = { -+ isa = PBXCopyFilesBuildPhase; -+ buildActionMask = 2147483647; -+ dstPath = ""; -+ dstSubfolderSpec = 10; -+ files = ( -+ ); -+ name = "Embed Frameworks"; -+ runOnlyForDeploymentPostprocessing = 0; -+ }; - C1E3DC4828595FAA00CA19FF /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; -@@ -743,6 +769,7 @@ - /* End PBXCopyFilesBuildPhase section */ - - /* Begin PBXFileReference section */ -+ 120490CA2CBFB25A006BDF0A /* NegativeInsulinDamperSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NegativeInsulinDamperSelectionView.swift; sourceTree = ""; }; - 142CB7582A60BF2E0075748A /* EditMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMode.swift; sourceTree = ""; }; - 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodsView.swift; sourceTree = ""; }; - 1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditFavoriteFoodViewModel.swift; sourceTree = ""; }; -@@ -1325,6 +1352,19 @@ - B4E96D5C248A82A2002DABAD /* StatusBarHUDView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusBarHUDView.xib; sourceTree = ""; }; - B4F3D25024AF890C0095CE44 /* BluetoothStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateManager.swift; sourceTree = ""; }; - B4FEEF7C24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceDataManager+DeviceStatus.swift"; sourceTree = ""; }; -+ B82181F52C93628300478A91 /* LiveActivityManagementViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManagementViewModel.swift; sourceTree = ""; }; -+ B82181F92C936AD600478A91 /* ChartAxisGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartAxisGenerator.swift; sourceTree = ""; }; -+ B851FFC42C37221800D738C1 /* LiveActivityManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManagementView.swift; sourceTree = ""; }; -+ B851FFC62C37271E00D738C1 /* LiveActivitySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettings.swift; sourceTree = ""; }; -+ B87539C82C2B06CE0085A975 /* LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; -+ B87539CA2C2B08430085A975 /* Bootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bootstrap.swift; sourceTree = ""; }; -+ B87539CC2C2B46950085A975 /* ChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = ""; }; -+ B87539CE2C2DCB770085A975 /* BasalViewActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasalViewActivity.swift; sourceTree = ""; }; -+ B87D411C2C28A69600120877 /* GlucoseLiveActivityConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseLiveActivityConfiguration.swift; sourceTree = ""; }; -+ B87D411E2C28A85F00120877 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; }; -+ B8A937B72C29BA5900E38645 /* GlucoseActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseActivityManager.swift; sourceTree = ""; }; -+ B8A937C02C29C29300E38645 /* GlucoseActivityAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseActivityAttributes.swift; sourceTree = ""; }; -+ B8FD0B512C39CA3E003FB72B /* LiveActivityBottomRowManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBottomRowManagerView.swift; sourceTree = ""; }; - C1004DEF2981F5B700B8CF94 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; - C1004DF12981F5B700B8CF94 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; - C1004DF42981F5B700B8CF94 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; -@@ -1727,6 +1767,7 @@ - 1419606928D9554E00BA86E0 /* LoopKitUI.framework in Frameworks */, - 1419606A28D955BC00BA86E0 /* MockKitUI.framework in Frameworks */, - 1481F9BB28DA26F4004C5AEB /* LoopUI.framework in Frameworks */, -+ B87D411F2C28A85F00120877 /* ActivityKit.framework in Frameworks */, - 1419606428D9550400BA86E0 /* LoopKitUI.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; -@@ -1836,6 +1877,7 @@ - 84AA81D12A4A2778000B658B /* Components */, - 84AA81D92A4A2966000B658B /* Helpers */, - 84AA81DE2A4A2B3D000B658B /* Timeline */, -+ B87D41192C28A61900120877 /* Live Activity */, - 84AA81DF2A4A2B7A000B658B /* Widgets */, - 84AA81D22A4A27A3000B658B /* LoopWidgets.swift */, - ); -@@ -2163,6 +2205,7 @@ - 43DE92581C5479E4001FFDE1 /* PotentialCarbEntryUserInfo.swift */, - 43C05CB721EBEA54006FB252 /* HKUnit.swift */, - 434FF1E91CF26C29000DB779 /* IdentifiableClass.swift */, -+ E9C00EEF24C620EF00628F35 /* LoopSettings.swift */, - C19E96DD23D2733F003F79B0 /* LoopCompletionFreshness.swift */, - 430B29892041F54A00BA9F93 /* NSUserDefaults.swift */, - 431E73471FF95A900069B5F7 /* PersistenceController.swift */, -@@ -2170,10 +2213,10 @@ - 43D9FFD121EAE05D00AF44BF /* LoopCore.h */, - 43D9FFD221EAE05D00AF44BF /* Info.plist */, - 4B60626A287E286000BF8BBB /* Localizable.strings */, -- E9C00EEF24C620EF00628F35 /* LoopSettings.swift */, - C16575742539FD60004AE16E /* LoopCoreConstants.swift */, - E9B3551B292844010076AB04 /* MissedMealNotification.swift */, - C1D0B62F2986D4D90098D215 /* LocalizedString.swift */, -+ B851FFC62C37271E00D738C1 /* LiveActivitySettings.swift */, - ); - path = LoopCore; - sourceTree = ""; -@@ -2276,6 +2319,9 @@ - C1AF062229426300002C1B19 /* ManualGlucoseEntryRow.swift */, - DDC389FD2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift */, - DD3DBD282A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift */, -+ B851FFC42C37221800D738C1 /* LiveActivityManagementView.swift */, -+ B8FD0B512C39CA3E003FB72B /* LiveActivityBottomRowManagerView.swift */, -+ 120490CA2CBFB25A006BDF0A /* NegativeInsulinDamperSelectionView.swift */, - ); - path = Views; - sourceTree = ""; -@@ -2315,6 +2361,7 @@ - 1DA6499D2441266400F61E75 /* Alerts */, - E95D37FF24EADE68005E2F50 /* Store Protocols */, - E9B355232935906B0076AB04 /* Missed Meal Detection */, -+ B8A937C52C29C44600E38645 /* Live Activity */, - C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */, - A96DAC2B2838F31200D94E38 /* SharedLogging.swift */, - 7E69CFFB2A16A77E00203CBD /* ResetLoopManager.swift */, -@@ -2525,6 +2572,7 @@ - C11613472983096D00777E7C /* InfoPlist.strings */, - 14B1736D28AEDA63006CCD7C /* LoopWidgetExtension.entitlements */, - 14B1736628AED9EE006CCD7C /* Info.plist */, -+ B87539CA2C2B08430085A975 /* Bootstrap.swift */, - ); - path = Bootstrap; - sourceTree = ""; -@@ -2535,6 +2583,7 @@ - 84AA81DA2A4A2973000B658B /* Date.swift */, - 84AA81D52A4A28AF000B658B /* WidgetBackground.swift */, - 84D2879E2AC756C8007ED283 /* ContentMargin.swift */, -+ B87539C82C2B06CE0085A975 /* LocalizedString.swift */, - ); - path = Helpers; - sourceTree = ""; -@@ -2628,6 +2677,7 @@ - 1D49795724E7289700948F05 /* ServicesViewModel.swift */, - C174233B259BEB0F00399C9D /* ManualEntryDoseViewModel.swift */, - 1DB619AB270BAD3D006C9D07 /* VersionUpdateViewModel.swift */, -+ B82181F52C93628300478A91 /* LiveActivityManagementViewModel.swift */, - ); - path = "View Models"; - sourceTree = ""; -@@ -2663,6 +2713,7 @@ - 968DCD53F724DE56FFE51920 /* Frameworks */ = { - isa = PBXGroup; - children = ( -+ B87D411E2C28A85F00120877 /* ActivityKit.framework */, - C159C82E286787EF00A86EC0 /* LoopKit.framework */, - C159C8212867859800A86EC0 /* MockKitUI.framework */, - C159C8192867857000A86EC0 /* LoopKitUI.framework */, -@@ -2760,6 +2811,26 @@ - path = LoopCore; - sourceTree = ""; - }; -+ B87D41192C28A61900120877 /* Live Activity */ = { -+ isa = PBXGroup; -+ children = ( -+ B87D411C2C28A69600120877 /* GlucoseLiveActivityConfiguration.swift */, -+ B87539CC2C2B46950085A975 /* ChartView.swift */, -+ B87539CE2C2DCB770085A975 /* BasalViewActivity.swift */, -+ ); -+ path = "Live Activity"; -+ sourceTree = ""; -+ }; -+ B8A937C52C29C44600E38645 /* Live Activity */ = { -+ isa = PBXGroup; -+ children = ( -+ B8A937B72C29BA5900E38645 /* GlucoseActivityManager.swift */, -+ B8A937C02C29C29300E38645 /* GlucoseActivityAttributes.swift */, -+ B82181F92C936AD600478A91 /* ChartAxisGenerator.swift */, -+ ); -+ path = "Live Activity"; -+ sourceTree = ""; -+ }; - C13072B82A76AF0A009A7C58 /* live_capture */ = { - isa = PBXGroup; - children = ( -@@ -2982,6 +3053,7 @@ - 14B1735828AED9EC006CCD7C /* Sources */, - 14B1735928AED9EC006CCD7C /* Frameworks */, - 14B1735A28AED9EC006CCD7C /* Resources */, -+ B82181FF2C9370F800478A91 /* Embed Frameworks */, - ); - buildRules = ( - ); -@@ -2989,6 +3061,8 @@ - 1481F9BE28DA26F4004C5AEB /* PBXTargetDependency */, - ); - name = "Loop Widget Extension"; -+ packageProductDependencies = ( -+ ); - productName = SmallStatusWidgetExtension; - productReference = 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */; - productType = "com.apple.product-type.app-extension"; -@@ -3623,12 +3697,15 @@ - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( -+ B87539CB2C2B08430085A975 /* Bootstrap.swift in Sources */, - 14B1738128AEDC70006CCD7C /* StatusExtensionContext.swift in Sources */, - 14B1737628AEDC6C006CCD7C /* HKUnit.swift in Sources */, - 14B1737728AEDC6C006CCD7C /* NSBundle.swift in Sources */, - 84AA81D62A4A28AF000B658B /* WidgetBackground.swift in Sources */, - 84AA81D82A4A2910000B658B /* StatusWidgetTimelimeEntry.swift in Sources */, - 84AA81D32A4A27A3000B658B /* LoopWidgets.swift in Sources */, -+ B87539CF2C2DCB770085A975 /* BasalViewActivity.swift in Sources */, -+ B87539CD2C2B46950085A975 /* ChartView.swift in Sources */, - 84AA81E32A4A36FB000B658B /* SystemActionLink.swift in Sources */, - 14B1737828AEDC6C006CCD7C /* NSTimeInterval.swift in Sources */, - 14B1737928AEDC6C006CCD7C /* NSUserDefaults+StatusExtension.swift in Sources */, -@@ -3639,7 +3716,10 @@ - 14B1737E28AEDC6C006CCD7C /* FeatureFlags.swift in Sources */, - 84AA81E72A4A4DEF000B658B /* PumpView.swift in Sources */, - 14B1737F28AEDC6C006CCD7C /* PluginManager.swift in Sources */, -+ B87D411D2C28A69600120877 /* GlucoseLiveActivityConfiguration.swift in Sources */, -+ B87539C92C2B06CE0085A975 /* LocalizedString.swift in Sources */, - 14B1738028AEDC6C006CCD7C /* Debug.swift in Sources */, -+ B8A937C42C29C43B00E38645 /* GlucoseActivityAttributes.swift in Sources */, - 84AA81DB2A4A2973000B658B /* Date.swift in Sources */, - 14B1737228AEDBF6006CCD7C /* BasalView.swift in Sources */, - 14B1737428AEDBF6006CCD7C /* GlucoseView.swift in Sources */, -@@ -3725,6 +3805,7 @@ - B4E202302661063E009421B5 /* AutomaticDosingStatus.swift in Sources */, - C191D2A125B3ACAA00C26C0B /* DosingStrategySelectionView.swift in Sources */, - A977A2F424ACFECF0059C207 /* CriticalEventLogExportManager.swift in Sources */, -+ B8FD0B522C39CA3E003FB72B /* LiveActivityBottomRowManagerView.swift in Sources */, - 89CA2B32226C18B8004D9350 /* TestingScenariosTableViewController.swift in Sources */, - 43E93FB71E469A5100EAB8DB /* HKUnit.swift in Sources */, - 43C05CAF21EB2C24006FB252 /* NSBundle.swift in Sources */, -@@ -3732,6 +3813,7 @@ - A967D94C24F99B9300CDDF8A /* OutputStream.swift in Sources */, - 1DB1065124467E18005542BD /* AlertManager.swift in Sources */, - 1D9650C82523FBA100A1370B /* DeviceDataManager+BolusEntryViewModelDelegate.swift in Sources */, -+ B851FFC52C37221800D738C1 /* LiveActivityManagementView.swift in Sources */, - 43C0944A1CACCC73001F6403 /* NotificationManager.swift in Sources */, - 149A28E42A8A63A700052EDF /* FavoriteFoodDetailView.swift in Sources */, - 1DDE274024AEA4F200796622 /* NotificationsCriticalAlertPermissionsView.swift in Sources */, -@@ -3786,6 +3868,7 @@ - C1AF062329426300002C1B19 /* ManualGlucoseEntryRow.swift in Sources */, - C148CEE724FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift in Sources */, - DDC389FC2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift in Sources */, -+ B82181F62C93628300478A91 /* LiveActivityManagementViewModel.swift in Sources */, - 1D12D3B92548EFDD00B53E8B /* main.swift in Sources */, - 435400341C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */, - A9DCF32A25B0FABF00C89088 /* LoopUIColorPalette+Default.swift in Sources */, -@@ -3818,11 +3901,13 @@ - A97F250825E056D500F0EE19 /* OnboardingManager.swift in Sources */, - 438D42F91D7C88BC003244B0 /* PredictionInputEffect.swift in Sources */, - 892A5D692230C41D008961AB /* RangeReplaceableCollection.swift in Sources */, -+ B8A937B82C29BA5900E38645 /* GlucoseActivityManager.swift in Sources */, - DDC389F62A2B61750066E2E8 /* ApplicationFactorStrategy.swift in Sources */, - 4F70C2101DE8FAC5006380B7 /* ExtensionDataManager.swift in Sources */, - 43DFB62320D4CAE7008A7BAE /* PumpManager.swift in Sources */, - A9FB75F1252BE320004C7D3F /* BolusDosingDecision.swift in Sources */, - 892A5D59222F0A27008961AB /* Debug.swift in Sources */, -+ B82182002C93716A00478A91 /* ChartAxisGenerator.swift in Sources */, - 431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */, - 1D05219D2469F1F5000EBBDE /* AlertStore.swift in Sources */, - 439897371CD2F80600223065 /* AnalyticsServicesManager.swift in Sources */, -@@ -3832,9 +3917,11 @@ - 895FE0952201234000FCF18A /* OverrideSelectionViewController.swift in Sources */, - C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */, - A9F66FC3247F451500096EA7 /* UIDevice+Loop.swift in Sources */, -+ 120490CB2CBFB25A006BDF0A /* NegativeInsulinDamperSelectionView.swift in Sources */, - 439706E622D2E84900C81566 /* PredictionSettingTableViewCell.swift in Sources */, - 430D85891F44037000AF2D4F /* HUDViewTableViewCell.swift in Sources */, - 43A51E211EB6DBDD000736CC /* LoopChartsTableViewController.swift in Sources */, -+ B8A937C32C29C3B400E38645 /* GlucoseActivityAttributes.swift in Sources */, - 8968B1122408B3520074BB48 /* UIFont.swift in Sources */, - 1452F4A92A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift in Sources */, - 438D42FB1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift in Sources */, -@@ -3956,6 +4043,7 @@ - C17DDC9D28AC33A1005FBF4C /* PersistedProperty.swift in Sources */, - 43C05CA921EB2B26006FB252 /* PersistenceController.swift in Sources */, - A9CE912224CA032E00302A40 /* NSUserDefaults.swift in Sources */, -+ B851FFCB2C3731DE00D738C1 /* LiveActivitySettings.swift in Sources */, - 43C05CAB21EB2B4A006FB252 /* NSBundle.swift in Sources */, - 43C05CC721EC2ABC006FB252 /* IdentifiableClass.swift in Sources */, - E9B3552B293591E70076AB04 /* MissedMealNotification.swift in Sources */, -@@ -3977,6 +4065,7 @@ - C17DDC9C28AC339E005FBF4C /* PersistedProperty.swift in Sources */, - 43C05CA821EB2B26006FB252 /* PersistenceController.swift in Sources */, - 43C05CAA21EB2B49006FB252 /* NSBundle.swift in Sources */, -+ B851FFCA2C3731DE00D738C1 /* LiveActivitySettings.swift in Sources */, - 43C05CC821EC2ABC006FB252 /* IdentifiableClass.swift in Sources */, - 43C05CAD21EB2BBF006FB252 /* NSUserDefaults.swift in Sources */, - E9B3552A293591E70076AB04 /* MissedMealNotification.swift in Sources */, -@@ -4832,6 +4921,7 @@ - INFOPLIST_FILE = "Loop Widget Extension/Bootstrap/Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = "Loop Widgets"; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 LoopKit Authors. All rights reserved."; -+ IPHONEOS_DEPLOYMENT_TARGET = 16.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", -@@ -4880,6 +4970,7 @@ - INFOPLIST_FILE = "Loop Widget Extension/Bootstrap/Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = "Loop Widgets"; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 LoopKit Authors. All rights reserved."; -+ IPHONEOS_DEPLOYMENT_TARGET = 16.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", -@@ -5138,6 +5229,7 @@ - DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - INFOPLIST_FILE = Loop/Info.plist; -+ IPHONEOS_DEPLOYMENT_TARGET = 16.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", -@@ -5167,6 +5259,7 @@ - DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - INFOPLIST_FILE = Loop/Info.plist; -+ IPHONEOS_DEPLOYMENT_TARGET = 16.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", -@@ -5423,6 +5516,7 @@ - DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = "Loop Status Extension/Info.plist"; -+ IPHONEOS_DEPLOYMENT_TARGET = 16.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", -@@ -5449,6 +5543,7 @@ - DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = "Loop Status Extension/Info.plist"; -+ IPHONEOS_DEPLOYMENT_TARGET = 16.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", -@@ -5516,6 +5611,7 @@ - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - INFOPLIST_FILE = "Loop Intent Extension/Info.plist"; -+ IPHONEOS_DEPLOYMENT_TARGET = 16.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", -@@ -5543,6 +5639,7 @@ - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - INFOPLIST_FILE = "Loop Intent Extension/Info.plist"; -+ IPHONEOS_DEPLOYMENT_TARGET = 16.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", -diff --git a/Loop/Loop/Info.plist b/Loop/Loop/Info.plist -index ddad5426..f9f4ca84 100644 ---- a/Loop/Loop/Info.plist -+++ b/Loop/Loop/Info.plist -@@ -71,6 +71,10 @@ - Carbohydrate meal data entered in the app and on the watch is stored in the Health database. Glucose data retrieved from the CGM is stored securely in HealthKit. - NSSiriUsageDescription - Loop uses Siri to allow you to enact presets with your voice. -+ NSSupportsLiveActivities -+ -+ NSSupportsLiveActivitiesFrequentUpdates -+ - NSUserActivityTypes - - EnableOverridePresetIntent -diff --git a/Loop/Loop/Loop.entitlements b/Loop/Loop/Loop.entitlements -index 50ba55d9..e6a2f9b9 100644 ---- a/Loop/Loop/Loop.entitlements -+++ b/Loop/Loop/Loop.entitlements -@@ -8,6 +8,8 @@ - - com.apple.developer.healthkit.access - -+ com.apple.developer.healthkit.background-delivery -+ - com.apple.developer.nfc.readersession.formats - - TAG -diff --git a/Loop/Loop/Managers/Live Activity/ChartAxisGenerator.swift b/Loop/Loop/Managers/Live Activity/ChartAxisGenerator.swift -new file mode 100644 -index 00000000..0fcc3ca8 ---- /dev/null -+++ b/Loop/Loop/Managers/Live Activity/ChartAxisGenerator.swift -@@ -0,0 +1,125 @@ -+// -+// ChartAxisGenerator.swift -+// Loop -+// -+// Created by Bastiaan Verhaar on 12/09/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import Foundation -+import HealthKit -+import SwiftCharts -+import UIKit -+ -+struct ChartAxisGenerator { -+ private static let yAxisStepSizeMGDLOverride: Double? = FeatureFlags.predictedGlucoseChartClampEnabled ? 40 : nil -+ private static let range = FeatureFlags.predictedGlucoseChartClampEnabled ? LoopConstants.glucoseChartDefaultDisplayBoundClamped : LoopConstants.glucoseChartDefaultDisplayBound -+ private static let predictedGlucoseSoftBoundsMinimum = FeatureFlags.predictedGlucoseChartClampEnabled ? HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 40) : nil -+ -+ private static let minSegmentCount: Double = 2 -+ private static let addPaddingSegmentIfEdge = false -+ private static let axisLabelSettings = ChartLabelSettings(font: .systemFont(ofSize: 14), fontColor: UIColor.secondaryLabel) -+ -+ // This logic is copied/ported from generateYAxisValuesUsingLinearSegmentStep -+ static func getYAxis(points: [Double], isMmol: Bool) -> [Double] { -+ let unit: HKUnit = isMmol ? .millimolesPerLiter : .milligramsPerDeciliter -+ -+ let glucoseDisplayRange = [ -+ range.lowerBound.doubleValue(for: unit), -+ range.upperBound.doubleValue(for: unit) -+ ] -+ -+ let actualPoints = points + glucoseDisplayRange -+ let sortedChartPoints = actualPoints.sorted {(obj1, obj2) in -+ return obj1 < obj2 -+ } -+ -+ guard let first = sortedChartPoints.first, let lastPar = sortedChartPoints.last else { -+ print("Trying to generate Y axis without datapoints, returning empty array") -+ return [] -+ } -+ -+ let maxSegmentCount: Double = glucoseValueBelowSoftBoundsMinimum(first, unit) ? 5 : 4 -+ -+ guard lastPar >=~ first else {fatalError("Invalid range generating axis values")} -+ let multiple: Double = !isMmol ? (yAxisStepSizeMGDLOverride ?? 25) : 1 -+ -+ let last = needsToAddOne(lastPar, first) ? lastPar + 1 : lastPar -+ -+ /// The first axis value will be less than or equal to the first scalar value, aligned with the desired multiple -+ var firstValue = first - (first.truncatingRemainder(dividingBy: multiple)) -+ /// The last axis value will be greater than or equal to the last scalar value, aligned with the desired multiple -+ let remainder = last.truncatingRemainder(dividingBy: multiple) -+ var lastValue = remainder == 0 ? last : last + (multiple - remainder) -+ var segmentSize = multiple -+ -+ /// If there should be a padding segment added when a scalar value falls on the first or last axis value, adjust the first and last axis values -+ if firstValue =~ first && addPaddingSegmentIfEdge { -+ firstValue = firstValue - segmentSize -+ } -+ -+ // do not allow the first label to be displayed as -0 -+ while firstValue < 0 && firstValue.rounded() == -0 { -+ firstValue = firstValue - segmentSize -+ } -+ -+ if lastValue =~ last && addPaddingSegmentIfEdge { -+ lastValue = lastValue + segmentSize -+ } -+ -+ let distance = lastValue - firstValue -+ var currentMultiple = multiple -+ var segmentCount = distance / currentMultiple -+ var potentialSegmentValues = stride(from: firstValue, to: lastValue, by: currentMultiple) -+ -+ /// Find the optimal number of segments and segment width -+ /// If the number of segments is greater than desired, make each segment wider -+ /// ensure no label of -0 will be displayed on the axis -+ while segmentCount > maxSegmentCount || -+ !potentialSegmentValues.filter({ $0 < 0 && $0.rounded() == -0 }).isEmpty -+ { -+ currentMultiple += multiple -+ segmentCount = distance / currentMultiple -+ potentialSegmentValues = stride(from: firstValue, to: lastValue, by: currentMultiple) -+ } -+ segmentCount = ceil(segmentCount) -+ -+ /// Increase the number of segments until there are enough as desired -+ while segmentCount < minSegmentCount { -+ segmentCount += 1 -+ } -+ segmentSize = currentMultiple -+ -+ /// Generate axis values from the first value, segment size and number of segments -+ let offset = firstValue -+ return (0...Int(segmentCount)).map {segment in -+ var scalar = offset + (Double(segment) * segmentSize) -+ // a value that could be displayed as 0 should truly be 0 to have the zero-line drawn correctly. -+ if scalar != 0, -+ scalar.rounded() == 0 -+ { -+ scalar = 0 -+ } -+ return ChartAxisValueDouble(scalar, labelSettings: axisLabelSettings).scalar -+ } -+ } -+ -+ private static func needsToAddOne(_ a: Double, _ b: Double) -> Bool { -+ return fabs(a - b) < Double.ulpOfOne -+ } -+ -+ private static func glucoseValueBelowSoftBoundsMinimum(_ glucoseMinimum: Double, _ unit: HKUnit) -> Bool { -+ guard let predictedGlucoseSoftBoundsMinimum = predictedGlucoseSoftBoundsMinimum else -+ { -+ return false -+ } -+ -+ return HKQuantity(unit: unit, doubleValue: glucoseMinimum) < predictedGlucoseSoftBoundsMinimum -+ } -+} -+ -+fileprivate extension Double { -+ static func >=~ (a: Double, b: Double) -> Bool { -+ return a =~ b || a > b -+ } -+} -diff --git a/Loop/Loop/Managers/Live Activity/GlucoseActivityAttributes.swift b/Loop/Loop/Managers/Live Activity/GlucoseActivityAttributes.swift -new file mode 100644 -index 00000000..936de751 ---- /dev/null -+++ b/Loop/Loop/Managers/Live Activity/GlucoseActivityAttributes.swift -@@ -0,0 +1,151 @@ -+// -+// LiveActivityAttributes.swift -+// LoopUI -+// -+// Created by Bastiaan Verhaar on 23/06/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import ActivityKit -+import Foundation -+import LoopKit -+import LoopCore -+ -+public struct GlucoseActivityAttributes: ActivityAttributes { -+ public struct ContentState: Codable, Hashable { -+ // Meta data -+ public let date: Date -+ public let ended: Bool -+ public let preset: Preset? -+ public let glucoseRanges: [GlucoseRangeValue] -+ -+ // Dynamic island data -+ public let currentGlucose: Double -+ public let trendType: GlucoseTrend? -+ public let delta: String -+ public let isMmol: Bool -+ -+ // Loop circle -+ public let isCloseLoop: Bool -+ public let lastCompleted: Date? -+ -+ // Bottom row -+ public let bottomRow: [BottomRowItem] -+ -+ // Chart view -+ public let glucoseSamples: [GlucoseSampleAttributes] -+ public let predicatedGlucose: [Double] -+ public let predicatedStartDate: Date? -+ public let predicatedInterval: TimeInterval? -+ public let yAxisMarks: [Double] -+ } -+ -+ public let mode: LiveActivityMode -+ public let addPredictiveLine: Bool -+ public let useLimits: Bool -+ public let upperLimitChartMmol: Double -+ public let lowerLimitChartMmol: Double -+ public let upperLimitChartMg: Double -+ public let lowerLimitChartMg: Double -+} -+ -+public struct Preset: Codable, Hashable { -+ public let title: String -+ public let startDate: Date -+ public let endDate: Date -+ public let minValue: Double -+ public let maxValue: Double -+} -+ -+public struct GlucoseRangeValue: Identifiable, Codable, Hashable { -+ public let id: UUID -+ public let minValue: Double -+ public let maxValue: Double -+ public let startDate: Date -+ public let endDate: Date -+} -+ -+public struct BottomRowItem: Codable, Hashable { -+ public enum BottomRowType: Codable, Hashable { -+ case generic -+ case basal -+ case currentBg -+ case loopCircle -+ } -+ -+ public let type: BottomRowType -+ -+ // Generic properties -+ public let label: String -+ public let value: String -+ public let unit: String -+ -+ public let trend: GlucoseTrend? -+ -+ // Basal properties -+ public let rate: Double -+ public let percentage: Double -+ -+ private init(type: BottomRowType, label: String?, value: String?, unit: String?, trend: GlucoseTrend?, rate: Double?, percentage: Double?) { -+ self.type = type -+ self.label = label ?? "" -+ self.value = value ?? "" -+ self.trend = trend -+ self.unit = unit ?? "" -+ self.rate = rate ?? 0 -+ self.percentage = percentage ?? 0 -+ } -+ -+ static func generic(label: String, value: String, unit: String) -> BottomRowItem { -+ return BottomRowItem( -+ type: .generic, -+ label: label, -+ value: value, -+ unit: unit, -+ trend: nil, -+ rate: nil, -+ percentage: nil -+ ) -+ } -+ -+ static func basal(rate: Double, percentage: Double) -> BottomRowItem { -+ return BottomRowItem( -+ type: .basal, -+ label: nil, -+ value: nil, -+ unit: nil, -+ trend: nil, -+ rate: rate, -+ percentage: percentage -+ ) -+ } -+ -+ static func currentBg(label: String, value: String, trend: GlucoseTrend?) -> BottomRowItem { -+ return BottomRowItem( -+ type: .currentBg, -+ label: label, -+ value: value, -+ unit: nil, -+ trend: trend, -+ rate: nil, -+ percentage: nil -+ ) -+ } -+ -+ static func loopIcon() -> BottomRowItem { -+ return BottomRowItem( -+ type: .loopCircle, -+ label: nil, -+ value: nil, -+ unit: nil, -+ trend: nil, -+ rate: nil, -+ percentage: nil -+ ) -+ } -+} -+ -+public struct GlucoseSampleAttributes: Codable, Hashable { -+ public let x: Date -+ public let y: Double -+} -diff --git a/Loop/Loop/Managers/Live Activity/GlucoseActivityManager.swift b/Loop/Loop/Managers/Live Activity/GlucoseActivityManager.swift -new file mode 100644 -index 00000000..3903ee8b ---- /dev/null -+++ b/Loop/Loop/Managers/Live Activity/GlucoseActivityManager.swift -@@ -0,0 +1,520 @@ -+// -+// LiveActivityManaer.swift -+// Loop -+// -+// Created by Bastiaan Verhaar on 24/06/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import LoopKitUI -+import LoopKit -+import LoopCore -+import Foundation -+import HealthKit -+import ActivityKit -+ -+extension Notification.Name { -+ static let LiveActivitySettingsChanged = Notification.Name(rawValue: "com.loopKit.notification.LiveActivitySettingsChanged") -+} -+ -+@available(iOS 16.2, *) -+class GlucoseActivityManager { -+ private let activityInfo = ActivityAuthorizationInfo() -+ private var activity: Activity? -+ private let healthStore = HKHealthStore() -+ -+ private let glucoseStore: GlucoseStoreProtocol -+ private let doseStore: DoseStoreProtocol -+ private var loopSettings: LoopSettings -+ -+ private var startDate: Date = Date.now -+ private var settings: LiveActivitySettings = UserDefaults.standard.liveActivity ?? LiveActivitySettings() -+ -+ private let cobFormatter: NumberFormatter = { -+ let numberFormatter = NumberFormatter() -+ numberFormatter.numberStyle = .none -+ return numberFormatter -+ }() -+ private let iobFormatter: NumberFormatter = { -+ let numberFormatter = NumberFormatter() -+ numberFormatter.numberStyle = .none -+ numberFormatter.maximumFractionDigits = 1 -+ numberFormatter.minimumFractionDigits = 1 -+ return numberFormatter -+ }() -+ private let timeFormatter: DateFormatter = { -+ let dateFormatter = DateFormatter() -+ dateFormatter.dateStyle = .none -+ dateFormatter.timeStyle = .short -+ -+ return dateFormatter -+ }() -+ -+ init?(glucoseStore: GlucoseStoreProtocol, doseStore: DoseStoreProtocol, loopSettings: LoopSettings) { -+ guard self.activityInfo.areActivitiesEnabled else { -+ print("ERROR: Live Activities are not enabled...") -+ return nil -+ } -+ -+ self.glucoseStore = glucoseStore -+ self.doseStore = doseStore -+ self.loopSettings = loopSettings -+ -+ // Ensure settings exist -+ if UserDefaults.standard.liveActivity == nil { -+ self.settings = LiveActivitySettings() -+ } -+ -+ let nc = NotificationCenter.default -+ nc.addObserver(self, selector: #selector(self.appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil) -+ nc.addObserver(self, selector: #selector(self.settingsChanged), name: .LiveActivitySettingsChanged, object: nil) -+ guard self.settings.enabled else { -+ return -+ } -+ -+ initEmptyActivity(settings: self.settings) -+ update() -+ -+ Task { -+ await self.endUnknownActivities() -+ } -+ } -+ -+ public func update(loopSettings: LoopSettings) { -+ self.loopSettings = loopSettings -+ update() -+ } -+ -+ private func update() { -+ Task { -+ if self.needsRecreation(), await UIApplication.shared.applicationState == .active { -+ // activity is no longer visible or old. End it and try to push the update again -+ print("INFO: Live Activities needs recreation") -+ await endActivity() -+ update() -+ return -+ } -+ -+ guard let unit = await self.healthStore.cachedPreferredUnits(for: .bloodGlucose) else { -+ print("ERROR: No unit found...") -+ return -+ } -+ -+ await self.endUnknownActivities() -+ -+ let statusContext = UserDefaults.appGroup?.statusExtensionContext -+ let glucoseFormatter = NumberFormatter.glucoseFormatter(for: unit) -+ -+ let glucoseSamples = self.getGlucoseSample(unit: unit) -+ guard let currentGlucose = glucoseSamples.last else { -+ print("ERROR: No glucose sample found...") -+ return -+ } -+ -+ let current = currentGlucose.quantity.doubleValue(for: unit) -+ -+ var delta: String = "+\(glucoseFormatter.string(from: Double(0)) ?? "")" -+ if glucoseSamples.count > 1 { -+ let prevSample = glucoseSamples[glucoseSamples.count - 2] -+ let deltaValue = current - (prevSample.quantity.doubleValue(for: unit)) -+ delta = "\(deltaValue < 0 ? "-" : "+")\(glucoseFormatter.string(from: abs(deltaValue)) ?? "??")" -+ } -+ -+ -+ let bottomRow = self.getBottomRow( -+ currentGlucose: current, -+ delta: delta, -+ statusContext: statusContext, -+ glucoseFormatter: glucoseFormatter -+ ) -+ -+ var predicatedGlucose: [Double] = [] -+ if let samples = statusContext?.predictedGlucose?.values, settings.addPredictiveLine { -+ predicatedGlucose = samples -+ } -+ -+ var endDateChart: Date? = nil -+ if predicatedGlucose.count == 0 { -+ endDateChart = glucoseSamples.last?.startDate -+ } else if let predictedGlucose = statusContext?.predictedGlucose { -+ endDateChart = predictedGlucose.startDate.addingTimeInterval(.hours(4)) -+ } -+ -+ guard let endDateChart = endDateChart else { -+ return -+ } -+ -+ var presetContext: Preset? = nil -+ if let override = self.loopSettings.preMealOverride ?? self.loopSettings.scheduleOverride, let start = glucoseSamples.first?.startDate { -+ presetContext = Preset( -+ title: override.getTitle(), -+ startDate: max(override.startDate, start), -+ endDate: override.duration.isInfinite ? endDateChart : min(override.actualEndDate, endDateChart), -+ minValue: override.settings.targetRange?.lowerBound.doubleValue(for: unit) ?? 0, -+ maxValue: override.settings.targetRange?.upperBound.doubleValue(for: unit) ?? 0 -+ ) -+ } -+ -+ var glucoseRanges: [GlucoseRangeValue] = [] -+ if let glucoseRangeSchedule = self.loopSettings.glucoseTargetRangeSchedule, let start = glucoseSamples.first?.startDate { -+ glucoseRanges = getGlucoseRanges( -+ glucoseRangeSchedule: glucoseRangeSchedule, -+ presetContext: presetContext, -+ start: start, -+ end: endDateChart, -+ unit: unit -+ ) -+ } -+ -+ let yAxisPoints = glucoseSamples.map{ item in item.quantity.doubleValue(for: unit) } + predicatedGlucose -+ let chartYAxis = ChartAxisGenerator.getYAxis( -+ points: yAxisPoints, -+ isMmol: unit == HKUnit.millimolesPerLiter -+ ) -+ -+ let state = GlucoseActivityAttributes.ContentState( -+ date: currentGlucose.startDate, -+ ended: false, -+ preset: presetContext, -+ glucoseRanges: glucoseRanges, -+ currentGlucose: current, -+ trendType: statusContext?.glucoseDisplay?.trendType, -+ delta: delta, -+ isMmol: unit == HKUnit.millimolesPerLiter, -+ isCloseLoop: statusContext?.isClosedLoop ?? false, -+ lastCompleted: statusContext?.lastLoopCompleted, -+ bottomRow: bottomRow, -+ // In order to prevent maxSize errors, only allow the last 100 samples to be sent -+ // Will most likely not be an issue, might be an issue for debugging/CGM simulator with 5sec interval -+ glucoseSamples: glucoseSamples.suffix(100).map { item in -+ return GlucoseSampleAttributes(x: item.startDate, y: item.quantity.doubleValue(for: unit)) -+ }, -+ predicatedGlucose: predicatedGlucose, -+ predicatedStartDate: statusContext?.predictedGlucose?.startDate, -+ predicatedInterval: statusContext?.predictedGlucose?.interval, -+ yAxisMarks: chartYAxis -+ ) -+ -+ await self.activity?.update(ActivityContent( -+ state: state, -+ staleDate: Date.now.addingTimeInterval(.hours(1)) -+ )) -+ } -+ } -+ -+ @objc private func settingsChanged() { -+ Task { -+ let newSettings = UserDefaults.standard.liveActivity ?? LiveActivitySettings() -+ -+ // Update live activity if needed -+ if !newSettings.enabled, let activity = self.activity { -+ await activity.end(nil, dismissalPolicy: .immediate) -+ self.activity = nil -+ -+ return -+ } else if newSettings.enabled && self.activity == nil { -+ initEmptyActivity(settings: newSettings) -+ -+ } else if newSettings != self.settings { -+ await self.activity?.end(nil, dismissalPolicy: .immediate) -+ self.activity = nil -+ -+ initEmptyActivity(settings: newSettings) -+ } -+ -+ self.settings = newSettings -+ update() -+ } -+ } -+ -+ @objc private func appMovedToForeground() { -+ guard self.settings.enabled else { -+ return -+ } -+ -+ guard let activity = self.activity else { -+ initEmptyActivity(settings: self.settings) -+ update() -+ return -+ } -+ -+ Task { -+ await activity.end(nil, dismissalPolicy: .immediate) -+ await self.endUnknownActivities() -+ self.activity = nil -+ -+ initEmptyActivity(settings: self.settings) -+ update() -+ } -+ } -+ -+ private func endUnknownActivities() async { -+ for unknownActivity in Activity.activities -+ .filter({ self.activity?.id != $0.id }) -+ { -+ await unknownActivity.end(nil, dismissalPolicy: .immediate) -+ } -+ } -+ -+ private func endActivity() async { -+ let dynamicState = self.activity?.content.state -+ -+ await self.activity?.end(nil, dismissalPolicy: .immediate) -+ for unknownActivity in Activity.activities { -+ await unknownActivity.end(nil, dismissalPolicy: .immediate) -+ } -+ -+ do { -+ if let dynamicState = dynamicState { -+ self.activity = try Activity.request( -+ attributes: GlucoseActivityAttributes( -+ mode: self.settings.mode, -+ addPredictiveLine: self.settings.addPredictiveLine, -+ useLimits: self.settings.useLimits, -+ upperLimitChartMmol: self.settings.upperLimitChartMmol, -+ lowerLimitChartMmol: self.settings.lowerLimitChartMmol, -+ upperLimitChartMg: self.settings.upperLimitChartMg, -+ lowerLimitChartMg: self.settings.lowerLimitChartMg -+ ), -+ content: .init(state: dynamicState, staleDate: nil), -+ pushType: .token -+ ) -+ } -+ self.startDate = Date.now -+ } catch { -+ print("ERROR: Error while ending live activity: \(error.localizedDescription)") -+ } -+ } -+ -+ private func needsRecreation() -> Bool { -+ if !self.settings.enabled { -+ return false -+ } -+ -+ switch activity?.activityState { -+ case .dismissed, -+ .ended, -+ .stale: -+ return true -+ case .active: -+ return -startDate.timeIntervalSinceNow > .hours(1) -+ default: -+ return true -+ } -+ } -+ -+ private func getInsulinOnBoard() -> String { -+ let updateGroup = DispatchGroup() -+ var iob = "??" -+ -+ updateGroup.enter() -+ self.doseStore.insulinOnBoard(at: Date.now) { result in -+ switch (result) { -+ case .failure: -+ break -+ case .success(let iobValue): -+ iob = self.iobFormatter.string(from: iobValue.value) ?? "??" -+ break -+ } -+ -+ updateGroup.leave() -+ } -+ -+ _ = updateGroup.wait(timeout: .distantFuture) -+ return iob -+ } -+ -+ private func getGlucoseSample(unit: HKUnit) -> [StoredGlucoseSample] { -+ let updateGroup = DispatchGroup() -+ var samples: [StoredGlucoseSample] = [] -+ -+ updateGroup.enter() -+ -+ // When in spacious mode, we want to show the predictive line -+ // In compact mode, we only want to show the history -+ let timeInterval: TimeInterval = self.settings.addPredictiveLine ? .hours(-2) : .hours(-6) -+ self.glucoseStore.getGlucoseSamples( -+ start: Date.now.addingTimeInterval(timeInterval), -+ end: Date.now -+ ) { result in -+ switch (result) { -+ case .failure: -+ break -+ case .success(let data): -+ samples = data -+ break -+ } -+ -+ updateGroup.leave() -+ } -+ -+ _ = updateGroup.wait(timeout: .distantFuture) -+ return samples -+ } -+ -+ private func getGlucoseRanges(glucoseRangeSchedule: GlucoseRangeSchedule, presetContext: Preset?, start: Date, end: Date, unit: HKUnit) -> [GlucoseRangeValue] { -+ var glucoseRanges: [GlucoseRangeValue] = [] -+ for item in glucoseRangeSchedule.quantityBetween(start: start, end: end) { -+ let minValue = item.value.lowerBound.doubleValue(for: unit) -+ let maxValue = item.value.upperBound.doubleValue(for: unit) -+ let startDate = max(item.startDate, start) -+ let endDate = min(item.endDate, end) -+ -+ if let presetContext = presetContext { -+ if presetContext.startDate > startDate, presetContext.endDate < endDate { -+ // A preset is active during this schedule -+ glucoseRanges.append(GlucoseRangeValue( -+ id: UUID(), -+ minValue: minValue, -+ maxValue: maxValue, -+ startDate: startDate, -+ endDate: presetContext.startDate -+ )) -+ glucoseRanges.append(GlucoseRangeValue( -+ id: UUID(), -+ minValue: minValue, -+ maxValue: maxValue, -+ startDate: presetContext.endDate, -+ endDate: endDate -+ )) -+ } else if presetContext.endDate > startDate, presetContext.endDate < endDate { -+ // Cut off the start of the glucose target -+ glucoseRanges.append(GlucoseRangeValue( -+ id: UUID(), -+ minValue: minValue, -+ maxValue: maxValue, -+ startDate: presetContext.endDate, -+ endDate: endDate -+ )) -+ } else if presetContext.startDate < endDate, presetContext.startDate > startDate { -+ // Cut off the end of the glucose target -+ glucoseRanges.append(GlucoseRangeValue( -+ id: UUID(), -+ minValue: minValue, -+ maxValue: maxValue, -+ startDate: startDate, -+ endDate: presetContext.startDate -+ )) -+ if presetContext.endDate == end { -+ break -+ } -+ } else { -+ // No overlap with target and override -+ glucoseRanges.append(GlucoseRangeValue( -+ id: UUID(), -+ minValue: minValue, -+ maxValue: maxValue, -+ startDate: startDate, -+ endDate: endDate -+ )) -+ } -+ } else { -+ glucoseRanges.append(GlucoseRangeValue( -+ id: UUID(), -+ minValue: minValue, -+ maxValue: maxValue, -+ startDate: startDate, -+ endDate: endDate -+ )) -+ } -+ } -+ -+ return glucoseRanges -+ } -+ -+ private func getBottomRow(currentGlucose: Double, delta: String, statusContext: StatusExtensionContext?, glucoseFormatter: NumberFormatter) -> [BottomRowItem] { -+ return self.settings.bottomRowConfiguration.map { type in -+ switch(type) { -+ case .iob: -+ return BottomRowItem.generic(label: type.name(), value: getInsulinOnBoard(), unit: "U") -+ -+ case .cob: -+ var cob: String = "0" -+ if let cobValue = statusContext?.carbsOnBoard { -+ cob = self.cobFormatter.string(from: cobValue) ?? "??" -+ } -+ return BottomRowItem.generic(label: type.name(), value: cob, unit: "g") -+ -+ case .basal: -+ guard let netBasalContext = statusContext?.netBasal else { -+ return BottomRowItem.basal(rate: 0, percentage: 0) -+ } -+ -+ return BottomRowItem.basal(rate: netBasalContext.rate, percentage: netBasalContext.percentage) -+ -+ case .currentBg: -+ return BottomRowItem.currentBg(label: type.name(), value: "\(glucoseFormatter.string(from: currentGlucose) ?? "??")", trend: statusContext?.glucoseDisplay?.trendType) -+ -+ case .eventualBg: -+ guard let eventual = statusContext?.predictedGlucose?.values.last else { -+ return BottomRowItem.generic(label: type.name(), value: "??", unit: "") -+ } -+ -+ return BottomRowItem.generic(label: type.name(), value: glucoseFormatter.string(from: eventual) ?? "??", unit: "") -+ -+ case .deltaBg: -+ return BottomRowItem.generic(label: type.name(), value: delta, unit: "") -+ -+ case .loopCircle: -+ return BottomRowItem.loopIcon() -+ -+ case .updatedAt: -+ return BottomRowItem.generic(label: type.name(), value: timeFormatter.string(from: Date.now), unit: "") -+ } -+ } -+ } -+ -+ private func initEmptyActivity(settings: LiveActivitySettings) { -+ do { -+ let dynamicState = GlucoseActivityAttributes.ContentState( -+ date: Date.now, -+ ended: true, -+ preset: nil, -+ glucoseRanges: [], -+ currentGlucose: 0, -+ trendType: nil, -+ delta: "", -+ isMmol: true, -+ isCloseLoop: false, -+ lastCompleted: nil, -+ bottomRow: [], -+ glucoseSamples: [], -+ predicatedGlucose: [], -+ predicatedStartDate: nil, -+ predicatedInterval: nil, -+ yAxisMarks: [] -+ ) -+ -+ self.activity = try Activity.request( -+ attributes: GlucoseActivityAttributes( -+ mode: settings.mode, -+ addPredictiveLine: settings.addPredictiveLine, -+ useLimits: settings.useLimits, -+ upperLimitChartMmol: settings.upperLimitChartMmol, -+ lowerLimitChartMmol: settings.lowerLimitChartMmol, -+ upperLimitChartMg: settings.upperLimitChartMg, -+ lowerLimitChartMg: settings.lowerLimitChartMg -+ ), -+ content: .init(state: dynamicState, staleDate: nil), -+ pushType: .token -+ ) -+ } catch { -+ print("ERROR: Error while creating empty live activity: \(error.localizedDescription)") -+ } -+ } -+} -+ -+extension TemporaryScheduleOverride { -+ func getTitle() -> String { -+ switch (self.context) { -+ case .preset(let preset): -+ return "\(preset.symbol) \(preset.name)" -+ case .custom: -+ return NSLocalizedString("Custom preset", comment: "The title of the cell indicating a generic custom preset is enabled") -+ case .preMeal: -+ return NSLocalizedString(" Pre-meal Preset", comment: "Status row title for premeal override enabled (leading space is to separate from symbol)") -+ case .legacyWorkout: -+ return "" -+ } -+ } -+} -diff --git a/Loop/Loop/Managers/LoopDataManager.swift b/Loop/Loop/Managers/LoopDataManager.swift -index 2319f4ec..5aa9c4fd 100644 ---- a/Loop/Loop/Managers/LoopDataManager.swift -+++ b/Loop/Loop/Managers/LoopDataManager.swift -@@ -68,6 +68,8 @@ final class LoopDataManager { - private var timeBasedDoseApplicationFactor: Double = 1.0 - - private var insulinOnBoard: InsulinValue? -+ -+ private var liveActivityManager: GlucoseActivityManager? - - deinit { - for observer in notificationObservers { -@@ -124,6 +126,12 @@ final class LoopDataManager { - self.automaticDosingStatus = automaticDosingStatus - - self.trustedTimeOffset = trustedTimeOffset -+ -+ self.liveActivityManager = GlucoseActivityManager( -+ glucoseStore: self.glucoseStore, -+ doseStore: self.doseStore, -+ loopSettings: self.settings -+ ) - - overrideIntentObserver = UserDefaults.appGroup?.observe(\.intentExtensionOverrideToSet, options: [.new], changeHandler: {[weak self] (defaults, change) in - guard let name = change.newValue??.lowercased(), let appGroup = UserDefaults.appGroup else { -@@ -144,12 +152,14 @@ final class LoopDataManager { - } - } - } -+ - settings.scheduleOverride = preset.createOverride(enactTrigger: .remote("Siri")) - if let observers = self?.presetActivationObservers { - for observer in observers { - observer.presetActivated(context: .preset(preset), duration: preset.duration) - } - } -+ self?.liveActivityManager?.update(loopSettings: settings) - } - // Remove the override from UserDefaults so we don't set it multiple times - appGroup.intentExtensionOverrideToSet = nil -@@ -167,6 +177,7 @@ final class LoopDataManager { - ) { (note) -> Void in - self.dataAccessQueue.async { - self.logger.default("Received notification of carb entries changing") -+ self.liveActivityManager?.update(loopSettings: self.settings) - - self.carbEffect = nil - self.carbsOnBoard = nil -@@ -182,7 +193,8 @@ final class LoopDataManager { - ) { (note) in - self.dataAccessQueue.async { - self.logger.default("Received notification of glucose samples changing") -- -+ self.liveActivityManager?.update(loopSettings: self.settings) -+ - self.glucoseMomentumEffect = nil - self.remoteRecommendationNeedsUpdating = true - -@@ -196,6 +208,7 @@ final class LoopDataManager { - ) { (note) in - self.dataAccessQueue.async { - self.logger.default("Received notification of dosing changing") -+ self.liveActivityManager?.update(loopSettings: self.settings) - - self.clearCachedInsulinEffects() - self.remoteRecommendationNeedsUpdating = true -@@ -247,6 +260,8 @@ final class LoopDataManager { - if newValue.preMealOverride != oldValue.preMealOverride { - // The prediction isn't actually invalid, but a target range change requires recomputing recommended doses - predictedGlucose = nil -+ -+ self.liveActivityManager?.update(loopSettings: newValue) - } - - if newValue.scheduleOverride != oldValue.scheduleOverride { -@@ -256,12 +271,14 @@ final class LoopDataManager { - for observer in self.presetActivationObservers { - observer.presetDeactivated(context: oldPreset.context) - } -- -+ self.liveActivityManager?.update(loopSettings: newValue) - } - if let newPreset = newValue.scheduleOverride { - for observer in self.presetActivationObservers { - observer.presetActivated(context: newPreset.context, duration: newPreset.duration) - } -+ -+ self.liveActivityManager?.update(loopSettings: newValue) - } - - // Invalidate cached effects affected by the override -@@ -358,6 +375,13 @@ final class LoopDataManager { - predictedGlucose = nil - } - } -+ -+ private var negativeInsulinDamper: Double? { -+ didSet { -+ predictedGlucose = nil -+ } -+ } -+ private var negativeInsulinDamperCachedBaseDate: Date = .distantPast - - /// When combining retrospective glucose discrepancies, extend the window slightly as a buffer. - private let retrospectiveCorrectionGroupingIntervalMultiplier = 1.01 -@@ -454,6 +478,7 @@ final class LoopDataManager { - insulinEffect = nil - insulinEffectIncludingPendingInsulin = nil - predictedGlucose = nil -+ negativeInsulinDamper = nil - } - - // MARK: - Background task management -@@ -995,6 +1020,64 @@ extension LoopDataManager { - let earliestEffectDate = Date(timeInterval: .hours(-24), since: now()) - let nextCounteractionEffectDate = insulinCounteractionEffects.last?.endDate ?? earliestEffectDate - let insulinEffectStartDate = nextCounteractionEffectDate.addingTimeInterval(.minutes(-5)) -+ -+ if negativeInsulinDamper == nil || nextCounteractionEffectDate != negativeInsulinDamperCachedBaseDate { -+ self.logger.debug("Recomputing negative insulin damper") -+ updateGroup.enter() -+ let lastDoseStartDate = nextCounteractionEffectDate.addingTimeInterval(.minutes(-15)) -+ doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, doseEnd: lastDoseStartDate, basalDosingEnd: lastDoseStartDate) { (result) -> Void in -+ switch result { -+ case .failure(let error): -+ self.logger.error("Could not fetch insulin effects for damper: %{public}@", error.localizedDescription) -+ self.negativeInsulinDamper = nil -+ self.negativeInsulinDamperCachedBaseDate = .distantPast -+ warnings.append(.fetchDataWarning(.negativeInsulinDamper(error: error))) -+ case .success(let effects): -+ var posDeltaSum = 0.0 -+ effects.enumerated().forEach{ -+ if $0.offset > 0 { -+ let delta = $0.element.quantity.doubleValue(for: .milligramsPerDeciliter) - effects[$0.offset - 1].quantity.doubleValue(for: .milligramsPerDeciliter) -+ posDeltaSum += max(0, delta) -+ } -+ } -+ -+ guard let insulinSensitivity = latestSettings.insulinSensitivitySchedule?.quantity(at: lastGlucoseDate), let basalRate = latestSettings.basalRateSchedule?.value(at: lastGlucoseDate) else { -+ -+ self.logger.error("Could not fetch ISF and/or basal rates for damper") -+ self.negativeInsulinDamper = nil -+ self.negativeInsulinDamperCachedBaseDate = .distantPast -+ -+ break -+ } -+ let model = self.doseStore.insulinModelProvider.model(for: self.pumpInsulinType) -+ -+ // anchorScale is set to 1 hour for rapid acting adult, and 44 minutes for ultra-rapid insulins -+ let anchorScale: Double -+ if let expModel = model as? ExponentialInsulinModel { -+ anchorScale = 0.8 * expModel.peakActivityTime.hours -+ } else { -+ anchorScale = 1.0 -+ } -+ -+ // NID will change the final prediction so that positive changes will be multiplied by weight alpha -+ // the long term slope will be marginalSlope -+ // in the initial linear scaling region alpha will be anchorAlpha at anchorPoint -+ // note that anchorPoint is unaffected by overrides (the changes cancel out) -+ let marginalSlope = 0.05 -+ let anchorPoint = anchorScale * basalRate * insulinSensitivity.doubleValue(for: .milligramsPerDeciliter) -+ let anchorAlpha = 0.75 -+ -+ let alpha = LoopDataManager.calculateNegativeInsulinDamperAlpha(anchorAlpha, anchorPoint, marginalSlope, posDeltaSum) -+ -+ // alpha should never be less than marginalSlope -+ self.negativeInsulinDamper = max(0, 1 - max(marginalSlope, alpha)) -+ self.negativeInsulinDamperCachedBaseDate = nextCounteractionEffectDate -+ } -+ -+ updateGroup.leave() -+ } -+ -+ } - - if glucoseMomentumEffect == nil { - updateGroup.enter() -@@ -1014,7 +1097,8 @@ extension LoopDataManager { - if insulinEffect == nil || insulinEffect?.first?.startDate ?? .distantFuture > insulinEffectStartDate { - self.logger.debug("Recomputing insulin effects") - updateGroup.enter() -- doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, basalDosingEnd: now()) { (result) -> Void in -+ let basalDosingEnd = now() -+ doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, doseEnd: nil, basalDosingEnd: basalDosingEnd) { (result) -> Void in - switch result { - case .failure(let error): - self.logger.error("Could not fetch insulin effects: %{public}@", error.localizedDescription) -@@ -1030,7 +1114,7 @@ extension LoopDataManager { - - if insulinEffectIncludingPendingInsulin == nil { - updateGroup.enter() -- doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, basalDosingEnd: nil) { (result) -> Void in -+ doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, doseEnd: nil, basalDosingEnd: nil) { (result) -> Void in - switch result { - case .failure(let error): - self.logger.error("Could not fetch insulin effects including pending insulin: %{public}@", error.localizedDescription) -@@ -1158,6 +1242,21 @@ extension LoopDataManager { - - return updatePredictedGlucoseAndRecommendedDose(with: dosingDecision) - } -+ -+ static func calculateNegativeInsulinDamperAlpha(_ anchorAlpha: Double, _ anchorPoint: Double, _ marginalSlope: Double, _ posDeltaSum: Double) -> Double { -+ let linearScaleSlope = (1.0 - anchorAlpha)/anchorPoint // how alpha scales down in the linear scale region -+ -+ // the slope in the linear scale region of alpha * posDeltaSum is 1 - 2*linearScaleSlope*posDeltaSum. -+ // the transitionPoint is where we transition from linear scale region to marginalSlope. The slope is continuous at this point -+ let transitionPoint = (1 - marginalSlope) / (2 * linearScaleSlope) -+ -+ if posDeltaSum < transitionPoint { // linear scaling region -+ return 1 - linearScaleSlope * posDeltaSum -+ } else { // marginal slope region -+ let transitionValue = (1 - linearScaleSlope * transitionPoint) * transitionPoint -+ return (transitionValue + marginalSlope * (posDeltaSum - transitionPoint)) / posDeltaSum -+ } -+ } - - private func notify(forChange context: LoopUpdateContext) { - NotificationCenter.default.post(name: .LoopDataUpdated, -@@ -1199,7 +1298,7 @@ extension LoopDataManager { - // All outstanding potential insulin delivery - return pendingTempBasalInsulin + pendingBolusAmount - } -- -+ - /// - Throws: - /// - LoopError.missingDataError - /// - LoopError.configurationError -@@ -1337,6 +1436,51 @@ extension LoopDataManager { - } - - var prediction = LoopMath.predictGlucose(startingAt: glucose, momentum: momentum, effects: effects) -+ -+ if inputs.contains(.damper), let damper = negativeInsulinDamper { -+ let damperOnly = inputs.isSubset(of: [.damper]) -+ if damperOnly { -+ prediction = try predictGlucose( -+ startingAt: startingGlucoseOverride, -+ using: settings.enabledEffects.subtracting(.damper), -+ historicalInsulinEffect: insulinEffectOverride, -+ insulinCounteractionEffects: insulinCounteractionEffectsOverride, -+ historicalCarbEffect: carbEffectOverride, -+ potentialBolus: potentialBolus, -+ potentialCarbEntry: potentialCarbEntry, -+ replacingCarbEntry: replacedCarbEntry, -+ includingPendingInsulin: includingPendingInsulin, -+ includingPositiveVelocityAndRC: includingPositiveVelocityAndRC) -+ } -+ -+ let alpha = 1 - damper -+ var dampedPrediction = [PredictedGlucoseValue]() -+ var value = 0.0 -+ prediction.enumerated().forEach{ -+ -+ if $0.offset == 0 { -+ value = $0.element.quantity.doubleValue(for: .milligramsPerDeciliter) -+ dampedPrediction.append($0.element) -+ return -+ } -+ let delta = $0.element.quantity.doubleValue(for: .milligramsPerDeciliter) - prediction[$0.offset - 1].quantity.doubleValue(for: .milligramsPerDeciliter) -+ -+ if damperOnly { -+ // we just want to display the effects of damper relative to everything else -+ if delta > 0 { -+ value -= damper * delta -+ } -+ } else if delta > 0 { -+ value += alpha * delta -+ } else { -+ value += delta -+ } -+ dampedPrediction.append(PredictedGlucoseValue(startDate: $0.element.startDate, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: value))) -+ } -+ -+ prediction = dampedPrediction -+ } -+ - - // Dosing requires prediction entries at least as long as the insulin model duration. - // If our prediction is shorter than that, then extend it here. -@@ -1367,7 +1511,7 @@ extension LoopDataManager { - var insulinEffect: [GlucoseEffect]? - let basalDosingEnd = includingPendingInsulin ? nil : now() - updateGroup.enter() -- doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, basalDosingEnd: basalDosingEnd) { result in -+ doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, doseEnd: nil, basalDosingEnd: basalDosingEnd) { result in - switch result { - case .failure(let error): - effectCalculationError.mutate { $0 = error } -@@ -1955,6 +2099,9 @@ protocol LoopState { - - /// The total corrective glucose effect from retrospective correction - var totalRetrospectiveCorrection: HKQuantity? { get } -+ -+ /// The negative insulin damper - if present then is in the range [0,1] -+ var negativeInsulinDamper: Double? { get} - - /// Calculates a new prediction from the current data using the specified effect inputs - /// -@@ -2079,6 +2226,11 @@ extension LoopDataManager { - dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) - return loopDataManager.retrospectiveCorrection.totalGlucoseCorrectionEffect - } -+ -+ var negativeInsulinDamper: Double? { -+ dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) -+ return loopDataManager.negativeInsulinDamper -+ } - - func predictGlucose(using inputs: PredictionInputEffect, potentialBolus: DoseEntry?, potentialCarbEntry: NewCarbEntry?, replacingCarbEntry replacedCarbEntry: StoredCarbEntry?, includingPendingInsulin: Bool, considerPositiveVelocityAndRC: Bool) throws -> [PredictedGlucoseValue] { - dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) -diff --git a/Loop/Loop/Managers/Store Protocols/DoseStoreProtocol.swift b/Loop/Loop/Managers/Store Protocols/DoseStoreProtocol.swift -index dd21ea2a..2887d14c 100644 ---- a/Loop/Loop/Managers/Store Protocols/DoseStoreProtocol.swift -+++ b/Loop/Loop/Managers/Store Protocols/DoseStoreProtocol.swift -@@ -50,7 +50,7 @@ protocol DoseStoreProtocol: AnyObject { - // MARK: IOB and insulin effect - func insulinOnBoard(at date: Date, completion: @escaping (_ result: DoseStoreResult) -> Void) - -- func getGlucoseEffects(start: Date, end: Date?, basalDosingEnd: Date?, completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void) -+ func getGlucoseEffects(start: Date, end: Date?, doseEnd: Date?, basalDosingEnd: Date?, completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void) - - func getInsulinOnBoardValues(start: Date, end: Date? , basalDosingEnd: Date?, completion: @escaping (_ result: DoseStoreResult<[InsulinValue]>) -> Void) - -diff --git a/Loop/Loop/Models/LoopSettings+Loop.swift b/Loop/Loop/Models/LoopSettings+Loop.swift -index e4952934..ba296f1d 100644 ---- a/Loop/Loop/Models/LoopSettings+Loop.swift -+++ b/Loop/Loop/Models/LoopSettings+Loop.swift -@@ -15,6 +15,9 @@ extension LoopSettings { - if !LoopConstants.retrospectiveCorrectionEnabled { - inputs.remove(.retrospection) - } -+ if !UserDefaults.standard.negativeInsulinDamperEnabled { -+ inputs.remove(.damper) -+ } - return inputs - } - } -diff --git a/Loop/Loop/Models/LoopWarning.swift b/Loop/Loop/Models/LoopWarning.swift -index 45439b3c..d43a7d80 100644 ---- a/Loop/Loop/Models/LoopWarning.swift -+++ b/Loop/Loop/Models/LoopWarning.swift -@@ -14,6 +14,7 @@ enum FetchDataWarningDetail { - case glucoseMomentumEffect(error: Error) - case insulinEffect(error: Error) - case insulinEffectIncludingPendingInsulin(error: Error) -+ case negativeInsulinDamper(error: Error) - case insulinCounteractionEffect(error: Error) - case carbEffect(error: Error) - case carbsOnBoard(error: Error) -@@ -32,6 +33,8 @@ extension FetchDataWarningDetail { - return "insulinEffect" - case .insulinEffectIncludingPendingInsulin: - return "insulinEffectIncludingPendingInsulin" -+ case .negativeInsulinDamper: -+ return "negativeInsulinDamper" - case .insulinCounteractionEffect: - return "insulinCounteractionEffect" - case .carbEffect: -@@ -53,6 +56,7 @@ extension FetchDataWarningDetail { - .insulinEffect(let error), - .insulinEffectIncludingPendingInsulin(let error), - .insulinCounteractionEffect(let error), -+ .negativeInsulinDamper(let error), - .carbEffect(let error), - .carbsOnBoard(let error), - .insulinOnBoard(let error), -diff --git a/Loop/Loop/Models/PredictionInputEffect.swift b/Loop/Loop/Models/PredictionInputEffect.swift -index 45fb5ea0..c80cde1d 100644 ---- a/Loop/Loop/Models/PredictionInputEffect.swift -+++ b/Loop/Loop/Models/PredictionInputEffect.swift -@@ -18,8 +18,9 @@ struct PredictionInputEffect: OptionSet { - static let momentum = PredictionInputEffect(rawValue: 1 << 2) - static let retrospection = PredictionInputEffect(rawValue: 1 << 3) - static let suspend = PredictionInputEffect(rawValue: 1 << 4) -+ static let damper = PredictionInputEffect(rawValue: 1 << 5) - -- static let all: PredictionInputEffect = [.carbs, .insulin, .momentum, .retrospection] -+ static let all: PredictionInputEffect = [.carbs, .insulin, .damper, .momentum, .retrospection] - - var localizedTitle: String? { - switch self { -@@ -27,6 +28,8 @@ struct PredictionInputEffect: OptionSet { - return NSLocalizedString("Carbohydrates", comment: "Title of the prediction input effect for carbohydrates") - case [.insulin]: - return NSLocalizedString("Insulin", comment: "Title of the prediction input effect for insulin") -+ case [.damper]: -+ return NSLocalizedString("Negative Insulin Damper", comment: "Title of the prediction input effect for negative insulin damper") - case [.momentum]: - return NSLocalizedString("Glucose Momentum", comment: "Title of the prediction input effect for glucose momentum") - case [.retrospection]: -@@ -44,6 +47,8 @@ struct PredictionInputEffect: OptionSet { - return String(format: NSLocalizedString("Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)", comment: "Description of the prediction input effect for carbohydrates. (1: The glucose unit string)"), unit.localizedShortUnitString) - case [.insulin]: - return String(format: NSLocalizedString("Insulin Absorbed (U) × Insulin Sensitivity (%1$@/U)", comment: "Description of the prediction input effect for insulin"), unit.localizedShortUnitString) -+ case [.damper]: -+ return String(format: NSLocalizedString("Reduces increases in glucose. The damper is stronger when there is more negative insulin", comment: "Description of the prediction input effect for negative insulin damper"), unit.localizedShortUnitString) - case [.momentum]: - return NSLocalizedString("15 min glucose regression coefficient (b₁), continued with decay over 30 min", comment: "Description of the prediction input effect for glucose momentum") - case [.retrospection]: -diff --git a/Loop/Loop/View Controllers/PredictionTableViewController.swift b/Loop/Loop/View Controllers/PredictionTableViewController.swift -index a460e52a..fc477e7e 100644 ---- a/Loop/Loop/View Controllers/PredictionTableViewController.swift -+++ b/Loop/Loop/View Controllers/PredictionTableViewController.swift -@@ -71,6 +71,8 @@ class PredictionTableViewController: LoopChartsTableViewController, Identifiable - private var retrospectiveGlucoseDiscrepancies: [GlucoseChange]? - - private var totalRetrospectiveCorrection: HKQuantity? -+ -+ private var negativeInsulinDamper: Double? - - private var refreshContext = RefreshContext.all - -@@ -111,6 +113,7 @@ class PredictionTableViewController: LoopChartsTableViewController, Identifiable - let reloadGroup = DispatchGroup() - var glucoseSamples: [StoredGlucoseSample]? - var totalRetrospectiveCorrection: HKQuantity? -+ var negativeInsulinDamper: Double? - - if self.refreshContext.remove(.glucose) != nil { - reloadGroup.enter() -@@ -132,6 +135,7 @@ class PredictionTableViewController: LoopChartsTableViewController, Identifiable - deviceManager.loopManager.getLoopState { (manager, state) in - self.retrospectiveGlucoseDiscrepancies = state.retrospectiveGlucoseDiscrepancies - totalRetrospectiveCorrection = state.totalRetrospectiveCorrection -+ negativeInsulinDamper = state.negativeInsulinDamper - self.glucoseChart.setPredictedGlucoseValues(state.predictedGlucoseIncludingPendingInsulin ?? []) - - do { -@@ -164,6 +168,9 @@ class PredictionTableViewController: LoopChartsTableViewController, Identifiable - if let totalRetrospectiveCorrection = totalRetrospectiveCorrection { - self.totalRetrospectiveCorrection = totalRetrospectiveCorrection - } -+ if let negativeInsulinDamper = negativeInsulinDamper { -+ self.negativeInsulinDamper = negativeInsulinDamper -+ } - - self.charts.prerender() - -@@ -197,9 +204,16 @@ class PredictionTableViewController: LoopChartsTableViewController, Identifiable - - private var eventualGlucoseDescription: String? - -- private var availableInputs: [PredictionInputEffect] = [.carbs, .insulin, .momentum, .retrospection, .suspend] -+ private var availableInputs: [PredictionInputEffect] = getAvailableInputs() - - private var selectedInputs = PredictionInputEffect.all -+ -+ private static func getAvailableInputs() -> [PredictionInputEffect] { -+ if UserDefaults.standard.negativeInsulinDamperEnabled { -+ return [.carbs, .insulin, .damper, .momentum, .retrospection, .suspend] -+ } -+ return [.carbs, .insulin, .momentum, .retrospection, .suspend] -+ } - - override func numberOfSections(in tableView: UITableView) -> Int { - return Section.allCases.count -@@ -261,6 +275,20 @@ class PredictionTableViewController: LoopChartsTableViewController, Identifiable - - var subtitleText = input.localizedDescription(forGlucoseUnit: glucoseChart.glucoseUnit) ?? "" - -+ if input == .damper, let negativeInsulinDamper = negativeInsulinDamper { -+ let formatter = NumberFormatter() -+ formatter.minimumIntegerDigits = 1 -+ formatter.maximumFractionDigits = 1 -+ formatter.maximumSignificantDigits = 2 -+ -+ let damper = String( -+ format: NSLocalizedString("Damper Strength: %1$@%%", comment: "Format string describing damper strength. (1: damper strength percentage)"), -+ formatter.string(from: 100 * negativeInsulinDamper) ?? "?" -+ ) -+ -+ subtitleText = String(format: "%@\n%@", subtitleText, damper) -+ -+ } - if input == .retrospection, - let lastDiscrepancy = retrospectiveGlucoseDiscrepancies?.last, - let currentGlucose = deviceManager.glucoseStore.latestGlucose -diff --git a/Loop/Loop/View Controllers/StatusTableViewController.swift b/Loop/Loop/View Controllers/StatusTableViewController.swift -index 6a4aadfc..0e14f216 100644 ---- a/Loop/Loop/View Controllers/StatusTableViewController.swift -+++ b/Loop/Loop/View Controllers/StatusTableViewController.swift -@@ -240,10 +240,16 @@ final class StatusTableViewController: LoopChartsTableViewController { - didSet { - if oldValue != bolusState { - switch bolusState { -- case .inProgress(_): -+ case .inProgress(let dose): - guard case .inProgress = oldValue else { - // Bolus starting - bolusProgressReporter = deviceManager.pumpManager?.createBolusProgressReporter(reportingOn: DispatchQueue.main) -+ // If there is an existing bolus progressCell, update its dose values now in case the app is currently in the -+ // background as otherwise these values won't get initialized and can contain stale data from some earlier bolus. -+ if let progressCell = tableView.cellForRow(at: IndexPath(row: StatusRow.status.rawValue, section: Section.status.rawValue)) as? BolusProgressTableViewCell { -+ progressCell.totalUnits = dose.programmedUnits -+ progressCell.deliveredUnits = 0 -+ } - break - } - default: -diff --git a/Loop/Loop/View Models/LiveActivityManagementViewModel.swift b/Loop/Loop/View Models/LiveActivityManagementViewModel.swift -new file mode 100644 -index 00000000..46fc560d ---- /dev/null -+++ b/Loop/Loop/View Models/LiveActivityManagementViewModel.swift -@@ -0,0 +1,35 @@ -+// -+// LiveActivityManagementViewModel.swift -+// Loop -+// -+// Created by Bastiaan Verhaar on 12/09/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import Foundation -+import LoopCore -+ -+class LiveActivityManagementViewModel : ObservableObject { -+ @Published var enabled: Bool -+ @Published var mode: LiveActivityMode -+ @Published var isEditingMode: Bool = false -+ @Published var addPredictiveLine: Bool -+ @Published var useLimits: Bool -+ @Published var upperLimitChartMmol: Double -+ @Published var lowerLimitChartMmol: Double -+ @Published var upperLimitChartMg: Double -+ @Published var lowerLimitChartMg: Double -+ -+ init() { -+ let liveActivitySettings = UserDefaults.standard.liveActivity ?? LiveActivitySettings() -+ -+ self.enabled = liveActivitySettings.enabled -+ self.mode = liveActivitySettings.mode -+ self.addPredictiveLine = liveActivitySettings.addPredictiveLine -+ self.useLimits = liveActivitySettings.useLimits -+ self.upperLimitChartMmol = liveActivitySettings.upperLimitChartMmol -+ self.lowerLimitChartMmol = liveActivitySettings.lowerLimitChartMmol -+ self.upperLimitChartMg = liveActivitySettings.upperLimitChartMg -+ self.lowerLimitChartMg = liveActivitySettings.lowerLimitChartMg -+ } -+} -diff --git a/Loop/Loop/Views/AlertManagementView.swift b/Loop/Loop/Views/AlertManagementView.swift -index e9a38e72..94e542a6 100644 ---- a/Loop/Loop/Views/AlertManagementView.swift -+++ b/Loop/Loop/Views/AlertManagementView.swift -@@ -7,8 +7,10 @@ - // - - import SwiftUI -+import LoopCore - import LoopKit - import LoopKitUI -+import HealthKit - - struct AlertManagementView: View { - @Environment(\.appName) private var appName -@@ -157,6 +159,11 @@ struct AlertManagementView: View { - } - } - } -+ -+ NavigationLink(destination: LiveActivityManagementView()) -+ { -+ Text(NSLocalizedString("Live activity", comment: "Alert Permissions live activity")) -+ } - } - } - -diff --git a/Loop/Loop/Views/LiveActivityBottomRowManagerView.swift b/Loop/Loop/Views/LiveActivityBottomRowManagerView.swift -new file mode 100644 -index 00000000..49e50caa ---- /dev/null -+++ b/Loop/Loop/Views/LiveActivityBottomRowManagerView.swift -@@ -0,0 +1,117 @@ -+// -+// LiveActivityBottomRowManagerView.swift -+// Loop -+// -+// Created by Bastiaan Verhaar on 06/07/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import LoopKitUI -+import LoopCore -+import SwiftUI -+ -+struct LiveActivityBottomRowManagerView: View { -+ @Environment(\.presentationMode) var presentationMode: Binding -+ -+ // The maximum items in the bottom row -+ private let maxSize = 4 -+ -+ @State var showAdd: Bool = false -+ @State var configuration: [BottomRowConfiguration] = (UserDefaults.standard.liveActivity ?? LiveActivitySettings()).bottomRowConfiguration -+ -+ var addItem: ActionSheet { -+ var buttons: [ActionSheet.Button] = BottomRowConfiguration.all.map { item in -+ ActionSheet.Button.default(Text(item.description())) { -+ configuration.append(item) -+ } -+ } -+ buttons.append(.cancel(Text(NSLocalizedString("Cancel", comment: "Button text to cancel")))) -+ -+ return ActionSheet(title: Text(NSLocalizedString("Add item to bottom row", comment: "Title for Add item")), buttons: buttons) -+ } -+ -+ var body: some View { -+ List { -+ ForEach($configuration, id: \.self) { item in -+ HStack { -+ deleteButton -+ .onTapGesture { -+ onDelete(item.wrappedValue) -+ } -+ Text(item.wrappedValue.description()) -+ -+ Spacer() -+ editBars -+ } -+ } -+ .onMove(perform: onReorder) -+ .deleteDisabled(true) -+ -+ Section { -+ Button(action: onSave) { -+ Text(NSLocalizedString("Save", comment: "")) -+ } -+ .buttonStyle(ActionButtonStyle()) -+ .listRowInsets(EdgeInsets()) -+ } -+ } -+ .toolbar { -+ ToolbarItem(placement: .navigationBarTrailing) { -+ Button( -+ action: { showAdd = true }, -+ label: { Image(systemName: "plus") } -+ ) -+ .disabled(configuration.count >= self.maxSize) -+ } -+ } -+ .actionSheet(isPresented: $showAdd, content: { addItem }) -+ .insetGroupedListStyle() -+ .navigationBarTitle(Text(NSLocalizedString("Bottom row", comment: "Live activity Bottom row configuration title"))) -+ } -+ -+ @ViewBuilder -+ private var deleteButton: some View { -+ ZStack { -+ Color.red -+ .clipShape(RoundedRectangle(cornerRadius: 12.5)) -+ .frame(width: 20, height: 20) -+ -+ Image(systemName: "minus") -+ .foregroundColor(.white) -+ } -+ .contentShape(Rectangle()) -+ } -+ -+ @ViewBuilder -+ private var editBars: some View { -+ Image(systemName: "line.3.horizontal") -+ .foregroundColor(Color(UIColor.tertiaryLabel)) -+ .font(.title2) -+ } -+ -+ private func onSave() { -+ var settings = UserDefaults.standard.liveActivity ?? LiveActivitySettings() -+ settings.bottomRowConfiguration = configuration -+ -+ UserDefaults.standard.liveActivity = settings -+ NotificationCenter.default.post(name: .LiveActivitySettingsChanged, object: settings) -+ -+ self.presentationMode.wrappedValue.dismiss() -+ } -+ -+ func onReorder(from: IndexSet, to: Int) { -+ withAnimation { -+ configuration.move(fromOffsets: from, toOffset: to) -+ } -+ } -+ -+ func onDelete(_ item: BottomRowConfiguration) { -+ withAnimation { -+ _ = configuration.remove(item) -+ } -+ } -+} -+ -+#Preview { -+ LiveActivityBottomRowManagerView() -+} -diff --git a/Loop/Loop/Views/LiveActivityManagementView.swift b/Loop/Loop/Views/LiveActivityManagementView.swift -new file mode 100644 -index 00000000..f7f875ca ---- /dev/null -+++ b/Loop/Loop/Views/LiveActivityManagementView.swift -@@ -0,0 +1,113 @@ -+// -+// LiveActivityManagementView.swift -+// Loop -+// -+// Created by Bastiaan Verhaar on 04/07/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import SwiftUI -+import LoopKitUI -+import LoopCore -+import HealthKit -+ -+struct LiveActivityManagementView: View { -+ @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference -+ @StateObject private var viewModel = LiveActivityManagementViewModel() -+ -+ var body: some View { -+ VStack { -+ List { -+ Section { -+ Toggle(NSLocalizedString("Enabled", comment: "Title for enable live activity toggle"), isOn: $viewModel.enabled) -+ -+ ExpandableSetting( -+ isEditing: $viewModel.isEditingMode, -+ leadingValueContent: { -+ Text(NSLocalizedString("Mode", comment: "Title for mode live activity toggle")) -+ .foregroundStyle(viewModel.isEditingMode ? .blue : .primary) -+ }, -+ trailingValueContent: { -+ Text(viewModel.mode.name()) -+ .foregroundStyle(viewModel.isEditingMode ? .blue : .primary) -+ }, -+ expandedContent: { -+ ResizeablePicker(selection: self.$viewModel.mode.animation(), -+ data: LiveActivityMode.all, -+ formatter: { $0.name() }) -+ } -+ ) -+ } -+ -+ Section { -+ if viewModel.mode == .large { -+ Toggle(NSLocalizedString("Add predictive line", comment: "Title for predictive line toggle"), isOn: $viewModel.addPredictiveLine) -+ .transition(.move(edge: viewModel.mode == .large ? .top : .bottom)) -+ } -+ -+ Toggle(NSLocalizedString("Use BG coloring", comment: "Title for BG coloring"), isOn: $viewModel.useLimits) -+ .transition(.move(edge: viewModel.mode == .large ? .top : .bottom)) -+ -+ if viewModel.useLimits { -+ if self.displayGlucosePreference.unit == .millimolesPerLiter { -+ TextInput(label: "Upper limit", value: $viewModel.upperLimitChartMmol) -+ .transition(.move(edge: viewModel.useLimits ? .top : .bottom)) -+ TextInput(label: "Lower limit", value: $viewModel.lowerLimitChartMmol) -+ .transition(.move(edge: viewModel.useLimits ? .top : .bottom)) -+ } else { -+ TextInput(label: "Upper limit", value: $viewModel.upperLimitChartMg) -+ .transition(.move(edge: viewModel.useLimits ? .top : .bottom)) -+ TextInput(label: "Lower limit", value: $viewModel.lowerLimitChartMg) -+ .transition(.move(edge: viewModel.useLimits ? .top : .bottom)) -+ } -+ } -+ } -+ -+ Section { -+ NavigationLink( -+ destination: LiveActivityBottomRowManagerView(), -+ label: { Text(NSLocalizedString("Bottom row configuration", comment: "Title for Bottom row configuration")) } -+ ) -+ } -+ -+ -+ } -+ .animation(.easeInOut, value: UUID()) -+ .insetGroupedListStyle() -+ -+ Spacer() -+ Button(action: save) { -+ Text(NSLocalizedString("Save", comment: "")) -+ } -+ .buttonStyle(ActionButtonStyle()) -+ .padding([.bottom, .horizontal]) -+ } -+ .navigationBarTitle(Text(NSLocalizedString("Live activity", comment: "Live activity screen title"))) -+ } -+ -+ @ViewBuilder -+ private func TextInput(label: String, value: Binding) -> some View { -+ HStack { -+ Text(NSLocalizedString(label, comment: "no comment")) -+ Spacer() -+ TextField("", value: value, format: .number) -+ .multilineTextAlignment(.trailing) -+ Text(self.displayGlucosePreference.unit.localizedShortUnitString) -+ } -+ } -+ -+ private func save() { -+ var settings = UserDefaults.standard.liveActivity ?? LiveActivitySettings() -+ settings.enabled = viewModel.enabled -+ settings.mode = viewModel.mode -+ settings.addPredictiveLine = viewModel.addPredictiveLine -+ settings.useLimits = viewModel.useLimits -+ settings.upperLimitChartMmol = viewModel.upperLimitChartMmol -+ settings.lowerLimitChartMmol = viewModel.lowerLimitChartMmol -+ settings.upperLimitChartMg = viewModel.upperLimitChartMg -+ settings.lowerLimitChartMg = viewModel.lowerLimitChartMg -+ -+ UserDefaults.standard.liveActivity = settings -+ NotificationCenter.default.post(name: .LiveActivitySettingsChanged, object: settings) -+ } -+} -diff --git a/Loop/Loop/Views/NegativeInsulinDamperSelectionView.swift b/Loop/Loop/Views/NegativeInsulinDamperSelectionView.swift -new file mode 100644 -index 00000000..97929ca8 ---- /dev/null -+++ b/Loop/Loop/Views/NegativeInsulinDamperSelectionView.swift -@@ -0,0 +1,45 @@ -+// -+// NegativeInsulinDamperSelectionView.swift -+// Loop -+// -+// Created by Moti Nisenson-Ken on 16/10/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+import Foundation -+import SwiftUI -+import LoopKit -+import LoopKitUI -+ -+struct NegativeInsulinDamperSelectionView: View { -+ @Binding var isNegativeInsulinDamperEnabled: Bool -+ -+ public var body: some View { -+ ScrollView { -+ VStack(spacing: 10) { -+ Text(NSLocalizedString("Negative Insulin Damper", comment: "Title for negative insulin damper experiment description")) -+ .font(.headline) -+ .padding(.bottom, 20) -+ -+ Divider() -+ -+ Text(NSLocalizedString("Negative Insulin Damper (NID) is used to mitigate the effects of temporarily increased insulin sensitivity. Such increases can result in spending significant times beneath target and eventually going low. Loop may erroneously predict glucose going too high, resulting in excess insulin being delivered. To counteract this, NID acts as a dynamic damper on increases to predicted glucose. The strength of this damper is controlled by the total predicted rise in glucose due to negative insulin. The greater the amount of negative insulin, the stronger the damper and the bigger the reductions. The calculation is done with a 15 minute lag.", comment: "Description of Negative Insulin Damper toggle.")) -+ .foregroundColor(.secondary) -+ Divider() -+ -+ Toggle(NSLocalizedString("Enable Negative Insulin Damper", comment: "Title for Negative Insulin Damper toggle"), isOn: $isNegativeInsulinDamperEnabled) -+ .onChange(of: isNegativeInsulinDamperEnabled) { newValue in -+ UserDefaults.standard.negativeInsulinDamperEnabled = newValue -+ } -+ .padding(.top, 20) -+ } -+ .padding() -+ } -+ .navigationBarTitleDisplayMode(.inline) -+ } -+ -+ struct NegativeInsulinDamperSelectionView_Previews: PreviewProvider { -+ static var previews: some View { -+ NegativeInsulinDamperSelectionView(isNegativeInsulinDamperEnabled: .constant(true)) -+ } -+ } -+} -diff --git a/Loop/Loop/Views/SettingsView+algorithmExperimentsSection.swift b/Loop/Loop/Views/SettingsView+algorithmExperimentsSection.swift -index 54bd2c71..081d2d08 100644 ---- a/Loop/Loop/Views/SettingsView+algorithmExperimentsSection.swift -+++ b/Loop/Loop/Views/SettingsView+algorithmExperimentsSection.swift -@@ -39,8 +39,10 @@ public struct ExperimentRow: View { - } - - public struct ExperimentsSettingsView: View { -- @State private var isGlucoseBasedApplicationFactorEnabled = UserDefaults.standard.glucoseBasedApplicationFactorEnabled -- @State private var isIntegralRetrospectiveCorrectionEnabled = UserDefaults.standard.integralRetrospectiveCorrectionEnabled -+ @State private var isGlucoseBasedApplicationFactorEnabled = false -+ @State private var isIntegralRetrospectiveCorrectionEnabled = false -+ @State private var isNegativeInsulinDamperEnabled = false -+ - var automaticDosingStrategy: AutomaticDosingStrategy - - public var body: some View { -@@ -70,11 +72,21 @@ public struct ExperimentsSettingsView: View { - name: NSLocalizedString("Integral Retrospective Correction", comment: "Title of integral retrospective correction experiment"), - enabled: isIntegralRetrospectiveCorrectionEnabled) - } -+ NavigationLink(destination: NegativeInsulinDamperSelectionView(isNegativeInsulinDamperEnabled: $isNegativeInsulinDamperEnabled)) { -+ ExperimentRow( -+ name: NSLocalizedString("Negative Insulin Damper", comment: "Title of negative insulin damper experiment"), -+ enabled: isNegativeInsulinDamperEnabled) -+ } - Spacer() - } - .padding() - } - .navigationBarTitleDisplayMode(.inline) -+ .onAppear { // force loading the values from UserDefaults -+ isGlucoseBasedApplicationFactorEnabled = UserDefaults.standard.glucoseBasedApplicationFactorEnabled -+ isIntegralRetrospectiveCorrectionEnabled = UserDefaults.standard.integralRetrospectiveCorrectionEnabled -+ isNegativeInsulinDamperEnabled = UserDefaults.standard.negativeInsulinDamperEnabled -+ } - } - } - -@@ -83,6 +95,7 @@ extension UserDefaults { - private enum Key: String { - case GlucoseBasedApplicationFactorEnabled = "com.loopkit.algorithmExperiments.glucoseBasedApplicationFactorEnabled" - case IntegralRetrospectiveCorrectionEnabled = "com.loopkit.algorithmExperiments.integralRetrospectiveCorrectionEnabled" -+ case NegativeInsulinDamperEnabled = "com.loopkit.algorithmExperiments.negativeInsulinDamperEnabled" - } - - var glucoseBasedApplicationFactorEnabled: Bool { -@@ -103,4 +116,12 @@ extension UserDefaults { - } - } - -+ var negativeInsulinDamperEnabled: Bool { -+ get { -+ bool(forKey: Key.NegativeInsulinDamperEnabled.rawValue) as Bool -+ } -+ set { -+ set(newValue, forKey: Key.NegativeInsulinDamperEnabled.rawValue) -+ } -+ } - } -diff --git a/Loop/LoopCore/LiveActivitySettings.swift b/Loop/LoopCore/LiveActivitySettings.swift -new file mode 100644 -index 00000000..71807464 ---- /dev/null -+++ b/Loop/LoopCore/LiveActivitySettings.swift -@@ -0,0 +1,159 @@ -+// -+// LiveActivitySettings.swift -+// LoopCore -+// -+// Created by Bastiaan Verhaar on 04/07/2024. -+// Copyright © 2024 LoopKit Authors. All rights reserved. -+// -+ -+import Foundation -+ -+public enum BottomRowConfiguration: Codable { -+ case iob -+ case cob -+ case basal -+ case currentBg -+ case eventualBg -+ case deltaBg -+ case loopCircle -+ case updatedAt -+ -+ static let defaults: [BottomRowConfiguration] = [.currentBg, .iob, .cob, .updatedAt] -+ public static let all: [BottomRowConfiguration] = [.iob, .cob, .basal, .currentBg, .eventualBg, .deltaBg, .loopCircle, .updatedAt] -+ -+ public func name() -> String { -+ switch self { -+ case .iob: -+ return NSLocalizedString("IOB", comment: "") -+ case .cob: -+ return NSLocalizedString("COB", comment: "") -+ case .basal: -+ return NSLocalizedString("Basal", comment: "") -+ case .currentBg: -+ return NSLocalizedString("Current BG", comment: "") -+ case .eventualBg: -+ return NSLocalizedString("Event", comment: "") -+ case .deltaBg: -+ return NSLocalizedString("Delta", comment: "") -+ case .loopCircle: -+ return NSLocalizedString("Loop", comment: "") -+ case .updatedAt: -+ return NSLocalizedString("Updated", comment: "") -+ } -+ } -+ -+ public func description() -> String { -+ switch self { -+ case .iob: -+ return NSLocalizedString("Active Insulin", comment: "") -+ case .cob: -+ return NSLocalizedString("Active Carbohydrates", comment: "") -+ case .basal: -+ return NSLocalizedString("Basal", comment: "") -+ case .currentBg: -+ return NSLocalizedString("Current Glucose", comment: "") -+ case .eventualBg: -+ return NSLocalizedString("Eventually", comment: "") -+ case .deltaBg: -+ return NSLocalizedString("Delta", comment: "") -+ case .loopCircle: -+ return NSLocalizedString("Loop circle", comment: "") -+ case .updatedAt: -+ return NSLocalizedString("Updated at", comment: "") -+ } -+ } -+} -+ -+public enum LiveActivityMode: Codable, CustomStringConvertible { -+ case large -+ case small -+ -+ public static let all: [LiveActivityMode] = [.large, .small] -+ public var description: String { -+ NSLocalizedString("In which mode do you want to render the Live Activity", comment: "") -+ } -+ -+ public func name() -> String { -+ switch self { -+ case .large: -+ return NSLocalizedString("Large", comment: "") -+ case .small: -+ return NSLocalizedString("Small", comment: "") -+ } -+ } -+} -+ -+public struct LiveActivitySettings: Codable, Equatable { -+ public var enabled: Bool -+ public var mode: LiveActivityMode -+ public var addPredictiveLine: Bool -+ public var useLimits: Bool -+ public var upperLimitChartMmol: Double -+ public var lowerLimitChartMmol: Double -+ public var upperLimitChartMg: Double -+ public var lowerLimitChartMg: Double -+ public var bottomRowConfiguration: [BottomRowConfiguration] -+ -+ private enum CodingKeys: String, CodingKey { -+ case enabled -+ case mode -+ case addPredictiveLine -+ case bottomRowConfiguration -+ case useLimits -+ case upperLimitChartMmol -+ case lowerLimitChartMmol -+ case upperLimitChartMg -+ case lowerLimitChartMg -+ } -+ -+ private static let defaultUpperLimitMmol = Double(10) -+ private static let defaultLowerLimitMmol = Double(4) -+ private static let defaultUpperLimitMg = Double(180) -+ private static let defaultLowerLimitMg = Double(72) -+ -+ public init(from decoder:Decoder) throws { -+ let values = try decoder.container(keyedBy: CodingKeys.self) -+ -+ self.enabled = try values.decode(Bool.self, forKey: .enabled) -+ self.mode = try values.decodeIfPresent(LiveActivityMode.self, forKey: .mode) ?? .large -+ self.addPredictiveLine = try values.decode(Bool.self, forKey: .addPredictiveLine) -+ self.useLimits = try values.decodeIfPresent(Bool.self, forKey: .useLimits) ?? true -+ self.upperLimitChartMmol = try values.decode(Double?.self, forKey: .upperLimitChartMmol) ?? LiveActivitySettings.defaultUpperLimitMmol -+ self.lowerLimitChartMmol = try values.decode(Double?.self, forKey: .lowerLimitChartMmol) ?? LiveActivitySettings.defaultLowerLimitMmol -+ self.upperLimitChartMg = try values.decode(Double?.self, forKey: .upperLimitChartMg) ?? LiveActivitySettings.defaultUpperLimitMg -+ self.lowerLimitChartMg = try values.decode(Double?.self, forKey: .lowerLimitChartMg) ?? LiveActivitySettings.defaultLowerLimitMg -+ self.bottomRowConfiguration = try values.decode([BottomRowConfiguration].self, forKey: .bottomRowConfiguration) -+ } -+ -+ public init() { -+ self.enabled = true -+ self.mode = .large -+ self.addPredictiveLine = true -+ self.useLimits = true -+ self.upperLimitChartMmol = LiveActivitySettings.defaultUpperLimitMmol -+ self.lowerLimitChartMmol = LiveActivitySettings.defaultLowerLimitMmol -+ self.upperLimitChartMg = LiveActivitySettings.defaultUpperLimitMg -+ self.lowerLimitChartMg = LiveActivitySettings.defaultLowerLimitMg -+ self.bottomRowConfiguration = BottomRowConfiguration.defaults -+ } -+ -+ public static func == (lhs: LiveActivitySettings, rhs: LiveActivitySettings) -> Bool { -+ return lhs.addPredictiveLine == rhs.addPredictiveLine && -+ lhs.mode == rhs.mode && -+ lhs.useLimits == rhs.useLimits && -+ lhs.lowerLimitChartMmol == rhs.lowerLimitChartMmol && -+ lhs.upperLimitChartMmol == rhs.upperLimitChartMmol && -+ lhs.lowerLimitChartMg == rhs.lowerLimitChartMg && -+ lhs.upperLimitChartMg == rhs.upperLimitChartMg -+ } -+ -+ public static func != (lhs: LiveActivitySettings, rhs: LiveActivitySettings) -> Bool { -+ return lhs.addPredictiveLine != rhs.addPredictiveLine || -+ lhs.mode != rhs.mode || -+ lhs.useLimits != rhs.useLimits || -+ lhs.lowerLimitChartMmol != rhs.lowerLimitChartMmol || -+ lhs.upperLimitChartMmol != rhs.upperLimitChartMmol || -+ lhs.lowerLimitChartMg != rhs.lowerLimitChartMg || -+ lhs.upperLimitChartMg != rhs.upperLimitChartMg -+ } -+} -\ No newline at end of file -diff --git a/Loop/LoopCore/NSUserDefaults.swift b/Loop/LoopCore/NSUserDefaults.swift -index 93fa7e17..dacf2ecd 100644 ---- a/Loop/LoopCore/NSUserDefaults.swift -+++ b/Loop/LoopCore/NSUserDefaults.swift -@@ -23,6 +23,7 @@ extension UserDefaults { - case allowSimulators = "com.loopkit.Loop.allowSimulators" - case LastMissedMealNotification = "com.loopkit.Loop.lastMissedMealNotification" - case userRequestedLoopReset = "com.loopkit.Loop.userRequestedLoopReset" -+ case liveActivity = "com.loopkit.Loop.liveActivity" - } - - public static let appGroup = UserDefaults(suiteName: Bundle.main.appGroupSuiteName) -@@ -165,6 +166,29 @@ extension UserDefaults { - setValue(newValue, forKey: Key.userRequestedLoopReset.rawValue) - } - } -+ -+ public var liveActivity: LiveActivitySettings? { -+ get { -+ let decoder = JSONDecoder() -+ guard let data = object(forKey: Key.liveActivity.rawValue) as? Data else { -+ return nil -+ } -+ return try? decoder.decode(LiveActivitySettings.self, from: data) -+ } -+ set { -+ do { -+ if let newValue = newValue { -+ let encoder = JSONEncoder() -+ let data = try encoder.encode(newValue) -+ set(data, forKey: Key.liveActivity.rawValue) -+ } else { -+ set(nil, forKey: Key.liveActivity.rawValue) -+ } -+ } catch { -+ assertionFailure("Unable to encode MissedMealNotification") -+ } -+ } -+ } - - public func removeLegacyLoopSettings() { - removeObject(forKey: "com.loudnate.Naterade.BasalRateSchedule") -diff --git a/Loop/LoopTests/Managers/LoopDataManagerDosingTests.swift b/Loop/LoopTests/Managers/LoopDataManagerDosingTests.swift -index a1f26a0e..000d2e7e 100644 ---- a/Loop/LoopTests/Managers/LoopDataManagerDosingTests.swift -+++ b/Loop/LoopTests/Managers/LoopDataManagerDosingTests.swift -@@ -52,6 +52,38 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { - let url = bundle.url(forResource: name, withExtension: "json")! - return try! decoder.decode([PredictedGlucoseValue].self, from: try! Data(contentsOf: url)) - } -+ -+ func testNegativeInsulinDamper() { -+ let marginalSlope = 0.05 -+ let anchorAlpha = 0.75 -+ let anchorPoint = 50.0 -+ -+ XCTAssertEqual(1.0, LoopDataManager.calculateNegativeInsulinDamperAlpha(anchorAlpha, anchorPoint, marginalSlope, 0)) -+ -+ XCTAssertEqual(anchorAlpha, LoopDataManager.calculateNegativeInsulinDamperAlpha(anchorAlpha, anchorPoint, marginalSlope, anchorPoint), accuracy: 1E-6) -+ -+ let linearScaleSlope = (1 - anchorAlpha)/anchorPoint -+ let transitionPoint = (1 - marginalSlope) / (2 * linearScaleSlope) -+ let transitionValue = (1 - linearScaleSlope * transitionPoint) * transitionPoint -+ -+ XCTAssertEqual(marginalSlope, LoopDataManager.calculateNegativeInsulinDamperAlpha(anchorAlpha, anchorPoint, marginalSlope, 1E12), accuracy: 1E-6) -+ -+ var prevAlpha = 1.1 -+ for i in 0...1_000_000 { -+ let iVal = Double(i) -+ let alpha = LoopDataManager.calculateNegativeInsulinDamperAlpha(anchorAlpha, anchorPoint, marginalSlope, iVal) -+ -+ XCTAssertLessThan(alpha, prevAlpha) -+ XCTAssertGreaterThan(alpha, marginalSlope) -+ -+ if Double(i) <= transitionPoint { -+ XCTAssertEqual(alpha, 1.0 - iVal * linearScaleSlope, accuracy: 1E-6) -+ } else { -+ XCTAssertEqual(alpha * iVal, transitionValue + marginalSlope * (iVal - transitionPoint), accuracy: 1E-6) -+ } -+ prevAlpha = alpha -+ } -+ } - - // MARK: Tests - func testForecastFromLiveCaptureInputData() { -diff --git a/Loop/LoopTests/Mock Stores/MockDoseStore.swift b/Loop/LoopTests/Mock Stores/MockDoseStore.swift -index 207596f3..4c5d945a 100644 ---- a/Loop/LoopTests/Mock Stores/MockDoseStore.swift -+++ b/Loop/LoopTests/Mock Stores/MockDoseStore.swift -@@ -90,11 +90,11 @@ class MockDoseStore: DoseStoreProtocol { - completion(.failure(.configurationError)) - } - -- func getGlucoseEffects(start: Date, end: Date? = nil, basalDosingEnd: Date? = Date(), completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void) { -+ func getGlucoseEffects(start: Date, end: Date? = nil, doseEnd: Date? = nil, basalDosingEnd: Date? = Date(), completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void) { - if let doseHistory, let sensitivitySchedule, let basalProfile = basalProfileApplyingOverrideHistory { - // To properly know glucose effects at startDate, we need to go back another DIA hours - let doseStart = start.addingTimeInterval(-longestEffectDuration) -- let doses = doseHistory.filterDateRange(doseStart, end) -+ let doses = doseHistory.filterDateRange(doseStart, doseEnd ?? end) - let trimmedDoses = doses.map { (dose) -> DoseEntry in - guard dose.type != .bolus else { - return dose -diff --git a/Loop/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/Loop/LoopTests/ViewModels/BolusEntryViewModelTests.swift -index 7f2c421e..32cb63ca 100644 ---- a/Loop/LoopTests/ViewModels/BolusEntryViewModelTests.swift -+++ b/Loop/LoopTests/ViewModels/BolusEntryViewModelTests.swift -@@ -822,6 +822,8 @@ fileprivate class MockLoopState: LoopState { - - var totalRetrospectiveCorrection: HKQuantity? - -+ var negativeInsulinDamper: Double? -+ - var predictGlucoseValueResult: [PredictedGlucoseValue] = [] - func predictGlucose(using inputs: PredictionInputEffect, potentialBolus: DoseEntry?, potentialCarbEntry: NewCarbEntry?, replacingCarbEntry replacedCarbEntry: StoredCarbEntry?, includingPendingInsulin: Bool, considerPositiveVelocityAndRC: Bool) throws -> [PredictedGlucoseValue] { - return predictGlucoseValueResult -Submodule LoopKit a03be57..d1745ae: -diff --git a/LoopKit/LoopKit/Insulin/ExponentialInsulinModelPreset.swift b/LoopKit/LoopKit/Insulin/ExponentialInsulinModelPreset.swift -index 7ba9842c..baf4d7ac 100644 ---- a/LoopKit/LoopKit/Insulin/ExponentialInsulinModelPreset.swift -+++ b/LoopKit/LoopKit/Insulin/ExponentialInsulinModelPreset.swift -@@ -63,22 +63,16 @@ extension ExponentialInsulinModelPreset { - } - } - -- var model: InsulinModel { -+ public var model: InsulinModel { - return ExponentialInsulinModel(actionDuration: actionDuration, peakActivityTime: peakActivity, delay: delay) - } --} -- -- --extension ExponentialInsulinModelPreset: InsulinModel { -- public var effectDuration: TimeInterval { -+ -+ public var effectDuration : TimeInterval { - return model.effectDuration - } -- -- public func percentEffectRemaining(at time: TimeInterval) -> Double { -- return model.percentEffectRemaining(at: time) -- } - } - -+ - extension ExponentialInsulinModelPreset: CustomDebugStringConvertible { - public var debugDescription: String { - return "\(self.rawValue)(\(String(reflecting: model)))" -diff --git a/LoopKit/LoopKit/Insulin/InsulinModelProvider.swift b/LoopKit/LoopKit/Insulin/InsulinModelProvider.swift -index f99aca82..f067d6bf 100644 ---- a/LoopKit/LoopKit/Insulin/InsulinModelProvider.swift -+++ b/LoopKit/LoopKit/Insulin/InsulinModelProvider.swift -@@ -10,22 +10,22 @@ public protocol InsulinModelProvider { - } - - public struct PresetInsulinModelProvider: InsulinModelProvider { -- var defaultRapidActingModel: InsulinModel? -+ var defaultRapidActingModel: ExponentialInsulinModelPreset? - -- public init(defaultRapidActingModel: InsulinModel?) { -+ public init(defaultRapidActingModel: ExponentialInsulinModelPreset?) { - self.defaultRapidActingModel = defaultRapidActingModel - } - - public func model(for type: InsulinType?) -> InsulinModel { - switch type { - case .fiasp: -- return ExponentialInsulinModelPreset.fiasp -+ return ExponentialInsulinModelPreset.fiasp.model - case .lyumjev: -- return ExponentialInsulinModelPreset.lyumjev -+ return ExponentialInsulinModelPreset.lyumjev.model - case .afrezza: -- return ExponentialInsulinModelPreset.afrezza -+ return ExponentialInsulinModelPreset.afrezza.model - default: -- return defaultRapidActingModel ?? ExponentialInsulinModelPreset.rapidActingAdult -+ return (defaultRapidActingModel ?? ExponentialInsulinModelPreset.rapidActingAdult).model - } - } - } -@@ -38,6 +38,10 @@ public struct StaticInsulinModelProvider: InsulinModelProvider { - self.model = model - } - -+ public init(_ preset: ExponentialInsulinModelPreset) { -+ self.model = preset.model -+ } -+ - public func model(for type: InsulinType?) -> InsulinModel { - return model - } -diff --git a/LoopKit/LoopKit/InsulinKit/DoseStore.swift b/LoopKit/LoopKit/InsulinKit/DoseStore.swift -index 5efe99ff..7be9c02f 100644 ---- a/LoopKit/LoopKit/InsulinKit/DoseStore.swift -+++ b/LoopKit/LoopKit/InsulinKit/DoseStore.swift -@@ -1333,10 +1333,11 @@ extension DoseStore { - /// - Parameters: - /// - start: The earliest date of effects to retrieve - /// - end: The latest date of effects to retrieve, if provided -+ /// - doseEnd: the latest startDate of doses whose effects will be considered. If not provided, then equal to end. - /// - basalDosingEnd: The date at which continuing doses should be assumed to be cancelled - /// - completion: A closure called once the effects have been retrieved - /// - result: An array of effects, in chronological order -- public func getGlucoseEffects(start: Date, end: Date? = nil, basalDosingEnd: Date? = Date(), completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void) { -+ public func getGlucoseEffects(start: Date, end: Date? = nil, doseEnd: Date? = nil, basalDosingEnd: Date? = Date(), completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void) { - guard let insulinSensitivitySchedule = self.insulinSensitivityScheduleApplyingOverrideHistory else { - completion(.failure(.configurationError)) - return -@@ -1344,7 +1345,7 @@ extension DoseStore { - - // To properly know glucose effects at startDate, we need to go back another DIA hours - let doseStart = start.addingTimeInterval(-longestEffectDuration) -- getNormalizedDoseEntries(start: doseStart, end: end) { (result) in -+ getNormalizedDoseEntries(start: doseStart, end: doseEnd ?? end) { (result) in - switch result { - case .failure(let error): - completion(.failure(error)) -diff --git a/LoopKit/LoopKitTests/DoseStoreTests.swift b/LoopKit/LoopKitTests/DoseStoreTests.swift -index 93777b09..bdb72ae8 100644 ---- a/LoopKit/LoopKitTests/DoseStoreTests.swift -+++ b/LoopKit/LoopKitTests/DoseStoreTests.swift -@@ -1428,7 +1428,7 @@ class DoseStoreEffectTests: PersistenceControllerTestCase { - override func setUp() { - super.setUp() - let healthStore = HKHealthStoreMock() -- let exponentialInsulinModel: InsulinModel = ExponentialInsulinModelPreset.rapidActingAdult -+ let exponentialInsulinModel: InsulinModel = ExponentialInsulinModelPreset.rapidActingAdult.model - let startDate = dateFormatter.date(from: "2015-07-13T12:00:00")! - - let sampleStore = HealthKitSampleStore(