diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 2871a59de..6e9ab1253 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -16,11 +16,13 @@ 656F8C102E49F36F0008DC1D /* QRCodeDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */; }; 656F8C122E49F3780008DC1D /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */; }; 656F8C142E49F3D20008DC1D /* RemoteCommandSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */; }; - 65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; }; 6584B1012E4A263900135D4D /* TOTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6584B1002E4A263900135D4D /* TOTPService.swift */; }; + 65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; }; 65E8A2862E44B0300065037B /* VolumeButtonHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */; }; DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; }; DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; }; + DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */; }; + DD026E5B2EA2C9C300A39CB5 /* InsulinFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD026E5A2EA2C9C300A39CB5 /* InsulinFormatter.swift */; }; DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */; }; DD0650EB2DCE8385004D3B41 /* LowBGCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */; }; DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */; }; @@ -408,12 +410,14 @@ 656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeDisplayView.swift; sourceTree = ""; }; 656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = ""; }; 656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCommandSettings.swift; sourceTree = ""; }; - 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = ""; }; 6584B1002E4A263900135D4D /* TOTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPService.swift; sourceTree = ""; }; + 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = ""; }; 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtonHandler.swift; sourceTree = ""; }; A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = ""; }; DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = ""; }; + DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinPrecisionManager.swift; sourceTree = ""; }; + DD026E5A2EA2C9C300A39CB5 /* InsulinFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinFormatter.swift; sourceTree = ""; }; DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGSection.swift; sourceTree = ""; }; DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBGCondition.swift; sourceTree = ""; }; DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBgAlarmEditor.swift; sourceTree = ""; }; @@ -1505,6 +1509,8 @@ FCC688542489367300A0279D /* Helpers */ = { isa = PBXGroup; children = ( + DD026E5A2EA2C9C300A39CB5 /* InsulinFormatter.swift */, + DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */, 656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */, DD4A407D2E6AFEE6007B318B /* AuthService.swift */, DD1D52B82E1EB5DC00432050 /* TabPosition.swift */, @@ -1971,6 +1977,7 @@ DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */, DDEF503A2D31615000999A5D /* LogManager.swift in Sources */, DD4878172C7B75350048F05C /* BolusView.swift in Sources */, + DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */, DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */, DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */, FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */, @@ -2085,6 +2092,7 @@ DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */, DD58171C2D299F940041FB98 /* BluetoothDevice.swift in Sources */, DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */, + DD026E5B2EA2C9C300A39CB5 /* InsulinFormatter.swift in Sources */, DD5334B02D1447C500CDD6EA /* BLEManager.swift in Sources */, DD4878032C7B297E0048F05C /* StorageValue.swift in Sources */, DD4878192C7C56D60048F05C /* TrioNightscoutRemoteController.swift in Sources */, diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index db64980bd..991bf5f55 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -3,6 +3,7 @@ import Charts import Foundation +import HealthKit import UIKit extension MainViewController { @@ -96,6 +97,13 @@ extension MainViewController { .withDashSeparatorInDate, .withColonSeparatorInTime] if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String: AnyObject]? { + if let bolusIncrement = lastPumpRecord["bolusIncrement"] as? Double, bolusIncrement > 0 { + Storage.shared.bolusIncrement.value = HKQuantity(unit: .internationalUnit(), doubleValue: bolusIncrement) + Storage.shared.bolusIncrementDetected.value = true + } else { + Storage.shared.bolusIncrementDetected.value = false + } + if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 { if let reservoirData = lastPumpRecord["reservoir"] as? Double { latestPumpVolume = reservoirData diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index 1756c6cac..4b33ca6ae 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -45,6 +45,7 @@ extension MainViewController { updatedTime = parsedTime let formattedTime = Localizer.formatTimestampToLocalString(parsedTime) infoManager.updateInfoData(type: .updated, value: formattedTime) + Observable.shared.enactedOrSuggested.value = updatedTime } // ISF @@ -122,6 +123,14 @@ extension MainViewController { infoManager.updateInfoData(type: .autosens, value: formattedSens) } + // Recommended Bolus + if let rec = InsulinMetric(from: lastLoopRecord, key: "recommendedBolus") { + infoManager.updateInfoData(type: .recBolus, value: rec) + Observable.shared.deviceRecBolus.value = rec.value + } else { + Observable.shared.deviceRecBolus.value = nil + } + // Eventual BG if let eventualBGValue = enactedOrSuggested["eventualBG"] as? Double { let eventualBGQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: eventualBGValue) diff --git a/LoopFollow/Extensions/HKUnit+Extensions.swift b/LoopFollow/Extensions/HKUnit+Extensions.swift index 9eccbab84..62e2a390d 100644 --- a/LoopFollow/Extensions/HKUnit+Extensions.swift +++ b/LoopFollow/Extensions/HKUnit+Extensions.swift @@ -18,7 +18,7 @@ extension HKUnit { case .millimolesPerLiter: return 1 case .internationalUnit(): - return 3 + return InsulinPrecisionManager.shared.fractionDigits default: return 0 } diff --git a/LoopFollow/Helpers/InsulinFormatter.swift b/LoopFollow/Helpers/InsulinFormatter.swift new file mode 100644 index 000000000..bbd165078 --- /dev/null +++ b/LoopFollow/Helpers/InsulinFormatter.swift @@ -0,0 +1,24 @@ +// LoopFollow +// InsulinFormatter.swift + +import Foundation +import HealthKit + +final class InsulinFormatter { + static let shared = InsulinFormatter() + private let precision = InsulinPrecisionManager.shared + private init() {} + + func string(_ q: HKQuantity) -> String { + string(q.doubleValue(for: .internationalUnit())) + } + + func string(_ units: Double) -> String { + let fd = precision.fractionDigits + let nf = NumberFormatter() + nf.minimumFractionDigits = fd + nf.maximumFractionDigits = fd + nf.numberStyle = .decimal + return nf.string(from: NSNumber(value: units)) ?? String(format: "%.\(fd)f", units) + } +} diff --git a/LoopFollow/Helpers/InsulinPrecisionManager.swift b/LoopFollow/Helpers/InsulinPrecisionManager.swift new file mode 100644 index 000000000..583964f7c --- /dev/null +++ b/LoopFollow/Helpers/InsulinPrecisionManager.swift @@ -0,0 +1,34 @@ +// LoopFollow +// InsulinPrecisionManager.swift + +import Combine +import Foundation +import HealthKit + +final class InsulinPrecisionManager: ObservableObject { + static let shared = InsulinPrecisionManager() + + @Published private(set) var fractionDigits: Int = 3 + private var cancellables = Set() + + private init() { + fractionDigits = Self.computeDigits(from: Storage.shared.bolusIncrement.value) + + Storage.shared.bolusIncrement.$value + .map(Self.computeDigits(from:)) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .assign(to: &$fractionDigits) + } + + private static func computeDigits(from q: HKQuantity) -> Int { + let step = max(0.001, q.doubleValue(for: .internationalUnit())) + if step >= 1 { return 0 } + var v = step + var d = 0 + while d < 6 && abs(round(v) - v) > 1e-10 { + v *= 10; d += 1 + } + return min(max(d, 0), 5) + } +} diff --git a/LoopFollow/Helpers/Views/HKQuantityInputView.swift b/LoopFollow/Helpers/Views/HKQuantityInputView.swift index 4e0670da0..674a6da88 100644 --- a/LoopFollow/Helpers/Views/HKQuantityInputView.swift +++ b/LoopFollow/Helpers/Views/HKQuantityInputView.swift @@ -13,9 +13,10 @@ struct HKQuantityInputView: View { var minValue: HKQuantity var maxValue: HKQuantity @FocusState.Binding var isFocused: Bool - var onValidationError: (String) -> Void + @ObservedObject private var insulinPrecision = InsulinPrecisionManager.shared + var body: some View { HStack { Text(label) diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 1214dcbcf..ecc1ce30b 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -7,6 +7,7 @@ import SwiftUI struct RemoteSettingsView: View { @ObservedObject var viewModel: RemoteSettingsViewModel @ObservedObject private var device = Storage.shared.device + @ObservedObject var bolusIncrement = Storage.shared.bolusIncrement @State private var showAlert: Bool = false @State private var alertType: AlertType? = nil @@ -109,6 +110,29 @@ struct RemoteSettingsView: View { guardrailsSection } + if !Storage.shared.bolusIncrementDetected.value { + Section(header: Text("Bolus Increment")) { + HStack { + Text("Increment") + Spacer() + TextFieldWithToolBar( + quantity: $bolusIncrement.value, + maxLength: 5, + unit: HKUnit.internationalUnit(), + allowDecimalSeparator: true, + minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.001), + maxValue: HKQuantity(unit: .internationalUnit(), doubleValue: 1), + onValidationError: { message in + handleValidationError(message) + } + ) + .frame(width: 100) + Text("U") + .foregroundColor(.secondary) + } + } + } + // MARK: - User Information Section if viewModel.remoteType != .none && viewModel.remoteType != .loopAPNS { @@ -160,9 +184,12 @@ struct RemoteSettingsView: View { Section(header: Text("Debug / Info")) { Text("Device Token: \(Storage.shared.deviceToken.value)") - Text("Production Env.: \(Storage.shared.productionEnvironment.value ? "True" : "False")") + Text("APNS Environment: \(Storage.shared.productionEnvironment.value ? "Production" : "Development")") Text("Team ID: \(Storage.shared.teamId.value ?? "")") Text("Bundle ID: \(Storage.shared.bundleId.value)") + if Storage.shared.bolusIncrementDetected.value { + Text("Bolus Increment: \(Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit()), specifier: "%.3f") U") + } } } @@ -260,6 +287,9 @@ struct RemoteSettingsView: View { Text("TOTP Code: Invalid QR code URL") .foregroundColor(.red) } + if Storage.shared.bolusIncrementDetected.value { + Text("Bolus Increment: \(Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit()), specifier: "%.3f") U") + } } if viewModel.areTeamIdsDifferent { diff --git a/LoopFollow/Remote/TRC/BolusView.swift b/LoopFollow/Remote/TRC/BolusView.swift index ed4669829..f6f504ef6 100644 --- a/LoopFollow/Remote/TRC/BolusView.swift +++ b/LoopFollow/Remote/TRC/BolusView.swift @@ -7,36 +7,70 @@ import SwiftUI struct BolusView: View { @Environment(\.presentationMode) private var presentationMode + @State private var bolusAmount = HKQuantity(unit: .internationalUnit(), doubleValue: 0.0) - private let pushNotificationManager = PushNotificationManager() @ObservedObject private var maxBolus = Storage.shared.maxBolus + @ObservedObject private var bolusIncrement = Storage.shared.bolusIncrement - @FocusState private var bolusFieldIsFocused: Bool + @ObservedObject private var deviceRecBolus = Observable.shared.deviceRecBolus + @ObservedObject private var enactedOrSuggested = Observable.shared.enactedOrSuggested + @FocusState private var bolusFieldIsFocused: Bool @State private var showAlert = false @State private var alertType: AlertType? = nil @State private var alertMessage: String? = nil @State private var isLoading = false @State private var statusMessage: String? = nil + private let pushNotificationManager = PushNotificationManager() + enum AlertType { case confirmBolus case statusSuccess case statusFailure case validation + case oldCalculationWarning + } + + // MARK: - Step/precision helpers driven by stored increment + + private var stepU: Double { + max(0.001, bolusIncrement.value.doubleValue(for: .internationalUnit())) + } + + private var stepFractionDigits: Int { + let inc = stepU + if inc >= 1 { return 0 } + var v = inc + var digits = 0 + while digits < 6 && abs(round(v) - v) > 1e-10 { + v *= 10; digits += 1 + } + return min(max(digits, 0), 5) + } + + private func roundedToStep(_ value: Double) -> Double { + guard stepU > 0 else { return value } + let stepped = (value / stepU).rounded() * stepU + let p = pow(10.0, Double(stepFractionDigits)) + return (stepped * p).rounded() / p } + // MARK: - View + var body: some View { NavigationView { - VStack { + TimelineView(.periodic(from: .now, by: 1)) { context in Form { + recommendedBlocks(now: context.date) + Section { HKQuantityInputView( label: "Bolus Amount", quantity: $bolusAmount, unit: .internationalUnit(), - maxLength: 4, - minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.05), + maxLength: 5, + minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0), maxValue: maxBolus.value, isFocused: $bolusFieldIsFocused, onValidationError: { message in @@ -52,7 +86,7 @@ struct BolusView: View { action: { bolusFieldIsFocused = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - if bolusAmount.doubleValue(for: HKUnit.internationalUnit()) > 0.0 { + if bolusAmount.doubleValue(for: .internationalUnit()) > 0.0 { alertType = .confirmBolus showAlert = true } @@ -69,7 +103,7 @@ struct BolusView: View { case .confirmBolus: return Alert( title: Text("Confirm Bolus"), - message: Text("Are you sure you want to send \(bolusAmount.doubleValue(for: HKUnit.internationalUnit()), specifier: "%.2f") U?"), + message: Text("Are you sure you want to send \(InsulinFormatter.shared.string(bolusAmount)) U?"), primaryButton: .default(Text("Confirm"), action: { AuthService.authenticate(reason: "Confirm your identity to send bolus.") { result in if case .success = result { @@ -99,6 +133,17 @@ struct BolusView: View { message: Text(alertMessage ?? "Invalid input."), dismissButton: .default(Text("OK")) ) + case .oldCalculationWarning: + return Alert( + title: Text("Old Calculation Warning"), + message: Text(alertMessage ?? ""), + primaryButton: .default(Text("Use Anyway")) { + if let rec = deviceRecBolus.value, rec >= stepU { + applyRecommendedBolus(rec) + } + }, + secondaryButton: .cancel() + ) case .none: return Alert(title: Text("Unknown Alert")) } @@ -106,20 +151,104 @@ struct BolusView: View { } } + // MARK: - Recommended bolus UI + + @ViewBuilder + private func recommendedBlocks(now: Date) -> some View { + if let rec = deviceRecBolus.value, + rec >= stepU, + let t = enactedOrSuggested.value + { + let ageSec = max(0, now.timeIntervalSince1970 - t) + if ageSec < 12 * 60 { + let mins = Int(ageSec / 60) + let isStale5 = ageSec >= 5 * 60 + + Section(header: Text("Recommended Bolus")) { + Button { + handleRecommendedBolusTap(rec: rec, ageSec: ageSec) + } label: { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("\(InsulinFormatter.shared.string(rec))U") + Text("Calculated \(mins) minute\(mins == 1 ? "" : "s") ago") + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + Image(systemName: "arrow.up.circle.fill") + .font(.title2) + } + .padding(.vertical, 8) + } + .buttonStyle(PlainButtonStyle()) + } + + Section { + let color: Color = isStale5 ? .red : .yellow + Text("WARNING: New treatments may have occurred since the last recommended bolus was calculated \(presentableMinutesFormat(timeInterval: ageSec)) ago.") + .font(.callout) + .foregroundColor(color) + .multilineTextAlignment(.leading) + } + } else { + EmptyView() + } + } else { + EmptyView() + } + } + + private func handleRecommendedBolusTap(rec: Double, ageSec: TimeInterval) { + let isStale5 = ageSec >= 5 * 60 + let isStale12 = ageSec >= 12 * 60 + if isStale12 { return } + if isStale5 { + let mins = Int(ageSec / 60) + alertMessage = "This recommended bolus was calculated \(mins) minutes ago. New treatments may have occurred since then. Proceed with caution." + alertType = .oldCalculationWarning + showAlert = true + } else { + applyRecommendedBolus(rec) + } + } + + private func applyRecommendedBolus(_ rec: Double) { + guard rec >= stepU else { return } + let maxU = maxBolus.value.doubleValue(for: .internationalUnit()) + let clamped = min(rec, maxU) + let stepped = roundedToStep(clamped) + bolusAmount = HKQuantity(unit: .internationalUnit(), doubleValue: stepped) + } + + private func presentableMinutesFormat(timeInterval: TimeInterval) -> String { + let minutes = max(0, Int(timeInterval / 60)) + var s = "\(minutes) minute" + if minutes == 0 || minutes > 1 { s += "s" } + return s + } + + // MARK: - Send + private func sendBolus() { isLoading = true - pushNotificationManager.sendBolusPushNotification(bolusAmount: bolusAmount) { success, errorMessage in DispatchQueue.main.async { isLoading = false if success { statusMessage = "Bolus command sent successfully." - LogManager.shared.log(category: .apns, message: "sendBolusPushNotification succeeded - Bolus: \(bolusAmount.doubleValue(for: .internationalUnit())) U") + LogManager.shared.log( + category: .apns, + message: "sendBolusPushNotification succeeded - Bolus: \(InsulinFormatter.shared.string(bolusAmount)) U" + ) bolusAmount = HKQuantity(unit: .internationalUnit(), doubleValue: 0.0) alertType = .statusSuccess } else { statusMessage = errorMessage ?? "Failed to send bolus command." - LogManager.shared.log(category: .apns, message: "sendBolusPushNotification failed with error: \(errorMessage ?? "unknown error")") + LogManager.shared.log( + category: .apns, + message: "sendBolusPushNotification failed with error: \(errorMessage ?? "unknown error")" + ) alertType = .statusFailure } showAlert = true diff --git a/LoopFollow/Remote/TRC/MealView.swift b/LoopFollow/Remote/TRC/MealView.swift index ef9b096ed..517db1811 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -93,7 +93,7 @@ struct MealView: View { quantity: $bolusAmount, unit: .internationalUnit(), maxLength: 4, - minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.05), + minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0), maxValue: maxBolus.value, isFocused: $bolusFieldIsFocused, onValidationError: { message in diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 992dcc1ff..8ba0ccb84 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -33,6 +33,7 @@ class Observable { var alertLastLoopTime = ObservableValue(default: nil) var deviceRecBolus = ObservableValue(default: nil) var deviceBatteryLevel = ObservableValue(default: nil) + var enactedOrSuggested = ObservableValue(default: nil) var settingsPath = ObservableValue(default: NavigationPath()) diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 64b82bb18..42e9ede2a 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -170,6 +170,9 @@ class Storage { var returnApnsKey = StorageValue(key: "returnApnsKey", defaultValue: "") var returnKeyId = StorageValue(key: "returnKeyId", defaultValue: "") + var bolusIncrement = SecureStorageValue(key: "bolusIncrement", defaultValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.05)) + var bolusIncrementDetected = StorageValue(key: "bolusIncrementDetected", defaultValue: false) + static let shared = Storage() private init() {} }