diff --git a/Common/FeatureFlags.swift b/Common/FeatureFlags.swift index 3ed4b4a4eb..197eed6d49 100644 --- a/Common/FeatureFlags.swift +++ b/Common/FeatureFlags.swift @@ -39,6 +39,7 @@ struct FeatureFlagConfiguration: Decodable { let adultChildInsulinModelSelectionEnabled: Bool let profileExpirationSettingsViewEnabled: Bool let missedMealNotifications: Bool + let allowAlgorithmExperiments: Bool fileprivate init() { @@ -229,6 +230,11 @@ struct FeatureFlagConfiguration: Decodable { self.missedMealNotifications = true #endif + #if ALLOW_ALGORITHM_EXPERIMENTS + self.allowAlgorithmExperiments = true + #else + self.allowAlgorithmExperiments = false + #endif } } @@ -263,7 +269,8 @@ extension FeatureFlagConfiguration : CustomDebugStringConvertible { "* dynamicCarbAbsorptionEnabled: \(dynamicCarbAbsorptionEnabled)", "* adultChildInsulinModelSelectionEnabled: \(adultChildInsulinModelSelectionEnabled)", "* profileExpirationSettingsViewEnabled: \(profileExpirationSettingsViewEnabled)", - "* missedMealNotifications: \(missedMealNotifications)" + "* missedMealNotifications: \(missedMealNotifications)", + "* allowAlgorithmExperiments: \(allowAlgorithmExperiments)" ].joined(separator: "\n") } } diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index d9e4871882..c48bf0fcc4 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -518,6 +518,11 @@ C1FB428D21791D2500FAB378 /* PumpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C3B6F620BBCAA30026CAFA /* PumpManager.swift */; }; C1FB428F217921D600FAB378 /* PumpManagerUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428E217921D600FAB378 /* PumpManagerUI.swift */; }; C1FB4290217922A100FAB378 /* PumpManagerUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428E217921D600FAB378 /* PumpManagerUI.swift */; }; + DDC389F62A2B61750066E2E8 /* ApplicationFactorStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389F52A2B61750066E2E8 /* ApplicationFactorStrategy.swift */; }; + DDC389F82A2B620B0066E2E8 /* GlucoseBasedApplicationFactorStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389F72A2B620B0066E2E8 /* GlucoseBasedApplicationFactorStrategy.swift */; }; + DDC389FA2A2B62470066E2E8 /* ConstantApplicationFactorStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389F92A2B62470066E2E8 /* ConstantApplicationFactorStrategy.swift */; }; + DDC389FC2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389FB2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift */; }; + DDC389FE2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389FD2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift */; }; E90909D124E34AC500F963D2 /* high_and_rising_with_cob_momentum_effect.json in Resources */ = {isa = PBXBuildFile; fileRef = E90909CC24E34AC500F963D2 /* high_and_rising_with_cob_momentum_effect.json */; }; E90909D224E34AC500F963D2 /* high_and_rising_with_cob_insulin_effect.json in Resources */ = {isa = PBXBuildFile; fileRef = E90909CD24E34AC500F963D2 /* high_and_rising_with_cob_insulin_effect.json */; }; E90909D324E34AC500F963D2 /* high_and_rising_with_cob_predicted_glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = E90909CE24E34AC500F963D2 /* high_and_rising_with_cob_predicted_glucose.json */; }; @@ -1687,6 +1692,11 @@ C1FF3D4B29C786A900BDC1EC /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; C1FF3D4C29C786A900BDC1EC /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; C1FF3D4D29C786A900BDC1EC /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/InfoPlist.strings; sourceTree = ""; }; + DDC389F52A2B61750066E2E8 /* ApplicationFactorStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationFactorStrategy.swift; sourceTree = ""; }; + DDC389F72A2B620B0066E2E8 /* GlucoseBasedApplicationFactorStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseBasedApplicationFactorStrategy.swift; sourceTree = ""; }; + DDC389F92A2B62470066E2E8 /* ConstantApplicationFactorStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantApplicationFactorStrategy.swift; sourceTree = ""; }; + DDC389FB2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsView+algorithmExperimentsSection.swift"; sourceTree = ""; }; + DDC389FD2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseBasedApplicationFactorSelectionView.swift; sourceTree = ""; }; E90909CC24E34AC500F963D2 /* high_and_rising_with_cob_momentum_effect.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = high_and_rising_with_cob_momentum_effect.json; sourceTree = ""; }; E90909CD24E34AC500F963D2 /* high_and_rising_with_cob_insulin_effect.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = high_and_rising_with_cob_insulin_effect.json; sourceTree = ""; }; E90909CE24E34AC500F963D2 /* high_and_rising_with_cob_predicted_glucose.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = high_and_rising_with_cob_predicted_glucose.json; sourceTree = ""; }; @@ -2314,6 +2324,7 @@ isa = PBXGroup; children = ( A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */, + DDC389FB2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift */, C1D289B422F90A52003FFBD9 /* BasalDeliveryState.swift */, A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */, C17824991E1999FA00D9D25C /* CaseCountable.swift */, @@ -2400,6 +2411,7 @@ 43F64DD81D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift */, 4311FB9A1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift */, C1AF062229426300002C1B19 /* ManualGlucoseEntryRow.swift */, + DDC389FD2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift */, ); path = Views; sourceTree = ""; @@ -2441,6 +2453,9 @@ C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */, A96DAC2B2838F31200D94E38 /* SharedLogging.swift */, 7E69CFFB2A16A77E00203CBD /* ResetLoopManager.swift */, + DDC389F52A2B61750066E2E8 /* ApplicationFactorStrategy.swift */, + DDC389F72A2B620B0066E2E8 /* GlucoseBasedApplicationFactorStrategy.swift */, + DDC389F92A2B62470066E2E8 /* ConstantApplicationFactorStrategy.swift */, ); path = Managers; sourceTree = ""; @@ -3872,6 +3887,7 @@ C1201E2C23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift in Sources */, 1D49795824E7289700948F05 /* ServicesViewModel.swift in Sources */, 1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */, + DDC389FA2A2B62470066E2E8 /* ConstantApplicationFactorStrategy.swift in Sources */, 892D7C5123B54A15008A9656 /* CarbEntryViewController.swift in Sources */, B4E202302661063E009421B5 /* AutomaticDosingStatus.swift in Sources */, C191D2A125B3ACAA00C26C0B /* DosingStrategySelectionView.swift in Sources */, @@ -3903,6 +3919,7 @@ 1DB619AC270BAD3D006C9D07 /* VersionUpdateViewModel.swift in Sources */, A9C62D882331703100535612 /* Service.swift in Sources */, 89CAB36324C8FE96009EE3CE /* PredictedGlucoseChartView.swift in Sources */, + DDC389F82A2B620B0066E2E8 /* GlucoseBasedApplicationFactorStrategy.swift in Sources */, 4F6663941E905FD2009E74FC /* ChartColorPalette+Loop.swift in Sources */, A9F703732489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift in Sources */, 4328E0351CFC0AE100E199AA /* WatchDataManager.swift in Sources */, @@ -3933,6 +3950,7 @@ E9B080B1253BDA6300BAD8F8 /* UserDefaults+LoopIntents.swift in Sources */, C1AF062329426300002C1B19 /* ManualGlucoseEntryRow.swift in Sources */, C148CEE724FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift in Sources */, + DDC389FC2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift in Sources */, 1D12D3B92548EFDD00B53E8B /* main.swift in Sources */, 435400341C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */, A9DCF32A25B0FABF00C89088 /* LoopUIColorPalette+Default.swift in Sources */, @@ -3967,6 +3985,7 @@ A97F250825E056D500F0EE19 /* OnboardingManager.swift in Sources */, 438D42F91D7C88BC003244B0 /* PredictionInputEffect.swift in Sources */, 892A5D692230C41D008961AB /* RangeReplaceableCollection.swift in Sources */, + DDC389F62A2B61750066E2E8 /* ApplicationFactorStrategy.swift in Sources */, 4F70C2101DE8FAC5006380B7 /* ExtensionDataManager.swift in Sources */, 43DFB62320D4CAE7008A7BAE /* PumpManager.swift in Sources */, A9FB75F1252BE320004C7D3F /* BolusDosingDecision.swift in Sources */, @@ -3974,6 +3993,7 @@ 431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */, 1D05219D2469F1F5000EBBDE /* AlertStore.swift in Sources */, 439897371CD2F80600223065 /* AnalyticsServicesManager.swift in Sources */, + DDC389FE2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift in Sources */, A9C62D842331700E00535612 /* DiagnosticLog+Subsystem.swift in Sources */, 895FE0952201234000FCF18A /* OverrideSelectionViewController.swift in Sources */, C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */, diff --git a/Loop/Extensions/SettingsView+algorithmExperimentsSection.swift b/Loop/Extensions/SettingsView+algorithmExperimentsSection.swift new file mode 100644 index 0000000000..c6b9f93970 --- /dev/null +++ b/Loop/Extensions/SettingsView+algorithmExperimentsSection.swift @@ -0,0 +1,67 @@ +// +// SettingsView+algorithmExperimentsSection.swift +// Loop +// +// Created by Jonas Björkert on 2023-06-03. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation +import SwiftUI +import LoopKit +import LoopKitUI + +extension SettingsView { + internal var algorithmExperimentsSection: some View { + NavigationLink(NSLocalizedString("Algorithm Experiments", comment: "The title of the Algorithm Experiments section in settings")) { + ExperimentsSettingsView(automaticDosingStrategy: viewModel.automaticDosingStrategy) + } + } +} + +public struct ExperimentRow: View { + var name: String + var enabled: Bool + + public var body: some View { + HStack { + Text(name) + .foregroundColor(.primary) + Spacer() + Text(enabled ? "On" : "Off") + .foregroundColor(enabled ? .red : .secondary) + } + } +} + +public struct ExperimentsSettingsView: View { + @State private var isGlucoseBasedApplicationFactorEnabled = UserDefaults.standard.glucoseBasedApplicationFactorEnabled + var automaticDosingStrategy: AutomaticDosingStrategy + + public var body: some View { + VStack(alignment: .center, spacing: 12) { + VStack { + Text("⚠️").font(.largeTitle) + Text("Caution") + } + Divider() + VStack(alignment: .leading, spacing: 12) { + Text(NSLocalizedString("Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop algorithm, so please use carefully.", comment: "Algorithm Experiments description.")) + Text(NSLocalizedString("In future versions of Loop these experiments may change, end up as standard parts of the Loop Algorithm, or be removed from Loop entirely. Please follow along in the Loop Zulip chat to stay informed of possible changes to these features.", comment: "Algorithm Experiments description second paragraph.")) + } + .foregroundColor(.secondary) + + Divider() + NavigationLink(destination: GlucoseBasedApplicationFactorSelectionView(isGlucoseBasedApplicationFactorEnabled: $isGlucoseBasedApplicationFactorEnabled, automaticDosingStrategy: automaticDosingStrategy)) { + ExperimentRow(name: "Glucose Based Partial Application", enabled: isGlucoseBasedApplicationFactorEnabled && automaticDosingStrategy == .automaticBolus) + } + .padding() + .background(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .cornerRadius(10) + Spacer() + } + .padding() + .navigationTitle(NSLocalizedString("Algorithm Experiments", comment: "Navigation title for algorithms experiments screen")) + } +} diff --git a/Loop/Managers/ApplicationFactorStrategy.swift b/Loop/Managers/ApplicationFactorStrategy.swift new file mode 100644 index 0000000000..bf67935c4e --- /dev/null +++ b/Loop/Managers/ApplicationFactorStrategy.swift @@ -0,0 +1,20 @@ +// +// ApplicationFactorStrategy.swift +// Loop +// +// Created by Jonas Björkert on 2023-06-03. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation +import HealthKit +import LoopKit +import LoopCore + +protocol ApplicationFactorStrategy { + func calculateDosingFactor( + for glucose: HKQuantity, + correctionRangeSchedule: GlucoseRangeSchedule, + settings: LoopSettings + ) -> Double +} diff --git a/Loop/Managers/ConstantApplicationFactorStrategy.swift b/Loop/Managers/ConstantApplicationFactorStrategy.swift new file mode 100644 index 0000000000..e13c40c42e --- /dev/null +++ b/Loop/Managers/ConstantApplicationFactorStrategy.swift @@ -0,0 +1,23 @@ +// +// ConstantDosingStrategy.swift +// Loop +// +// Created by Jonas Björkert on 2023-06-03. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation +import HealthKit +import LoopKit +import LoopCore + +struct ConstantApplicationFactorStrategy: ApplicationFactorStrategy { + func calculateDosingFactor( + for glucose: HKQuantity, + correctionRangeSchedule: GlucoseRangeSchedule, + settings: LoopSettings + ) -> Double { + // The original strategy uses a constant dosing factor. + return LoopConstants.bolusPartialApplicationFactor + } +} diff --git a/Loop/Managers/GlucoseBasedApplicationFactorStrategy.swift b/Loop/Managers/GlucoseBasedApplicationFactorStrategy.swift new file mode 100644 index 0000000000..2573deba31 --- /dev/null +++ b/Loop/Managers/GlucoseBasedApplicationFactorStrategy.swift @@ -0,0 +1,57 @@ +// +// GlucoseBasedApplicationFactorStrategy.swift +// Loop +// +// Created by Jonas Björkert on 2023-06-03. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation +import HealthKit +import LoopKit +import LoopCore + +struct GlucoseBasedApplicationFactorStrategy: ApplicationFactorStrategy { + static let minPartialApplicationFactor = 0.20 // min fraction of correction when glucose > minGlucoseSlidingScale + static let maxPartialApplicationFactor = 0.80 // max fraction of correction when glucose > maxGlucoseSlidingScale + // set minGlucoseSlidingScale based on user setting for correction range + // use mg/dL for calculations + static let minGlucoseDeltaSlidingScale = 10.0 // mg/dL + static let maxGlucoseSlidingScale = 200.0 // mg/dL + + func calculateDosingFactor( + for glucose: HKQuantity, + correctionRangeSchedule: GlucoseRangeSchedule, + settings: LoopSettings + ) -> Double { + // Calculate current glucose and lower bound target + let currentGlucose = glucose.doubleValue(for: .milligramsPerDeciliter) + let correctionRange = correctionRangeSchedule.quantityRange(at: Date()) + let lowerBoundTarget = correctionRange.lowerBound.doubleValue(for: .milligramsPerDeciliter) + + // Calculate minimum glucose sliding scale and scaling fraction + let minGlucoseSlidingScale = GlucoseBasedApplicationFactorStrategy.minGlucoseDeltaSlidingScale + lowerBoundTarget + let scalingFraction = (GlucoseBasedApplicationFactorStrategy.maxPartialApplicationFactor - GlucoseBasedApplicationFactorStrategy.minPartialApplicationFactor) / (GlucoseBasedApplicationFactorStrategy.maxGlucoseSlidingScale - minGlucoseSlidingScale) + let scalingGlucose = max(currentGlucose - minGlucoseSlidingScale, 0.0) + + // Calculate effectiveBolusApplicationFactor + let effectiveBolusApplicationFactor = min(GlucoseBasedApplicationFactorStrategy.minPartialApplicationFactor + scalingGlucose * scalingFraction, GlucoseBasedApplicationFactorStrategy.maxPartialApplicationFactor) + + return effectiveBolusApplicationFactor + } +} + +extension UserDefaults { + private enum Key: String { + case GlucoseBasedApplicationFactorEnabled = "com.loopkit.algorithmExperiments.glucoseBasedApplicationFactorEnabled" + } + + var glucoseBasedApplicationFactorEnabled: Bool { + get { + bool(forKey: Key.GlucoseBasedApplicationFactorEnabled.rawValue) as Bool + } + set { + set(newValue, forKey: Key.GlucoseBasedApplicationFactorEnabled.rawValue) + } + } +} diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index ffc66ee314..9994b09b3a 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1698,7 +1698,23 @@ extension LoopDataManager { return self.delegate?.roundBolusVolume(units: units) ?? units } - let maxAutomaticBolus = min(iobHeadroom, maxBolus! * LoopConstants.bolusPartialApplicationFactor) + // Create dosing strategy based on user setting + let applicationFactorStrategy: ApplicationFactorStrategy = UserDefaults.standard.glucoseBasedApplicationFactorEnabled + ? GlucoseBasedApplicationFactorStrategy() + : ConstantApplicationFactorStrategy() + + let correctionRangeSchedule = settings.effectiveGlucoseTargetRangeSchedule() + + let effectiveBolusApplicationFactor = applicationFactorStrategy.calculateDosingFactor( + for: glucose.quantity, + correctionRangeSchedule: correctionRangeSchedule!, + settings: settings + ) + + self.logger.debug(" *** Glucose: %{public}@, effectiveBolusApplicationFactor: %.2f", glucose.quantity.description, effectiveBolusApplicationFactor) + + // If a user customizes maxPartialApplicationFactor > 1; this respects maxBolus + let maxAutomaticBolus = min(iobHeadroom, maxBolus! * min(effectiveBolusApplicationFactor, 1.0)) dosingRecommendation = predictedGlucose.recommendedAutomaticDose( to: glucoseTargetRange!, @@ -1708,7 +1724,7 @@ extension LoopDataManager { model: doseStore.insulinModelProvider.model(for: pumpInsulinType), basalRates: basalRateSchedule!, maxAutomaticBolus: maxAutomaticBolus, - partialApplicationFactor: LoopConstants.bolusPartialApplicationFactor * self.timeBasedDoseApplicationFactor, + partialApplicationFactor: effectiveBolusApplicationFactor * self.timeBasedDoseApplicationFactor, lastTempBasal: lastTempBasal, volumeRounder: volumeRounder, rateRounder: rateRounder, @@ -2128,6 +2144,7 @@ extension LoopDataManager { "insulinOnBoard: \(String(describing: manager.insulinOnBoard))", "error: \(String(describing: state.error))", "overrideInUserDefaults: \(String(describing: UserDefaults.appGroup?.intentExtensionOverrideToSet))", + "glucoseBasedApplicationFactorEnabled: \(UserDefaults.standard.glucoseBasedApplicationFactorEnabled)", "", String(reflecting: self.retrospectiveCorrection), "", diff --git a/Loop/Views/GlucoseBasedApplicationFactorSelectionView.swift b/Loop/Views/GlucoseBasedApplicationFactorSelectionView.swift new file mode 100644 index 0000000000..2b5556355b --- /dev/null +++ b/Loop/Views/GlucoseBasedApplicationFactorSelectionView.swift @@ -0,0 +1,64 @@ +// +// GlucoseBasedApplicationFactorSelectionView.swift +// Loop +// +// Created by Jonas Björkert on 2023-06-04. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation +import SwiftUI +import LoopKit +import LoopKitUI + +public struct GlucoseBasedApplicationFactorSelectionView: View { + @Binding var isGlucoseBasedApplicationFactorEnabled: Bool + var automaticDosingStrategy: AutomaticDosingStrategy + + public init(isGlucoseBasedApplicationFactorEnabled: Binding, automaticDosingStrategy: AutomaticDosingStrategy) { + self.automaticDosingStrategy = automaticDosingStrategy + self._isGlucoseBasedApplicationFactorEnabled = isGlucoseBasedApplicationFactorEnabled + } + + public var body: some View { + automaticBolusSection + .onChange(of: isGlucoseBasedApplicationFactorEnabled) { newValue in + UserDefaults.standard.glucoseBasedApplicationFactorEnabled = newValue + } + } + + private var automaticBolusSection: some View { + VStack(spacing: 10) { + Text(NSLocalizedString("Glucose Based Partial Application", comment: "Title for glucose based partial application experiment description")) + .font(.headline) + .padding(.bottom, 20) + + Divider() + + if automaticDosingStrategy == .automaticBolus { + Text(NSLocalizedString("Loop normally gives 40% of your predicted insulin needs each dosing cycle.\n\nWhen Glucose Based Partial Application is enabled, this experimental feature varies the percentage of recommended bolus delivered each cycle with glucose level.\n\nNear correction range, it will use 20% (similar to Temp Basal), and gradually increase to a maximum of 80% at high glucose (200 mg/dL, 11.1 mmol/L).\n\nPlease be aware that during fast rising glucose, such as after an unannounced meal, this feature, combined with Loop's velocity and retrospective correction effects, may result in a larger dose than your ISF would call for.", comment: "Description of Glucose Based Partial Application toggle.")) + .foregroundColor(.secondary) + Divider() + + HStack { + Toggle(NSLocalizedString("Enable Glucose Based Partial Application", comment: "Title for Glucose Based Partial Application toggle"), isOn: $isGlucoseBasedApplicationFactorEnabled) + Spacer() + } + .padding(.top, 20) + } else { + Text(NSLocalizedString("This option only applies when Loop's Dosing Strategy is set to Automatic Bolus.", comment: "String shown when glucose based partial application cannot be enabled because dosing strategy is not set to Automatic Bolus")) + } + + Spacer() + } + .padding() + } +} + +struct GlucoseBasedApplicationFactorSelectionView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + GlucoseBasedApplicationFactorSelectionView(isGlucoseBasedApplicationFactorEnabled: .constant(true), automaticDosingStrategy: .automaticBolus) + } + } +} diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index bd4e86e8de..afdc404fcc 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -30,7 +30,7 @@ public struct SettingsView: View { @State private var therapySettingsIsPresented: Bool = false @State private var deletePumpDataAlertIsPresented = false @State private var deleteCGMDataAlertIsPresented = false - + var localizedAppNameAndVersion: String public init(viewModel: SettingsViewModel, localizedAppNameAndVersion: String) { @@ -224,6 +224,10 @@ extension SettingsView { ForEach(pluginMenuItems.filter {$0.section == .configuration}) { item in item.view } + + if FeatureFlags.allowAlgorithmExperiments { + algorithmExperimentsSection + } } }