From d457a454fb4930d1b6809d5ef27082229959ed66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 11 Apr 2026 15:55:56 +0200 Subject: [PATCH 1/3] Common remote commands --- LoopFollow.xcodeproj/project.pbxproj | 36 ++++- .../CommonBoluses/CommonBolusesManager.swift | 133 +++++++++++++++++ .../RemoteBolusHistoryEntry.swift | 27 ++++ .../CommonMeals/CommonMealsManager.swift | 139 ++++++++++++++++++ .../CommonMeals/RemoteMealHistoryEntry.swift | 39 +++++ .../Remote/LoopAPNS/LoopAPNSBolusView.swift | 33 +++++ .../Remote/LoopAPNS/LoopAPNSCarbsView.swift | 35 +++++ LoopFollow/Remote/TRC/BolusView.swift | 42 ++++++ LoopFollow/Remote/TRC/MealView.swift | 64 ++++++++ LoopFollow/Storage/Storage.swift | 5 + 10 files changed, 551 insertions(+), 2 deletions(-) create mode 100644 LoopFollow/Remote/CommonBoluses/CommonBolusesManager.swift create mode 100644 LoopFollow/Remote/CommonBoluses/RemoteBolusHistoryEntry.swift create mode 100644 LoopFollow/Remote/CommonMeals/CommonMealsManager.swift create mode 100644 LoopFollow/Remote/CommonMeals/RemoteMealHistoryEntry.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 9a11022d6..36090bfd0 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + B500000000000000000000A2 /* RemoteBolusHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */; }; + B500000000000000000000A4 /* CommonBolusesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A3 /* CommonBolusesManager.swift */; }; + B500000000000000000000B2 /* RemoteMealHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */; }; + B500000000000000000000B4 /* CommonMealsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B3 /* CommonMealsManager.swift */; }; 2D8068C66833EEAED7B4BEB8 /* FutureCarbsCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */; }; 374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77982F5BD8AB00E96858 /* APNSClient.swift */; }; 374A77A52F5BE17000E96858 /* AppGroupID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A779F2F5BE17000E96858 /* AppGroupID.swift */; }; @@ -195,8 +199,8 @@ DD9ED0CA2D355257000D2A63 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0C92D355256000D2A63 /* LogView.swift */; }; DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */; }; DD9ED0CE2D35587A000D2A63 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0CD2D355879000D2A63 /* LogEntry.swift */; }; - DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */; }; DDA9ACA62D6A66D000E6F1A9 /* ContactColorMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA52D6A66C800E6F1A9 /* ContactColorMode.swift */; }; + DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */; }; DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */; }; DDA9ACAC2D6B317100E6F1A9 /* ContactType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */; }; DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */; }; @@ -459,6 +463,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteBolusHistoryEntry.swift; sourceTree = ""; }; + B500000000000000000000A3 /* CommonBolusesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonBolusesManager.swift; sourceTree = ""; }; + B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMealHistoryEntry.swift; sourceTree = ""; }; + B500000000000000000000B3 /* CommonMealsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonMealsManager.swift; sourceTree = ""; }; 059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = ""; }; 2B9BEC26E4E48EF9B811A372 /* PendingFutureCarb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingFutureCarb.swift; sourceTree = ""; }; 2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureCarbsCondition.swift; sourceTree = ""; }; @@ -643,8 +651,8 @@ DD9ED0C92D355256000D2A63 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = ""; }; DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; DD9ED0CD2D355879000D2A63 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = ""; }; - DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorOption.swift; sourceTree = ""; }; DDA9ACA52D6A66C800E6F1A9 /* ContactColorMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorMode.swift; sourceTree = ""; }; + DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorOption.swift; sourceTree = ""; }; DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactIncludeOption.swift; sourceTree = ""; }; DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactType.swift; sourceTree = ""; }; DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkHeartbeatBluetoothDevice.swift; sourceTree = ""; }; @@ -1034,9 +1042,29 @@ path = Metric; sourceTree = ""; }; + B500000000000000000000B5 /* CommonMeals */ = { + isa = PBXGroup; + children = ( + B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */, + B500000000000000000000B3 /* CommonMealsManager.swift */, + ); + path = CommonMeals; + sourceTree = ""; + }; + B500000000000000000000A5 /* CommonBoluses */ = { + isa = PBXGroup; + children = ( + B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */, + B500000000000000000000A3 /* CommonBolusesManager.swift */, + ); + path = CommonBoluses; + sourceTree = ""; + }; DD0C0C6E2C4AFFB800DBADDF /* Remote */ = { isa = PBXGroup; children = ( + B500000000000000000000A5 /* CommonBoluses */, + B500000000000000000000B5 /* CommonMeals */, DDDF6F4A2D479B6A00884336 /* Nightscout */, DDDF6F482D479AEF00884336 /* NoRemoteView.swift */, DDEF503E2D479B8A00884336 /* LoopAPNS */, @@ -2186,6 +2214,10 @@ 6584B1012E4A263900135D4D /* TOTPService.swift in Sources */, DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */, DDEF503A2D31615000999A5D /* LogManager.swift in Sources */, + B500000000000000000000A2 /* RemoteBolusHistoryEntry.swift in Sources */, + B500000000000000000000A4 /* CommonBolusesManager.swift in Sources */, + B500000000000000000000B2 /* RemoteMealHistoryEntry.swift in Sources */, + B500000000000000000000B4 /* CommonMealsManager.swift in Sources */, DD4878172C7B75350048F05C /* BolusView.swift in Sources */, DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */, 374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */, diff --git a/LoopFollow/Remote/CommonBoluses/CommonBolusesManager.swift b/LoopFollow/Remote/CommonBoluses/CommonBolusesManager.swift new file mode 100644 index 000000000..16f30b5da --- /dev/null +++ b/LoopFollow/Remote/CommonBoluses/CommonBolusesManager.swift @@ -0,0 +1,133 @@ +// LoopFollow +// CommonBolusesManager.swift + +import Foundation + +struct CommonBolus: Identifiable, Equatable { + let id = UUID() + let units: Double + + static func == (lhs: CommonBolus, rhs: CommonBolus) -> Bool { + lhs.id == rhs.id + } +} + +final class CommonBolusesManager: ObservableObject { + static let shared = CommonBolusesManager() + + @Published private(set) var commonBoluses: [CommonBolus] = [] + + private static let maxEntries = 500 + private static let maxAgeDays = 90.0 + private static let sigma: Double = 60.0 + private static let halfLife: Double = 10.0 + private static let minScore: Double = 0.1 + private static let maxResults = 5 + + private init() {} + + // MARK: - Public API + + func recordBolus(units: Double, at date: Date = Date()) { + let entry = RemoteBolusHistoryEntry(units: units, date: date) + var history = Storage.shared.remoteBolusHistory.value + history.append(entry) + history = Self.pruned(history, now: date) + Storage.shared.remoteBolusHistory.value = history + } + + func refresh(now: Date = Date(), stepIncrement: Double, maxBolus: Double) { + let history = Storage.shared.remoteBolusHistory.value + commonBoluses = Self.computeCommonBoluses( + from: history, + now: now, + stepIncrement: stepIncrement, + maxBolus: maxBolus + ) + } + + // MARK: - Scoring (static for testability) + + static func computeCommonBoluses( + from history: [RemoteBolusHistoryEntry], + now: Date, + stepIncrement: Double, + maxBolus: Double + ) -> [CommonBolus] { + guard stepIncrement > 0 else { return [] } + + let nowMinute = { + let cal = Calendar.current + return cal.component(.hour, from: now) * 60 + cal.component(.minute, from: now) + }() + let nowDOW = Calendar.current.component(.weekday, from: now) + + var groups: [Double: Double] = [:] + + for entry in history { + let rounded = (entry.units / stepIncrement).rounded(.down) * stepIncrement + let amount = roundToFraction(rounded, stepIncrement: stepIncrement) + guard amount > 0, amount <= maxBolus else { continue } + + let t = timeOfDayScore(entryMinute: entry.minuteOfDay, nowMinute: nowMinute) + let d = dayOfWeekScore(entryDOW: entry.dayOfWeek, nowDOW: nowDOW) + let daysAgo = now.timeIntervalSince(entry.date) / 86400.0 + let r = recencyScore(daysAgo: daysAgo) + + groups[amount, default: 0] += t * d * r + } + + return groups + .filter { $0.value >= minScore } + .sorted { $0.value > $1.value } + .prefix(maxResults) + .map { CommonBolus(units: $0.key) } + } + + static func timeOfDayScore(entryMinute: Int, nowMinute: Int) -> Double { + let diff = abs(entryMinute - nowMinute) + let circularDiff = Double(min(diff, 1440 - diff)) + return exp(-(circularDiff * circularDiff) / (2 * sigma * sigma)) + } + + static func dayOfWeekScore(entryDOW: Int, nowDOW: Int) -> Double { + if entryDOW == nowDOW { return 1.0 } + let nowWeekend = nowDOW == 1 || nowDOW == 7 + let entryWeekend = entryDOW == 1 || entryDOW == 7 + if nowWeekend == entryWeekend { return 0.7 } + return 0.15 + } + + static func recencyScore(daysAgo: Double) -> Double { + pow(0.5, daysAgo / halfLife) + } + + // MARK: - Helpers + + private static func pruned(_ history: [RemoteBolusHistoryEntry], now: Date) -> [RemoteBolusHistoryEntry] { + let cutoff = now.addingTimeInterval(-maxAgeDays * 86400) + var filtered = history.filter { $0.date > cutoff } + if filtered.count > maxEntries { + filtered.sort { $0.date > $1.date } + filtered = Array(filtered.prefix(maxEntries)) + } + return filtered + } + + private static func roundToFraction(_ value: Double, stepIncrement: Double) -> Double { + let digits = fractionDigits(for: stepIncrement) + let p = pow(10.0, Double(digits)) + return (value * p).rounded() / p + } + + private static func fractionDigits(for step: Double) -> Int { + if step >= 1 { return 0 } + var v = step + var digits = 0 + while digits < 6, abs(v.rounded() - v) > 1e-10 { + v *= 10 + digits += 1 + } + return min(max(digits, 0), 5) + } +} diff --git a/LoopFollow/Remote/CommonBoluses/RemoteBolusHistoryEntry.swift b/LoopFollow/Remote/CommonBoluses/RemoteBolusHistoryEntry.swift new file mode 100644 index 000000000..896b7459b --- /dev/null +++ b/LoopFollow/Remote/CommonBoluses/RemoteBolusHistoryEntry.swift @@ -0,0 +1,27 @@ +// LoopFollow +// RemoteBolusHistoryEntry.swift + +import Foundation + +/// A record of a remotely-sent bolus, stored locally for pattern-based suggestions. +struct RemoteBolusHistoryEntry: Codable, Equatable { + /// Bolus amount in international units + let units: Double + + /// When the bolus was sent + let date: Date + + /// Day of week: 1=Sunday ... 7=Saturday (Calendar.component(.weekday)) + let dayOfWeek: Int + + /// Minute of day: 0...1439 (hour * 60 + minute) + let minuteOfDay: Int + + init(units: Double, date: Date) { + self.units = units + self.date = date + let cal = Calendar.current + dayOfWeek = cal.component(.weekday, from: date) + minuteOfDay = cal.component(.hour, from: date) * 60 + cal.component(.minute, from: date) + } +} diff --git a/LoopFollow/Remote/CommonMeals/CommonMealsManager.swift b/LoopFollow/Remote/CommonMeals/CommonMealsManager.swift new file mode 100644 index 000000000..223e277f7 --- /dev/null +++ b/LoopFollow/Remote/CommonMeals/CommonMealsManager.swift @@ -0,0 +1,139 @@ +// LoopFollow +// CommonMealsManager.swift + +import Foundation + +struct CommonMeal: Identifiable, Equatable { + let id = UUID() + let carbs: Double + let fat: Double + let protein: Double + let bolus: Double + + static func == (lhs: CommonMeal, rhs: CommonMeal) -> Bool { + lhs.id == rhs.id + } +} + +final class CommonMealsManager: ObservableObject { + static let shared = CommonMealsManager() + + @Published private(set) var commonMeals: [CommonMeal] = [] + + private static let maxEntries = 500 + private static let maxAgeDays = 90.0 + private static let sigma: Double = 60.0 + private static let halfLife: Double = 10.0 + private static let minScore: Double = 0.1 + private static let maxResults = 5 + + private init() {} + + // MARK: - Public API + + func recordMeal(carbs: Double, fat: Double = 0, protein: Double = 0, bolus: Double = 0, at date: Date = Date()) { + let entry = RemoteMealHistoryEntry(carbs: carbs, fat: fat, protein: protein, bolus: bolus, date: date) + var history = Storage.shared.remoteMealHistory.value + history.append(entry) + history = Self.pruned(history, now: date) + Storage.shared.remoteMealHistory.value = history + } + + func refresh(now: Date = Date(), carbStep: Double = 1.0, maxCarbs: Double, includeFatProtein: Bool) { + let history = Storage.shared.remoteMealHistory.value + commonMeals = Self.computeCommonMeals( + from: history, + now: now, + carbStep: carbStep, + maxCarbs: maxCarbs, + includeFatProtein: includeFatProtein + ) + } + + // MARK: - Scoring (static for testability) + + static func computeCommonMeals( + from history: [RemoteMealHistoryEntry], + now: Date, + carbStep: Double, + maxCarbs: Double, + includeFatProtein: Bool + ) -> [CommonMeal] { + guard carbStep > 0 else { return [] } + + let nowMinute = { + let cal = Calendar.current + return cal.component(.hour, from: now) * 60 + cal.component(.minute, from: now) + }() + let nowDOW = Calendar.current.component(.weekday, from: now) + + // Group by rounded carbs; track score + best entry (highest scored) for fat/protein + var groupScores: [Double: Double] = [:] + var groupBestEntry: [Double: (entry: RemoteMealHistoryEntry, score: Double)] = [:] + + for entry in history { + let rounded = (entry.carbs / carbStep).rounded() * carbStep + guard rounded > 0, rounded <= maxCarbs else { continue } + + let t = timeOfDayScore(entryMinute: entry.minuteOfDay, nowMinute: nowMinute) + let d = dayOfWeekScore(entryDOW: entry.dayOfWeek, nowDOW: nowDOW) + let daysAgo = now.timeIntervalSince(entry.date) / 86400.0 + let r = recencyScore(daysAgo: daysAgo) + + let score = t * d * r + groupScores[rounded, default: 0] += score + + if let current = groupBestEntry[rounded] { + if score > current.score { + groupBestEntry[rounded] = (entry, score) + } + } else { + groupBestEntry[rounded] = (entry, score) + } + } + + return groupScores + .filter { $0.value >= minScore } + .sorted { $0.value > $1.value } + .prefix(maxResults) + .map { item in + let best = groupBestEntry[item.key]?.entry + return CommonMeal( + carbs: item.key, + fat: includeFatProtein ? (best?.fat ?? 0) : 0, + protein: includeFatProtein ? (best?.protein ?? 0) : 0, + bolus: best?.bolus ?? 0 + ) + } + } + + static func timeOfDayScore(entryMinute: Int, nowMinute: Int) -> Double { + let diff = abs(entryMinute - nowMinute) + let circularDiff = Double(min(diff, 1440 - diff)) + return exp(-(circularDiff * circularDiff) / (2 * sigma * sigma)) + } + + static func dayOfWeekScore(entryDOW: Int, nowDOW: Int) -> Double { + if entryDOW == nowDOW { return 1.0 } + let nowWeekend = nowDOW == 1 || nowDOW == 7 + let entryWeekend = entryDOW == 1 || entryDOW == 7 + if nowWeekend == entryWeekend { return 0.7 } + return 0.15 + } + + static func recencyScore(daysAgo: Double) -> Double { + pow(0.5, daysAgo / halfLife) + } + + // MARK: - Helpers + + private static func pruned(_ history: [RemoteMealHistoryEntry], now: Date) -> [RemoteMealHistoryEntry] { + let cutoff = now.addingTimeInterval(-maxAgeDays * 86400) + var filtered = history.filter { $0.date > cutoff } + if filtered.count > maxEntries { + filtered.sort { $0.date > $1.date } + filtered = Array(filtered.prefix(maxEntries)) + } + return filtered + } +} diff --git a/LoopFollow/Remote/CommonMeals/RemoteMealHistoryEntry.swift b/LoopFollow/Remote/CommonMeals/RemoteMealHistoryEntry.swift new file mode 100644 index 000000000..65ffb1e31 --- /dev/null +++ b/LoopFollow/Remote/CommonMeals/RemoteMealHistoryEntry.swift @@ -0,0 +1,39 @@ +// LoopFollow +// RemoteMealHistoryEntry.swift + +import Foundation + +/// A record of a remotely-sent meal, stored locally for pattern-based common meals. +struct RemoteMealHistoryEntry: Codable, Equatable { + /// Carbs in grams + let carbs: Double + + /// Fat in grams (0 if not applicable) + let fat: Double + + /// Protein in grams (0 if not applicable) + let protein: Double + + /// Bolus in units (0 if no bolus with meal) + let bolus: Double + + /// When the meal was sent + let date: Date + + /// Day of week: 1=Sunday ... 7=Saturday (Calendar.component(.weekday)) + let dayOfWeek: Int + + /// Minute of day: 0...1439 (hour * 60 + minute) + let minuteOfDay: Int + + init(carbs: Double, fat: Double = 0, protein: Double = 0, bolus: Double = 0, date: Date = Date()) { + self.carbs = carbs + self.fat = fat + self.protein = protein + self.bolus = bolus + self.date = date + let cal = Calendar.current + dayOfWeek = cal.component(.weekday, from: date) + minuteOfDay = cal.component(.hour, from: date) * 60 + cal.component(.minute, from: date) + } +} diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift index 011fe0e10..d8681cec0 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift @@ -13,6 +13,7 @@ struct LoopAPNSBolusView: View { @State private var alertMessage = "" @State private var alertType: AlertType = .success + @ObservedObject private var commonBoluses = CommonBolusesManager.shared @FocusState private var insulinFieldIsFocused: Bool // Add state for recommended bolus and warning @@ -72,6 +73,30 @@ struct LoopAPNSBolusView: View { } } + if !commonBoluses.commonBoluses.isEmpty { + Section(header: Text("Common Boluses")) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(commonBoluses.commonBoluses) { bolus in + Button { + insulinAmount = HKQuantity(unit: .internationalUnit(), doubleValue: bolus.units) + } label: { + Text("\(InsulinFormatter.shared.string(bolus.units))U") + .font(.subheadline.weight(.medium)) + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background(Color.accentColor.opacity(0.15)) + .foregroundColor(.accentColor) + .cornerRadius(8) + } + .buttonStyle(.plain) + } + } + .padding(.vertical, 4) + } + } + } + Section { HKQuantityInputView( label: "Insulin Amount", @@ -181,6 +206,10 @@ struct LoopAPNSBolusView: View { showAlert = true } + let step = Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit()) + let maxBolus = Storage.shared.maxBolus.value.doubleValue(for: .internationalUnit()) + commonBoluses.refresh(stepIncrement: max(0.001, step), maxBolus: maxBolus) + loadRecommendedBolus() // Reset timer state so it shows '-' until first tick otpTimeRemaining = nil @@ -369,6 +398,10 @@ struct LoopAPNSBolusView: View { DispatchQueue.main.async { self.isLoading = false if success { + let sentUnits = insulinAmount.doubleValue(for: .internationalUnit()) + if sentUnits > 0 { + CommonBolusesManager.shared.recordBolus(units: sentUnits) + } // Mark TOTP code as used TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value) self.alertMessage = "Insulin sent successfully!" diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index 45e2c403d..b934961c0 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -8,6 +8,7 @@ struct LoopAPNSCarbsView: View { private typealias AbsorptionPreset = (hours: Int, minutes: Int) @Environment(\.presentationMode) var presentationMode + @ObservedObject private var commonMeals = CommonMealsManager.shared @State private var carbsAmount = HKQuantity(unit: .gram(), doubleValue: 0.0) @State private var absorptionHours = 3 @State private var absorptionMinutes = 0 @@ -102,6 +103,30 @@ struct LoopAPNSCarbsView: View { NavigationView { VStack { Form { + if !commonMeals.commonMeals.isEmpty { + Section(header: Text("Common Meals")) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(commonMeals.commonMeals) { meal in + Button { + carbsAmount = HKQuantity(unit: .gram(), doubleValue: meal.carbs) + } label: { + Text("\(Int(meal.carbs))g") + .font(.subheadline.weight(.medium)) + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background(Color.accentColor.opacity(0.15)) + .foregroundColor(.accentColor) + .cornerRadius(8) + } + .buttonStyle(.plain) + } + } + .padding(.vertical, 4) + } + } + } + Section { HKQuantityInputView( label: "Carbs Amount", @@ -379,6 +404,12 @@ struct LoopAPNSCarbsView: View { alertType = .error showAlert = true } + + commonMeals.refresh( + maxCarbs: Storage.shared.maxCarbs.value.doubleValue(for: .gram()), + includeFatProtein: false + ) + // Reset timer state so it shows '-' until first tick otpTimeRemaining = nil // Don't reset TOTP usage flag here - let the timer handle it @@ -529,6 +560,10 @@ struct LoopAPNSCarbsView: View { DispatchQueue.main.async { self.isLoading = false if success { + let sentCarbs = carbsAmount.doubleValue(for: .gram()) + if sentCarbs > 0 { + CommonMealsManager.shared.recordMeal(carbs: sentCarbs) + } // Mark TOTP code as used TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value) let timeFormatter = DateFormatter() diff --git a/LoopFollow/Remote/TRC/BolusView.swift b/LoopFollow/Remote/TRC/BolusView.swift index 30bfab213..272a65a47 100644 --- a/LoopFollow/Remote/TRC/BolusView.swift +++ b/LoopFollow/Remote/TRC/BolusView.swift @@ -14,6 +14,7 @@ struct BolusView: View { @ObservedObject private var deviceRecBolus = Observable.shared.deviceRecBolus @ObservedObject private var enactedOrSuggested = Observable.shared.enactedOrSuggested + @ObservedObject private var commonBoluses = CommonBolusesManager.shared @FocusState private var bolusFieldIsFocused: Bool @State private var showAlert = false @@ -67,6 +68,30 @@ struct BolusView: View { Form { recommendedBlocks(now: context.date) + if !commonBoluses.commonBoluses.isEmpty { + Section(header: Text("Common Boluses")) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(commonBoluses.commonBoluses) { bolus in + Button { + applyCommonBolus(bolus.units) + } label: { + Text("\(InsulinFormatter.shared.string(bolus.units))U") + .font(.subheadline.weight(.medium)) + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background(Color.accentColor.opacity(0.15)) + .foregroundColor(.accentColor) + .cornerRadius(8) + } + .buttonStyle(.plain) + } + } + .padding(.vertical, 4) + } + } + } + Section { HKQuantityInputView( label: "Bolus Amount", @@ -105,6 +130,12 @@ struct BolusView: View { .navigationTitle("Bolus") .navigationBarTitleDisplayMode(.inline) } + .onAppear { + commonBoluses.refresh( + stepIncrement: stepU, + maxBolus: maxBolus.value.doubleValue(for: .internationalUnit()) + ) + } .alert(isPresented: $showAlert) { switch alertType { case .confirmBolus: @@ -245,6 +276,13 @@ struct BolusView: View { } } + private func applyCommonBolus(_ units: Double) { + let maxU = maxBolus.value.doubleValue(for: .internationalUnit()) + let clamped = min(units, maxU) + let stepped = roundedToStep(clamped) + bolusAmount = HKQuantity(unit: .internationalUnit(), doubleValue: stepped) + } + private func applyRecommendedBolus(_ rec: Double) { let maxU = maxBolus.value.doubleValue(for: .internationalUnit()) let clamped = min(rec, maxU) @@ -267,6 +305,10 @@ struct BolusView: View { DispatchQueue.main.async { isLoading = false if success { + let sentUnits = bolusAmount.doubleValue(for: .internationalUnit()) + if sentUnits > 0 { + CommonBolusesManager.shared.recordBolus(units: sentUnits) + } statusMessage = "Bolus command sent successfully." LogManager.shared.log( category: .apns, diff --git a/LoopFollow/Remote/TRC/MealView.swift b/LoopFollow/Remote/TRC/MealView.swift index 55dd704e6..5fbc51b12 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -20,6 +20,7 @@ struct MealView: View { @ObservedObject private var mealWithBolus = Storage.shared.mealWithBolus @ObservedObject private var mealWithFatProtein = Storage.shared.mealWithFatProtein @ObservedObject private var maxBolus = Storage.shared.maxBolus + @ObservedObject private var commonMeals = CommonMealsManager.shared @FocusState private var carbsFieldIsFocused: Bool @FocusState private var proteinFieldIsFocused: Bool @@ -46,6 +47,40 @@ struct MealView: View { NavigationView { VStack { Form { + if !commonMeals.commonMeals.isEmpty { + Section(header: Text("Common Meals")) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(commonMeals.commonMeals) { meal in + Button { + applyCommonMeal(meal) + } label: { + VStack(spacing: 2) { + Text("\(Int(meal.carbs))g") + .font(.subheadline.weight(.medium)) + if mealWithFatProtein.value, meal.fat > 0 || meal.protein > 0 { + Text("F\(Int(meal.fat)) P\(Int(meal.protein))") + .font(.caption2) + } + if mealWithBolus.value, meal.bolus > 0 { + Text("\(InsulinFormatter.shared.string(meal.bolus))U") + .font(.caption2) + } + } + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background(Color.accentColor.opacity(0.15)) + .foregroundColor(.accentColor) + .cornerRadius(8) + } + .buttonStyle(.plain) + } + } + .padding(.vertical, 4) + } + } + } + Section(header: Text("Meal Data")) { // TODO: This banner can be deleted in March 2027. Check the commit for other places to cleanup. if showFatProteinOrderBanner { @@ -173,6 +208,11 @@ struct MealView: View { selectedTime = nil isScheduling = false + commonMeals.refresh( + maxCarbs: maxCarbs.value.doubleValue(for: .gram()), + includeFatProtein: mealWithFatProtein.value + ) + if !Storage.shared.hasSeenFatProteinOrderChange.value && Storage.shared.mealWithFatProtein.value { showFatProteinOrderBanner = true } @@ -300,6 +340,15 @@ struct MealView: View { DispatchQueue.main.async { isLoading = false if success { + let sentCarbs = carbs.doubleValue(for: .gram()) + if sentCarbs > 0 { + CommonMealsManager.shared.recordMeal( + carbs: sentCarbs, + fat: fat.doubleValue(for: .gram()), + protein: protein.doubleValue(for: .gram()), + bolus: bolusAmount.doubleValue(for: .internationalUnit()) + ) + } statusMessage = "Meal command sent successfully." LogManager.shared.log( category: .apns, @@ -332,6 +381,21 @@ struct MealView: View { return formatter.string(from: date) } + private func applyCommonMeal(_ meal: CommonMeal) { + let maxC = maxCarbs.value.doubleValue(for: .gram()) + carbs = HKQuantity(unit: .gram(), doubleValue: min(meal.carbs, maxC)) + if mealWithFatProtein.value { + let maxF = maxFat.value.doubleValue(for: .gram()) + let maxP = maxProtein.value.doubleValue(for: .gram()) + fat = HKQuantity(unit: .gram(), doubleValue: min(meal.fat, maxF)) + protein = HKQuantity(unit: .gram(), doubleValue: min(meal.protein, maxP)) + } + if mealWithBolus.value, meal.bolus > 0 { + let maxB = maxBolus.value.doubleValue(for: .internationalUnit()) + bolusAmount = HKQuantity(unit: .internationalUnit(), doubleValue: min(meal.bolus, maxB)) + } + } + private func handleValidationError(_ message: String) { alertMessage = message alertType = .validationError diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 11997da35..009bfb334 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -215,6 +215,9 @@ class Storage { var bolusIncrement = SecureStorageValue(key: "bolusIncrement", defaultValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.05)) var bolusIncrementDetected = StorageValue(key: "bolusIncrementDetected", defaultValue: false) + + var remoteBolusHistory = StorageValue<[RemoteBolusHistoryEntry]>(key: "remoteBolusHistory", defaultValue: []) + var remoteMealHistory = StorageValue<[RemoteMealHistoryEntry]>(key: "remoteMealHistory", defaultValue: []) // Statistics display preferences var showGMI = StorageValue(key: "showGMI", defaultValue: true) var showStdDev = StorageValue(key: "showStdDev", defaultValue: true) @@ -404,6 +407,8 @@ class Storage { loopAPNSQrCodeURL.reload() bolusIncrementDetected.reload() + remoteBolusHistory.reload() + remoteMealHistory.reload() showGMI.reload() showStdDev.reload() showTITR.reload() From 66dd779f2fbfb4f2486afa9e309bfbf9e1cdaea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 11 Apr 2026 22:02:47 +0200 Subject: [PATCH 2/3] Help texts --- LoopFollow.xcodeproj/project.pbxproj | 4 ++++ LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift | 2 +- LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift | 2 +- LoopFollow/Remote/TRC/BolusView.swift | 2 +- LoopFollow/Remote/TRC/MealView.swift | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 36090bfd0..d096ed739 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -100,6 +100,7 @@ DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */; }; DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */; }; DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF102C997B4600FB655A /* LoadingButtonView.swift */; }; + B500000000000000000000C2 /* CommonSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000C1 /* CommonSectionHeader.swift */; }; DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52B82E1EB5DC00432050 /* TabPosition.swift */; }; DD1D52BB2E1EB60B00432050 /* MoreMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */; }; DD1D52C02E4C100000000001 /* AppearanceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BF2E4C100000000001 /* AppearanceMode.swift */; }; @@ -553,6 +554,7 @@ DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorageValue.swift; sourceTree = ""; }; DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKQuantityInputView.swift; sourceTree = ""; }; DD16AF102C997B4600FB655A /* LoadingButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonView.swift; sourceTree = ""; }; + B500000000000000000000C1 /* CommonSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSectionHeader.swift; sourceTree = ""; }; DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = ""; }; DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuViewController.swift; sourceTree = ""; }; DD1D52BF2E4C100000000001 /* AppearanceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceMode.swift; sourceTree = ""; }; @@ -1420,6 +1422,7 @@ DDF6999D2C5AAA640058A8D9 /* ErrorMessageView.swift */, DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */, DD16AF102C997B4600FB655A /* LoadingButtonView.swift */, + B500000000000000000000C1 /* CommonSectionHeader.swift */, DDE75D262DE5E539007C1FC1 /* ActionRow.swift */, 654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */, DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */, @@ -2188,6 +2191,7 @@ DDC6CA4B2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift in Sources */, 6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */, DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */, + B500000000000000000000C2 /* CommonSectionHeader.swift in Sources */, DDFF3D7F2D1414A200BF9D9E /* BLEDevice.swift in Sources */, DD9ACA042D32821400415D8A /* DeviceStatusTask.swift in Sources */, FC16A97D24996747003D6245 /* SpeakBG.swift in Sources */, diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift index d8681cec0..00851c9bb 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift @@ -74,7 +74,7 @@ struct LoopAPNSBolusView: View { } if !commonBoluses.commonBoluses.isEmpty { - Section(header: Text("Common Boluses")) { + Section(header: CommonSectionHeader(title: "Common Boluses", infoText: CommonSectionHeader.bolusInfoText)) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(commonBoluses.commonBoluses) { bolus in diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index b934961c0..65516ab3a 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -104,7 +104,7 @@ struct LoopAPNSCarbsView: View { VStack { Form { if !commonMeals.commonMeals.isEmpty { - Section(header: Text("Common Meals")) { + Section(header: CommonSectionHeader(title: "Common Meals", infoText: CommonSectionHeader.mealInfoText)) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(commonMeals.commonMeals) { meal in diff --git a/LoopFollow/Remote/TRC/BolusView.swift b/LoopFollow/Remote/TRC/BolusView.swift index 272a65a47..83c5e9341 100644 --- a/LoopFollow/Remote/TRC/BolusView.swift +++ b/LoopFollow/Remote/TRC/BolusView.swift @@ -69,7 +69,7 @@ struct BolusView: View { recommendedBlocks(now: context.date) if !commonBoluses.commonBoluses.isEmpty { - Section(header: Text("Common Boluses")) { + Section(header: CommonSectionHeader(title: "Common Boluses", infoText: CommonSectionHeader.bolusInfoText)) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(commonBoluses.commonBoluses) { bolus in diff --git a/LoopFollow/Remote/TRC/MealView.swift b/LoopFollow/Remote/TRC/MealView.swift index 5fbc51b12..0fdf02fac 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -48,7 +48,7 @@ struct MealView: View { VStack { Form { if !commonMeals.commonMeals.isEmpty { - Section(header: Text("Common Meals")) { + Section(header: CommonSectionHeader(title: "Common Meals", infoText: CommonSectionHeader.mealInfoText)) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(commonMeals.commonMeals) { meal in From 98f6c015d7090a968d3607895e6671316cbc2c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sun, 12 Apr 2026 11:59:16 +0200 Subject: [PATCH 3/3] Rename Common to Quick-Pick throughout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames all files, classes, types, and UI labels introduced by the quick-pick feature from "Common" to "Quick-Pick": - CommonBoluses/ → QuickPickBoluses/ - CommonMeals/ → QuickPickMeals/ - CommonBolusesManager → QuickPickBolusesManager - CommonMealsManager → QuickPickMealsManager - CommonSectionHeader → QuickPickSectionHeader - UI labels: "Common Boluses/Meals" → "Quick-Pick Boluses/Meals" --- LoopFollow.xcodeproj/project.pbxproj | 36 +++++----- .../Views/QuickPickSectionHeader.swift | 69 +++++++++++++++++++ .../Remote/LoopAPNS/LoopAPNSBolusView.swift | 12 ++-- .../Remote/LoopAPNS/LoopAPNSCarbsView.swift | 12 ++-- .../QuickPickBolusesManager.swift} | 20 +++--- .../RemoteBolusHistoryEntry.swift | 0 .../QuickPickMealsManager.swift} | 20 +++--- .../RemoteMealHistoryEntry.swift | 0 LoopFollow/Remote/TRC/BolusView.swift | 16 ++--- LoopFollow/Remote/TRC/MealView.swift | 16 ++--- 10 files changed, 135 insertions(+), 66 deletions(-) create mode 100644 LoopFollow/Helpers/Views/QuickPickSectionHeader.swift rename LoopFollow/Remote/{CommonBoluses/CommonBolusesManager.swift => QuickPickBoluses/QuickPickBolusesManager.swift} (88%) rename LoopFollow/Remote/{CommonBoluses => QuickPickBoluses}/RemoteBolusHistoryEntry.swift (100%) rename LoopFollow/Remote/{CommonMeals/CommonMealsManager.swift => QuickPickMeals/QuickPickMealsManager.swift} (90%) rename LoopFollow/Remote/{CommonMeals => QuickPickMeals}/RemoteMealHistoryEntry.swift (100%) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index d096ed739..3162c5b68 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -8,9 +8,9 @@ /* Begin PBXBuildFile section */ B500000000000000000000A2 /* RemoteBolusHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */; }; - B500000000000000000000A4 /* CommonBolusesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A3 /* CommonBolusesManager.swift */; }; + B500000000000000000000A4 /* QuickPickBolusesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A3 /* QuickPickBolusesManager.swift */; }; B500000000000000000000B2 /* RemoteMealHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */; }; - B500000000000000000000B4 /* CommonMealsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B3 /* CommonMealsManager.swift */; }; + B500000000000000000000B4 /* QuickPickMealsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B3 /* QuickPickMealsManager.swift */; }; 2D8068C66833EEAED7B4BEB8 /* FutureCarbsCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */; }; 374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77982F5BD8AB00E96858 /* APNSClient.swift */; }; 374A77A52F5BE17000E96858 /* AppGroupID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A779F2F5BE17000E96858 /* AppGroupID.swift */; }; @@ -100,7 +100,7 @@ DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */; }; DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */; }; DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF102C997B4600FB655A /* LoadingButtonView.swift */; }; - B500000000000000000000C2 /* CommonSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000C1 /* CommonSectionHeader.swift */; }; + B500000000000000000000C2 /* QuickPickSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000C1 /* QuickPickSectionHeader.swift */; }; DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52B82E1EB5DC00432050 /* TabPosition.swift */; }; DD1D52BB2E1EB60B00432050 /* MoreMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */; }; DD1D52C02E4C100000000001 /* AppearanceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BF2E4C100000000001 /* AppearanceMode.swift */; }; @@ -465,9 +465,9 @@ /* Begin PBXFileReference section */ B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteBolusHistoryEntry.swift; sourceTree = ""; }; - B500000000000000000000A3 /* CommonBolusesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonBolusesManager.swift; sourceTree = ""; }; + B500000000000000000000A3 /* QuickPickBolusesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickBolusesManager.swift; sourceTree = ""; }; B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMealHistoryEntry.swift; sourceTree = ""; }; - B500000000000000000000B3 /* CommonMealsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonMealsManager.swift; sourceTree = ""; }; + B500000000000000000000B3 /* QuickPickMealsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickMealsManager.swift; sourceTree = ""; }; 059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = ""; }; 2B9BEC26E4E48EF9B811A372 /* PendingFutureCarb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingFutureCarb.swift; sourceTree = ""; }; 2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureCarbsCondition.swift; sourceTree = ""; }; @@ -554,7 +554,7 @@ DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorageValue.swift; sourceTree = ""; }; DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKQuantityInputView.swift; sourceTree = ""; }; DD16AF102C997B4600FB655A /* LoadingButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonView.swift; sourceTree = ""; }; - B500000000000000000000C1 /* CommonSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSectionHeader.swift; sourceTree = ""; }; + B500000000000000000000C1 /* QuickPickSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickSectionHeader.swift; sourceTree = ""; }; DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = ""; }; DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuViewController.swift; sourceTree = ""; }; DD1D52BF2E4C100000000001 /* AppearanceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceMode.swift; sourceTree = ""; }; @@ -1044,29 +1044,29 @@ path = Metric; sourceTree = ""; }; - B500000000000000000000B5 /* CommonMeals */ = { + B500000000000000000000B5 /* QuickPickMeals */ = { isa = PBXGroup; children = ( B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */, - B500000000000000000000B3 /* CommonMealsManager.swift */, + B500000000000000000000B3 /* QuickPickMealsManager.swift */, ); - path = CommonMeals; + path = QuickPickMeals; sourceTree = ""; }; - B500000000000000000000A5 /* CommonBoluses */ = { + B500000000000000000000A5 /* QuickPickBoluses */ = { isa = PBXGroup; children = ( B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */, - B500000000000000000000A3 /* CommonBolusesManager.swift */, + B500000000000000000000A3 /* QuickPickBolusesManager.swift */, ); - path = CommonBoluses; + path = QuickPickBoluses; sourceTree = ""; }; DD0C0C6E2C4AFFB800DBADDF /* Remote */ = { isa = PBXGroup; children = ( - B500000000000000000000A5 /* CommonBoluses */, - B500000000000000000000B5 /* CommonMeals */, + B500000000000000000000A5 /* QuickPickBoluses */, + B500000000000000000000B5 /* QuickPickMeals */, DDDF6F4A2D479B6A00884336 /* Nightscout */, DDDF6F482D479AEF00884336 /* NoRemoteView.swift */, DDEF503E2D479B8A00884336 /* LoopAPNS */, @@ -1422,7 +1422,7 @@ DDF6999D2C5AAA640058A8D9 /* ErrorMessageView.swift */, DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */, DD16AF102C997B4600FB655A /* LoadingButtonView.swift */, - B500000000000000000000C1 /* CommonSectionHeader.swift */, + B500000000000000000000C1 /* QuickPickSectionHeader.swift */, DDE75D262DE5E539007C1FC1 /* ActionRow.swift */, 654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */, DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */, @@ -2191,7 +2191,7 @@ DDC6CA4B2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift in Sources */, 6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */, DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */, - B500000000000000000000C2 /* CommonSectionHeader.swift in Sources */, + B500000000000000000000C2 /* QuickPickSectionHeader.swift in Sources */, DDFF3D7F2D1414A200BF9D9E /* BLEDevice.swift in Sources */, DD9ACA042D32821400415D8A /* DeviceStatusTask.swift in Sources */, FC16A97D24996747003D6245 /* SpeakBG.swift in Sources */, @@ -2219,9 +2219,9 @@ DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */, DDEF503A2D31615000999A5D /* LogManager.swift in Sources */, B500000000000000000000A2 /* RemoteBolusHistoryEntry.swift in Sources */, - B500000000000000000000A4 /* CommonBolusesManager.swift in Sources */, + B500000000000000000000A4 /* QuickPickBolusesManager.swift in Sources */, B500000000000000000000B2 /* RemoteMealHistoryEntry.swift in Sources */, - B500000000000000000000B4 /* CommonMealsManager.swift in Sources */, + B500000000000000000000B4 /* QuickPickMealsManager.swift in Sources */, DD4878172C7B75350048F05C /* BolusView.swift in Sources */, DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */, 374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */, diff --git a/LoopFollow/Helpers/Views/QuickPickSectionHeader.swift b/LoopFollow/Helpers/Views/QuickPickSectionHeader.swift new file mode 100644 index 000000000..e966de81d --- /dev/null +++ b/LoopFollow/Helpers/Views/QuickPickSectionHeader.swift @@ -0,0 +1,69 @@ +// LoopFollow +// QuickPickSectionHeader.swift + +import SwiftUI + +struct QuickPickSectionHeader: View { + let title: String + let infoText: String + @State private var showInfo = false + + var body: some View { + HStack(spacing: 4) { + Text(title) + Button { + showInfo = true + } label: { + Image(systemName: "info.circle") + .foregroundStyle(Color.accentColor) + } + .buttonStyle(.plain) + } + .sheet(isPresented: $showInfo) { + QuickPickInfoSheet(title: title, text: infoText) + } + } +} + +private struct QuickPickInfoSheet: View { + let title: String + let text: String + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + ScrollView { + Text(text) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { dismiss() } + } + } + } + .presentationDetents([.medium]) + .presentationDragIndicator(.visible) + } +} + +extension QuickPickSectionHeader { + static let bolusInfoText = """ + These buttons show your most-used recent bolus amounts. + + They're based on what you've sent before at similar times on similar days — so if you usually give 4 units before breakfast on weekdays, that button will show up on weekday mornings. + + Tap a button to fill in the amount. Nothing is sent until you review and confirm. + """ + + static let mealInfoText = """ + These buttons show your most-used recent meals. + + They're based on what you've sent before at similar times on similar days — so if you usually send the same breakfast on weekday mornings, it'll appear as an option. + + Tap a button to fill in the details. Nothing is sent until you review and confirm. + """ +} diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift index 00851c9bb..5c98b6a78 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift @@ -13,7 +13,7 @@ struct LoopAPNSBolusView: View { @State private var alertMessage = "" @State private var alertType: AlertType = .success - @ObservedObject private var commonBoluses = CommonBolusesManager.shared + @ObservedObject private var quickPickBoluses = QuickPickBolusesManager.shared @FocusState private var insulinFieldIsFocused: Bool // Add state for recommended bolus and warning @@ -73,11 +73,11 @@ struct LoopAPNSBolusView: View { } } - if !commonBoluses.commonBoluses.isEmpty { - Section(header: CommonSectionHeader(title: "Common Boluses", infoText: CommonSectionHeader.bolusInfoText)) { + if !quickPickBoluses.quickPickBoluses.isEmpty { + Section(header: QuickPickSectionHeader(title: "Quick-Pick Boluses", infoText: QuickPickSectionHeader.bolusInfoText)) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { - ForEach(commonBoluses.commonBoluses) { bolus in + ForEach(quickPickBoluses.quickPickBoluses) { bolus in Button { insulinAmount = HKQuantity(unit: .internationalUnit(), doubleValue: bolus.units) } label: { @@ -208,7 +208,7 @@ struct LoopAPNSBolusView: View { let step = Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit()) let maxBolus = Storage.shared.maxBolus.value.doubleValue(for: .internationalUnit()) - commonBoluses.refresh(stepIncrement: max(0.001, step), maxBolus: maxBolus) + quickPickBoluses.refresh(stepIncrement: max(0.001, step), maxBolus: maxBolus) loadRecommendedBolus() // Reset timer state so it shows '-' until first tick @@ -400,7 +400,7 @@ struct LoopAPNSBolusView: View { if success { let sentUnits = insulinAmount.doubleValue(for: .internationalUnit()) if sentUnits > 0 { - CommonBolusesManager.shared.recordBolus(units: sentUnits) + QuickPickBolusesManager.shared.recordBolus(units: sentUnits) } // Mark TOTP code as used TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value) diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index 65516ab3a..e114c639e 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -8,7 +8,7 @@ struct LoopAPNSCarbsView: View { private typealias AbsorptionPreset = (hours: Int, minutes: Int) @Environment(\.presentationMode) var presentationMode - @ObservedObject private var commonMeals = CommonMealsManager.shared + @ObservedObject private var quickPickMeals = QuickPickMealsManager.shared @State private var carbsAmount = HKQuantity(unit: .gram(), doubleValue: 0.0) @State private var absorptionHours = 3 @State private var absorptionMinutes = 0 @@ -103,11 +103,11 @@ struct LoopAPNSCarbsView: View { NavigationView { VStack { Form { - if !commonMeals.commonMeals.isEmpty { - Section(header: CommonSectionHeader(title: "Common Meals", infoText: CommonSectionHeader.mealInfoText)) { + if !quickPickMeals.quickPickMeals.isEmpty { + Section(header: QuickPickSectionHeader(title: "Quick-Pick Meals", infoText: QuickPickSectionHeader.mealInfoText)) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { - ForEach(commonMeals.commonMeals) { meal in + ForEach(quickPickMeals.quickPickMeals) { meal in Button { carbsAmount = HKQuantity(unit: .gram(), doubleValue: meal.carbs) } label: { @@ -405,7 +405,7 @@ struct LoopAPNSCarbsView: View { showAlert = true } - commonMeals.refresh( + quickPickMeals.refresh( maxCarbs: Storage.shared.maxCarbs.value.doubleValue(for: .gram()), includeFatProtein: false ) @@ -562,7 +562,7 @@ struct LoopAPNSCarbsView: View { if success { let sentCarbs = carbsAmount.doubleValue(for: .gram()) if sentCarbs > 0 { - CommonMealsManager.shared.recordMeal(carbs: sentCarbs) + QuickPickMealsManager.shared.recordMeal(carbs: sentCarbs) } // Mark TOTP code as used TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value) diff --git a/LoopFollow/Remote/CommonBoluses/CommonBolusesManager.swift b/LoopFollow/Remote/QuickPickBoluses/QuickPickBolusesManager.swift similarity index 88% rename from LoopFollow/Remote/CommonBoluses/CommonBolusesManager.swift rename to LoopFollow/Remote/QuickPickBoluses/QuickPickBolusesManager.swift index 16f30b5da..84c2c6ab3 100644 --- a/LoopFollow/Remote/CommonBoluses/CommonBolusesManager.swift +++ b/LoopFollow/Remote/QuickPickBoluses/QuickPickBolusesManager.swift @@ -1,21 +1,21 @@ // LoopFollow -// CommonBolusesManager.swift +// QuickPickBolusesManager.swift import Foundation -struct CommonBolus: Identifiable, Equatable { +struct QuickPickBolus: Identifiable, Equatable { let id = UUID() let units: Double - static func == (lhs: CommonBolus, rhs: CommonBolus) -> Bool { + static func == (lhs: QuickPickBolus, rhs: QuickPickBolus) -> Bool { lhs.id == rhs.id } } -final class CommonBolusesManager: ObservableObject { - static let shared = CommonBolusesManager() +final class QuickPickBolusesManager: ObservableObject { + static let shared = QuickPickBolusesManager() - @Published private(set) var commonBoluses: [CommonBolus] = [] + @Published private(set) var quickPickBoluses: [QuickPickBolus] = [] private static let maxEntries = 500 private static let maxAgeDays = 90.0 @@ -38,7 +38,7 @@ final class CommonBolusesManager: ObservableObject { func refresh(now: Date = Date(), stepIncrement: Double, maxBolus: Double) { let history = Storage.shared.remoteBolusHistory.value - commonBoluses = Self.computeCommonBoluses( + quickPickBoluses = Self.computeQuickPickBoluses( from: history, now: now, stepIncrement: stepIncrement, @@ -48,12 +48,12 @@ final class CommonBolusesManager: ObservableObject { // MARK: - Scoring (static for testability) - static func computeCommonBoluses( + static func computeQuickPickBoluses( from history: [RemoteBolusHistoryEntry], now: Date, stepIncrement: Double, maxBolus: Double - ) -> [CommonBolus] { + ) -> [QuickPickBolus] { guard stepIncrement > 0 else { return [] } let nowMinute = { @@ -81,7 +81,7 @@ final class CommonBolusesManager: ObservableObject { .filter { $0.value >= minScore } .sorted { $0.value > $1.value } .prefix(maxResults) - .map { CommonBolus(units: $0.key) } + .map { QuickPickBolus(units: $0.key) } } static func timeOfDayScore(entryMinute: Int, nowMinute: Int) -> Double { diff --git a/LoopFollow/Remote/CommonBoluses/RemoteBolusHistoryEntry.swift b/LoopFollow/Remote/QuickPickBoluses/RemoteBolusHistoryEntry.swift similarity index 100% rename from LoopFollow/Remote/CommonBoluses/RemoteBolusHistoryEntry.swift rename to LoopFollow/Remote/QuickPickBoluses/RemoteBolusHistoryEntry.swift diff --git a/LoopFollow/Remote/CommonMeals/CommonMealsManager.swift b/LoopFollow/Remote/QuickPickMeals/QuickPickMealsManager.swift similarity index 90% rename from LoopFollow/Remote/CommonMeals/CommonMealsManager.swift rename to LoopFollow/Remote/QuickPickMeals/QuickPickMealsManager.swift index 223e277f7..cd3f4de25 100644 --- a/LoopFollow/Remote/CommonMeals/CommonMealsManager.swift +++ b/LoopFollow/Remote/QuickPickMeals/QuickPickMealsManager.swift @@ -1,24 +1,24 @@ // LoopFollow -// CommonMealsManager.swift +// QuickPickMealsManager.swift import Foundation -struct CommonMeal: Identifiable, Equatable { +struct QuickPickMeal: Identifiable, Equatable { let id = UUID() let carbs: Double let fat: Double let protein: Double let bolus: Double - static func == (lhs: CommonMeal, rhs: CommonMeal) -> Bool { + static func == (lhs: QuickPickMeal, rhs: QuickPickMeal) -> Bool { lhs.id == rhs.id } } -final class CommonMealsManager: ObservableObject { - static let shared = CommonMealsManager() +final class QuickPickMealsManager: ObservableObject { + static let shared = QuickPickMealsManager() - @Published private(set) var commonMeals: [CommonMeal] = [] + @Published private(set) var quickPickMeals: [QuickPickMeal] = [] private static let maxEntries = 500 private static let maxAgeDays = 90.0 @@ -41,7 +41,7 @@ final class CommonMealsManager: ObservableObject { func refresh(now: Date = Date(), carbStep: Double = 1.0, maxCarbs: Double, includeFatProtein: Bool) { let history = Storage.shared.remoteMealHistory.value - commonMeals = Self.computeCommonMeals( + quickPickMeals = Self.computeQuickPickMeals( from: history, now: now, carbStep: carbStep, @@ -52,13 +52,13 @@ final class CommonMealsManager: ObservableObject { // MARK: - Scoring (static for testability) - static func computeCommonMeals( + static func computeQuickPickMeals( from history: [RemoteMealHistoryEntry], now: Date, carbStep: Double, maxCarbs: Double, includeFatProtein: Bool - ) -> [CommonMeal] { + ) -> [QuickPickMeal] { guard carbStep > 0 else { return [] } let nowMinute = { @@ -98,7 +98,7 @@ final class CommonMealsManager: ObservableObject { .prefix(maxResults) .map { item in let best = groupBestEntry[item.key]?.entry - return CommonMeal( + return QuickPickMeal( carbs: item.key, fat: includeFatProtein ? (best?.fat ?? 0) : 0, protein: includeFatProtein ? (best?.protein ?? 0) : 0, diff --git a/LoopFollow/Remote/CommonMeals/RemoteMealHistoryEntry.swift b/LoopFollow/Remote/QuickPickMeals/RemoteMealHistoryEntry.swift similarity index 100% rename from LoopFollow/Remote/CommonMeals/RemoteMealHistoryEntry.swift rename to LoopFollow/Remote/QuickPickMeals/RemoteMealHistoryEntry.swift diff --git a/LoopFollow/Remote/TRC/BolusView.swift b/LoopFollow/Remote/TRC/BolusView.swift index 83c5e9341..7255931d9 100644 --- a/LoopFollow/Remote/TRC/BolusView.swift +++ b/LoopFollow/Remote/TRC/BolusView.swift @@ -14,7 +14,7 @@ struct BolusView: View { @ObservedObject private var deviceRecBolus = Observable.shared.deviceRecBolus @ObservedObject private var enactedOrSuggested = Observable.shared.enactedOrSuggested - @ObservedObject private var commonBoluses = CommonBolusesManager.shared + @ObservedObject private var quickPickBoluses = QuickPickBolusesManager.shared @FocusState private var bolusFieldIsFocused: Bool @State private var showAlert = false @@ -68,13 +68,13 @@ struct BolusView: View { Form { recommendedBlocks(now: context.date) - if !commonBoluses.commonBoluses.isEmpty { - Section(header: CommonSectionHeader(title: "Common Boluses", infoText: CommonSectionHeader.bolusInfoText)) { + if !quickPickBoluses.quickPickBoluses.isEmpty { + Section(header: QuickPickSectionHeader(title: "Quick-Pick Boluses", infoText: QuickPickSectionHeader.bolusInfoText)) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { - ForEach(commonBoluses.commonBoluses) { bolus in + ForEach(quickPickBoluses.quickPickBoluses) { bolus in Button { - applyCommonBolus(bolus.units) + applyQuickPickBolus(bolus.units) } label: { Text("\(InsulinFormatter.shared.string(bolus.units))U") .font(.subheadline.weight(.medium)) @@ -131,7 +131,7 @@ struct BolusView: View { .navigationBarTitleDisplayMode(.inline) } .onAppear { - commonBoluses.refresh( + quickPickBoluses.refresh( stepIncrement: stepU, maxBolus: maxBolus.value.doubleValue(for: .internationalUnit()) ) @@ -276,7 +276,7 @@ struct BolusView: View { } } - private func applyCommonBolus(_ units: Double) { + private func applyQuickPickBolus(_ units: Double) { let maxU = maxBolus.value.doubleValue(for: .internationalUnit()) let clamped = min(units, maxU) let stepped = roundedToStep(clamped) @@ -307,7 +307,7 @@ struct BolusView: View { if success { let sentUnits = bolusAmount.doubleValue(for: .internationalUnit()) if sentUnits > 0 { - CommonBolusesManager.shared.recordBolus(units: sentUnits) + QuickPickBolusesManager.shared.recordBolus(units: sentUnits) } statusMessage = "Bolus command sent successfully." LogManager.shared.log( diff --git a/LoopFollow/Remote/TRC/MealView.swift b/LoopFollow/Remote/TRC/MealView.swift index 0fdf02fac..9f0e7e9e3 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -20,7 +20,7 @@ struct MealView: View { @ObservedObject private var mealWithBolus = Storage.shared.mealWithBolus @ObservedObject private var mealWithFatProtein = Storage.shared.mealWithFatProtein @ObservedObject private var maxBolus = Storage.shared.maxBolus - @ObservedObject private var commonMeals = CommonMealsManager.shared + @ObservedObject private var quickPickMeals = QuickPickMealsManager.shared @FocusState private var carbsFieldIsFocused: Bool @FocusState private var proteinFieldIsFocused: Bool @@ -47,13 +47,13 @@ struct MealView: View { NavigationView { VStack { Form { - if !commonMeals.commonMeals.isEmpty { - Section(header: CommonSectionHeader(title: "Common Meals", infoText: CommonSectionHeader.mealInfoText)) { + if !quickPickMeals.quickPickMeals.isEmpty { + Section(header: QuickPickSectionHeader(title: "Quick-Pick Meals", infoText: QuickPickSectionHeader.mealInfoText)) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { - ForEach(commonMeals.commonMeals) { meal in + ForEach(quickPickMeals.quickPickMeals) { meal in Button { - applyCommonMeal(meal) + applyQuickPickMeal(meal) } label: { VStack(spacing: 2) { Text("\(Int(meal.carbs))g") @@ -208,7 +208,7 @@ struct MealView: View { selectedTime = nil isScheduling = false - commonMeals.refresh( + quickPickMeals.refresh( maxCarbs: maxCarbs.value.doubleValue(for: .gram()), includeFatProtein: mealWithFatProtein.value ) @@ -342,7 +342,7 @@ struct MealView: View { if success { let sentCarbs = carbs.doubleValue(for: .gram()) if sentCarbs > 0 { - CommonMealsManager.shared.recordMeal( + QuickPickMealsManager.shared.recordMeal( carbs: sentCarbs, fat: fat.doubleValue(for: .gram()), protein: protein.doubleValue(for: .gram()), @@ -381,7 +381,7 @@ struct MealView: View { return formatter.string(from: date) } - private func applyCommonMeal(_ meal: CommonMeal) { + private func applyQuickPickMeal(_ meal: QuickPickMeal) { let maxC = maxCarbs.value.doubleValue(for: .gram()) carbs = HKQuantity(unit: .gram(), doubleValue: min(meal.carbs, maxC)) if mealWithFatProtein.value {