From c1e00ea86b5c56674047c2a9cf0efcc33f432a93 Mon Sep 17 00:00:00 2001 From: codebymini Date: Sat, 21 Mar 2026 12:24:11 +0100 Subject: [PATCH 1/4] Refactor absorption time handling in LoopAPNSCarbsView to use separate hour and minute states, enhancing clarity and usability --- .../Remote/LoopAPNS/LoopAPNSCarbsView.swift | 305 +++++++++++++----- 1 file changed, 220 insertions(+), 85 deletions(-) diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index 28b5745d8..bc7dfc7d8 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -7,10 +7,12 @@ import SwiftUI struct LoopAPNSCarbsView: View { @Environment(\.presentationMode) var presentationMode @State private var carbsAmount = HKQuantity(unit: .gram(), doubleValue: 0.0) - @State private var absorptionTimeString = "3.0" + @State private var absorptionHours = 3 + @State private var absorptionMinutes = 0 @State private var foodType = "" @State private var consumedDate = Date() @State private var showDatePickerSheet = false + @State private var showAbsorptionPickerSheet = false @State private var isLoading = false @State private var showAlert = false @State private var alertMessage = "" @@ -18,16 +20,52 @@ struct LoopAPNSCarbsView: View { @State private var otpTimeRemaining: Int? = nil @State private var showTOTPWarning = false private let otpPeriod: TimeInterval = 30 + private let timeAdjustmentStepMinutes = 5 private var otpTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @FocusState private var carbsFieldIsFocused: Bool - @FocusState private var absorptionFieldIsFocused: Bool // Computed property to check if TOTP should be blocked private var isTOTPBlocked: Bool { TOTPService.shared.isTOTPBlocked(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value) } + private var absorptionTimeValue: Double { + Double(absorptionHours) + (Double(absorptionMinutes) / 60.0) + } + + private var absorptionTimeText: String { + if absorptionMinutes == 0 { + return "\(absorptionHours) hr" + } + + return "\(absorptionHours) hr \(absorptionMinutes) min" + } + + private var absorptionConfirmationText: String { + String(format: "%.1f", absorptionTimeValue) + } + + private var absorptionMinuteOptions: [Int] { + if absorptionHours == 0 { + return [30] + } + + if absorptionHours == 8 { + return [0] + } + + return [0, 30] + } + + private var oldestAcceptedDate: Date { + Date().addingTimeInterval(-60 * 60 * 12) + } + + private var latestAcceptedDate: Date { + Date().addingTimeInterval(60 * 60) + } + enum AlertType { case success case error @@ -54,112 +92,118 @@ struct LoopAPNSCarbsView: View { } ) - VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + Text("Time") + + Spacer() + + Button(action: { + adjustConsumedDate(byMinutes: -timeAdjustmentStepMinutes) + }) { + Image(systemName: "minus") + .font(.subheadline.weight(.semibold)) + .foregroundColor(Color(.systemGray6)) + .frame(width: 28, height: 28) + .background(Color.blue) + .clipShape(Circle()) + } + .buttonStyle(.plain) + + Button(action: { + showDatePickerSheet = true + }) { + Text(consumedDate, format: Date.FormatStyle().hour().minute()) + .font(.body.monospacedDigit()) + .foregroundColor(.primary) + .frame(minWidth: 58) + } + .buttonStyle(.plain) + + Button(action: { + adjustConsumedDate(byMinutes: timeAdjustmentStepMinutes) + }) { + Image(systemName: "plus") + .font(.subheadline.weight(.semibold)) + .foregroundColor(Color(.systemGray6)) + .frame(width: 28, height: 28) + .background(Color.blue) + .clipShape(Circle()) + } + .buttonStyle(.plain) + } + + HStack(alignment: .center, spacing: 12) { Text("Food Type") - .font(.headline) - HStack(spacing: 12) { - // Fast carb entry emoji (0.5 hours) + Spacer() + + HStack(spacing: 10) { Button(action: { foodType = "🍭" - absorptionTimeString = "0.5" + setAbsorptionTime(hours: 0, minutes: 30) }) { Text("🍭") - .font(.title) - .frame(width: 44, height: 44) - .background(Color.blue.opacity(0.1)) - .cornerRadius(8) + .font(.title3) + .frame(width: 42, height: 42) + .background(foodType == "🍭" ? Color.white.opacity(0.12) : Color.clear) + .cornerRadius(12) } - .buttonStyle(PlainButtonStyle()) + .buttonStyle(.plain) - // Medium carb entry emoji (3 hours) Button(action: { foodType = "🌮" - absorptionTimeString = "3.0" + setAbsorptionTime(hours: 3, minutes: 0) }) { Text("🌮") - .font(.title) - .frame(width: 44, height: 44) - .background(Color.blue.opacity(0.1)) - .cornerRadius(8) + .font(.title3) + .frame(width: 42, height: 42) + .background(foodType == "🌮" ? Color.white.opacity(0.12) : Color.clear) + .cornerRadius(12) } - .buttonStyle(PlainButtonStyle()) + .buttonStyle(.plain) - // Slow carb entry emoji (5 hours) Button(action: { foodType = "🍕" - absorptionTimeString = "5.0" + setAbsorptionTime(hours: 5, minutes: 0) }) { Text("🍕") - .font(.title) - .frame(width: 44, height: 44) - .background(Color.blue.opacity(0.1)) - .cornerRadius(8) + .font(.title3) + .frame(width: 42, height: 42) + .background(foodType == "🍕" ? Color.white.opacity(0.12) : Color.clear) + .cornerRadius(12) } - .buttonStyle(PlainButtonStyle()) + .buttonStyle(.plain) - // Custom carb entry emoji (clears and focuses absorption) Button(action: { foodType = "🍽️" - absorptionTimeString = "" - absorptionFieldIsFocused = true + showAbsorptionPickerSheet = true }) { Text("🍽️") - .font(.title) - .frame(width: 44, height: 44) - .background(Color.blue.opacity(0.1)) - .cornerRadius(8) + .font(.title3) + .frame(width: 42, height: 42) + .background(foodType == "🍽️" ? Color.white.opacity(0.12) : Color.clear) + .cornerRadius(12) } - .buttonStyle(PlainButtonStyle()) - - Spacer() + .buttonStyle(.plain) } } - HStack { + HStack(spacing: 8) { Text("Absorption Time") - Spacer() - TextField("0.0", text: $absorptionTimeString) - .keyboardType(.decimalPad) - .multilineTextAlignment(.trailing) - .focused($absorptionFieldIsFocused) - .onChange(of: absorptionTimeString) { newValue in - // Only allow numbers and decimal point - let filtered = newValue.filter { "0123456789.".contains($0) } - // Ensure only one decimal point - let components = filtered.components(separatedBy: ".") - if components.count > 2 { - absorptionTimeString = String(filtered.dropLast()) - } else { - absorptionTimeString = filtered - } - } - Text("hr") - .foregroundColor(.secondary) - } - // Time input section - VStack(alignment: .leading) { - Text("Time") - .font(.headline) + Image(systemName: "info.circle") + .foregroundColor(.blue) + .font(.subheadline) + + Spacer() Button(action: { - showDatePickerSheet = true + showAbsorptionPickerSheet = true }) { - HStack { - Text(consumedDate, format: Date.FormatStyle().hour().minute()) - .font(.body) - .foregroundColor(.primary) - Spacer() - Image(systemName: "chevron.right") - .font(.caption) - .foregroundColor(.secondary) - } - .padding(.vertical, 8) - .padding(.horizontal, 12) - .background(Color(.systemGray6)) - .cornerRadius(8) + Text(absorptionTimeText) + .foregroundColor(.secondary) } + .buttonStyle(.plain) } } Section { @@ -226,16 +270,74 @@ struct LoopAPNSCarbsView: View { .navigationBarTitleDisplayMode(.inline) } .sheet(isPresented: $showDatePickerSheet) { - VStack { + VStack(spacing: 16) { Text("Consumption Time") .font(.headline) - .padding() - Form { - DatePicker("Time", selection: $consumedDate, displayedComponents: [.hourAndMinute, .date]) - .datePickerStyle(.automatic) + .padding(.top) + + DatePicker( + "Time", + selection: Binding( + get: { consumedDate }, + set: { consumedDate = clampedConsumedDate($0) } + ), + in: oldestAcceptedDate ... latestAcceptedDate, + displayedComponents: [.hourAndMinute, .date] + ) + .datePickerStyle(.wheel) + .labelsHidden() + + Button("Done") { + consumedDate = clampedConsumedDate(consumedDate) + showDatePickerSheet = false + } + .buttonStyle(.borderedProminent) + .padding(.horizontal) + .padding(.bottom) + } + .presentationDetents([.fraction(0.4)]) + .presentationDragIndicator(.visible) + } + .sheet(isPresented: $showAbsorptionPickerSheet) { + VStack(spacing: 16) { + Text("Absorption Time") + .font(.headline) + .padding(.top) + + HStack(spacing: 0) { + Picker("Hours", selection: $absorptionHours) { + ForEach(0 ... 8, id: \.self) { hour in + Text("\(hour) hr") + .tag(hour) + } + } + + Picker("Minutes", selection: $absorptionMinutes) { + ForEach(absorptionMinuteOptions, id: \.self) { minute in + Text("\(minute) min") + .tag(minute) + } + } + } + .pickerStyle(.wheel) + .frame(height: 180) + + Button("Done") { + normalizeAbsorptionTime() + showAbsorptionPickerSheet = false } + .buttonStyle(.borderedProminent) + .padding(.horizontal) + .padding(.bottom) + } + .presentationDetents([.fraction(0.35)]) + .presentationDragIndicator(.visible) + .onAppear { + normalizeAbsorptionTime() + } + .onChange(of: absorptionHours) { _ in + normalizeAbsorptionTime() } - .presentationDetents([.fraction(1 / 4)]) } .onAppear { // Validate APNS setup @@ -303,7 +405,7 @@ struct LoopAPNSCarbsView: View { timeFormatter.dateStyle = .short return Alert( title: Text("Confirm Carbs"), - message: Text("Send \(Int(carbsAmount.doubleValue(for: .gram())))g of carbs with \(absorptionTimeString)h absorption time at \(timeFormatter.string(from: consumedDate))?"), + message: Text("Send \(Int(carbsAmount.doubleValue(for: .gram())))g of carbs with \(absorptionConfirmationText)h absorption time at \(timeFormatter.string(from: consumedDate))?"), primaryButton: .default(Text("Send")) { sendCarbsConfirmed() }, @@ -370,8 +472,8 @@ struct LoopAPNSCarbsView: View { return } - // Parse absorption time string to double - guard let absorptionTimeValue = Double(absorptionTimeString), absorptionTimeValue >= 0.5, absorptionTimeValue <= 8.0 else { + let selectedAbsorptionTime = absorptionTimeValue + guard selectedAbsorptionTime >= 0.5, selectedAbsorptionTime <= 8.0 else { alertMessage = "Please enter a valid absorption time between 0.5 and 8.0 hours" alertType = .error isLoading = false @@ -386,7 +488,7 @@ struct LoopAPNSCarbsView: View { let payload = LoopAPNSPayload( type: .carbs, carbsAmount: carbsAmount.doubleValue(for: .gram()), - absorptionTime: absorptionTimeValue, + absorptionTime: selectedAbsorptionTime, foodType: foodType.isEmpty ? nil : foodType, consumedDate: adjustedConsumedDate, otp: otpCode @@ -405,7 +507,7 @@ struct LoopAPNSCarbsView: View { self.alertType = .success LogManager.shared.log( category: .apns, - message: "Carbs sent - Amount: \(carbsAmount.doubleValue(for: .gram()))g, Absorption: \(absorptionTimeString)h, Time: \(adjustedConsumedDate)" + message: "Carbs sent - Amount: \(carbsAmount.doubleValue(for: .gram()))g, Absorption: \(absorptionConfirmationText)h, Time: \(adjustedConsumedDate)" ) } else { self.alertMessage = errorMessage ?? "Failed to send carbs. Check your Loop APNS configuration." @@ -419,6 +521,39 @@ struct LoopAPNSCarbsView: View { } } } + + private func setAbsorptionTime(hours: Int, minutes: Int) { + absorptionHours = min(max(hours, 0), 8) + absorptionMinutes = minutes + normalizeAbsorptionTime() + } + + private func normalizeAbsorptionTime() { + if absorptionHours <= 0 && absorptionMinutes <= 0 { + absorptionHours = 0 + absorptionMinutes = 30 + return + } + + if absorptionHours >= 8 { + absorptionHours = 8 + absorptionMinutes = 0 + return + } + + if !absorptionMinuteOptions.contains(absorptionMinutes) { + absorptionMinutes = absorptionMinuteOptions.first ?? 0 + } + } + + private func adjustConsumedDate(byMinutes minutes: Int) { + let adjustedDate = Calendar.current.date(byAdding: .minute, value: minutes, to: consumedDate) ?? consumedDate + consumedDate = clampedConsumedDate(adjustedDate) + } + + private func clampedConsumedDate(_ date: Date) -> Date { + min(max(date, oldestAcceptedDate), latestAcceptedDate) + } } // APNS Payload structure for carbs From 2686bc185884312e3ec19b865480ef7124e6a8de Mon Sep 17 00:00:00 2001 From: codebymini Date: Mon, 30 Mar 2026 10:15:05 +0200 Subject: [PATCH 2/4] Redesign date and absorption time pickers to use NavigationStack for improved layout and navigation experience --- .../Remote/LoopAPNS/LoopAPNSCarbsView.swift | 102 +++++++++--------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index bc7dfc7d8..d50277290 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -270,67 +270,73 @@ struct LoopAPNSCarbsView: View { .navigationBarTitleDisplayMode(.inline) } .sheet(isPresented: $showDatePickerSheet) { - VStack(spacing: 16) { - Text("Consumption Time") - .font(.headline) - .padding(.top) - - DatePicker( - "Time", - selection: Binding( - get: { consumedDate }, - set: { consumedDate = clampedConsumedDate($0) } - ), - in: oldestAcceptedDate ... latestAcceptedDate, - displayedComponents: [.hourAndMinute, .date] - ) - .datePickerStyle(.wheel) - .labelsHidden() + NavigationStack { + VStack { + DatePicker( + "Time", + selection: Binding( + get: { consumedDate }, + set: { consumedDate = clampedConsumedDate($0) } + ), + in: oldestAcceptedDate ... latestAcceptedDate, + displayedComponents: [.hourAndMinute, .date] + ) + .datePickerStyle(.wheel) + .labelsHidden() + .padding() - Button("Done") { - consumedDate = clampedConsumedDate(consumedDate) - showDatePickerSheet = false + Spacer() + } + .navigationTitle("Consumption Time") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + consumedDate = clampedConsumedDate(consumedDate) + showDatePickerSheet = false + } + } } - .buttonStyle(.borderedProminent) - .padding(.horizontal) - .padding(.bottom) } - .presentationDetents([.fraction(0.4)]) + .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) } .sheet(isPresented: $showAbsorptionPickerSheet) { - VStack(spacing: 16) { - Text("Absorption Time") - .font(.headline) - .padding(.top) - - HStack(spacing: 0) { - Picker("Hours", selection: $absorptionHours) { - ForEach(0 ... 8, id: \.self) { hour in - Text("\(hour) hr") - .tag(hour) + NavigationStack { + VStack { + HStack(spacing: 0) { + Picker("Hours", selection: $absorptionHours) { + ForEach(0 ... 8, id: \.self) { hour in + Text("\(hour) hr") + .tag(hour) + } } - } - Picker("Minutes", selection: $absorptionMinutes) { - ForEach(absorptionMinuteOptions, id: \.self) { minute in - Text("\(minute) min") - .tag(minute) + Picker("Minutes", selection: $absorptionMinutes) { + ForEach(absorptionMinuteOptions, id: \.self) { minute in + Text("\(minute) min") + .tag(minute) + } } } - } - .pickerStyle(.wheel) - .frame(height: 180) + .pickerStyle(.wheel) + .frame(height: 180) + .padding(.horizontal) - Button("Done") { - normalizeAbsorptionTime() - showAbsorptionPickerSheet = false + Spacer() + } + .navigationTitle("Absorption Time") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + normalizeAbsorptionTime() + showAbsorptionPickerSheet = false + } + } } - .buttonStyle(.borderedProminent) - .padding(.horizontal) - .padding(.bottom) } - .presentationDetents([.fraction(0.35)]) + .presentationDetents([.medium]) .presentationDragIndicator(.visible) .onAppear { normalizeAbsorptionTime() From aeccbe3a24fd0f1e7ef610d1bbeddcee3467225c Mon Sep 17 00:00:00 2001 From: codebymini Date: Mon, 30 Mar 2026 10:16:57 +0200 Subject: [PATCH 3/4] Refactor time constraint handling in LoopAPNSCarbsView to use configurable maxPastHours and maxFutureHours constants --- LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index d50277290..6296e65f0 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -21,6 +21,8 @@ struct LoopAPNSCarbsView: View { @State private var showTOTPWarning = false private let otpPeriod: TimeInterval = 30 private let timeAdjustmentStepMinutes = 5 + private let maxPastHours = 12 + private let maxFutureHours = 1 private var otpTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @FocusState private var carbsFieldIsFocused: Bool @@ -59,11 +61,11 @@ struct LoopAPNSCarbsView: View { } private var oldestAcceptedDate: Date { - Date().addingTimeInterval(-60 * 60 * 12) + Date().addingTimeInterval(-TimeInterval(maxPastHours) * 60 * 60) } private var latestAcceptedDate: Date { - Date().addingTimeInterval(60 * 60) + Date().addingTimeInterval(TimeInterval(maxFutureHours) * 60 * 60) } enum AlertType { @@ -443,8 +445,6 @@ struct LoopAPNSCarbsView: View { // Validate time constraints (similar to LoopCaregiver) let now = Date() - let maxPastHours = 12 - let maxFutureHours = 1 let oldestAcceptedDate = now.addingTimeInterval(-60 * 60 * Double(maxPastHours)) let latestAcceptedDate = now.addingTimeInterval(60 * 60 * Double(maxFutureHours)) From 527a749b953d99cae398cf38cf66b2d14614ce91 Mon Sep 17 00:00:00 2001 From: codebymini Date: Mon, 30 Mar 2026 10:22:44 +0200 Subject: [PATCH 4/4] Enhance absorption time handling in LoopAPNSCarbsView with configurable limits and presets --- .../Remote/LoopAPNS/LoopAPNSCarbsView.swift | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index 6296e65f0..45e2c403d 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -5,6 +5,8 @@ import HealthKit import SwiftUI struct LoopAPNSCarbsView: View { + private typealias AbsorptionPreset = (hours: Int, minutes: Int) + @Environment(\.presentationMode) var presentationMode @State private var carbsAmount = HKQuantity(unit: .gram(), doubleValue: 0.0) @State private var absorptionHours = 3 @@ -23,6 +25,11 @@ struct LoopAPNSCarbsView: View { private let timeAdjustmentStepMinutes = 5 private let maxPastHours = 12 private let maxFutureHours = 1 + private let minAllowedAbsorptionTime = 0.5 + private let maxAllowedAbsorptionTime = 8.0 + private let lollipopStandardAbsorption: AbsorptionPreset = (0, 30) + private let tacoStandardAbsorption: AbsorptionPreset = (3, 0) + private let pizzaStandardAbsorption: AbsorptionPreset = (5, 0) private var otpTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @FocusState private var carbsFieldIsFocused: Bool @@ -48,12 +55,29 @@ struct LoopAPNSCarbsView: View { String(format: "%.1f", absorptionTimeValue) } + private var minimumAbsorptionPreset: AbsorptionPreset { + let minimumAbsorptionMinutes = Int(minAllowedAbsorptionTime * 60) + return (minimumAbsorptionMinutes / 60, minimumAbsorptionMinutes % 60) + } + + private var maximumAbsorptionHours: Int { + Int(maxAllowedAbsorptionTime) + } + + private var absorptionValidationMessage: String { + String( + format: "Please enter a valid absorption time between %.1f and %.1f hours", + minAllowedAbsorptionTime, + maxAllowedAbsorptionTime + ) + } + private var absorptionMinuteOptions: [Int] { - if absorptionHours == 0 { - return [30] + if absorptionHours == minimumAbsorptionPreset.hours { + return [minimumAbsorptionPreset.minutes] } - if absorptionHours == 8 { + if absorptionHours == maximumAbsorptionHours { return [0] } @@ -142,7 +166,7 @@ struct LoopAPNSCarbsView: View { HStack(spacing: 10) { Button(action: { foodType = "🍭" - setAbsorptionTime(hours: 0, minutes: 30) + setAbsorptionTime(hours: lollipopStandardAbsorption.hours, minutes: lollipopStandardAbsorption.minutes) }) { Text("🍭") .font(.title3) @@ -154,7 +178,7 @@ struct LoopAPNSCarbsView: View { Button(action: { foodType = "🌮" - setAbsorptionTime(hours: 3, minutes: 0) + setAbsorptionTime(hours: tacoStandardAbsorption.hours, minutes: tacoStandardAbsorption.minutes) }) { Text("🌮") .font(.title3) @@ -166,7 +190,7 @@ struct LoopAPNSCarbsView: View { Button(action: { foodType = "🍕" - setAbsorptionTime(hours: 5, minutes: 0) + setAbsorptionTime(hours: pizzaStandardAbsorption.hours, minutes: pizzaStandardAbsorption.minutes) }) { Text("🍕") .font(.title3) @@ -308,7 +332,7 @@ struct LoopAPNSCarbsView: View { VStack { HStack(spacing: 0) { Picker("Hours", selection: $absorptionHours) { - ForEach(0 ... 8, id: \.self) { hour in + ForEach(0 ... maximumAbsorptionHours, id: \.self) { hour in Text("\(hour) hr") .tag(hour) } @@ -479,8 +503,8 @@ struct LoopAPNSCarbsView: View { } let selectedAbsorptionTime = absorptionTimeValue - guard selectedAbsorptionTime >= 0.5, selectedAbsorptionTime <= 8.0 else { - alertMessage = "Please enter a valid absorption time between 0.5 and 8.0 hours" + guard selectedAbsorptionTime >= minAllowedAbsorptionTime, selectedAbsorptionTime <= maxAllowedAbsorptionTime else { + alertMessage = absorptionValidationMessage alertType = .error isLoading = false showAlert = true @@ -529,20 +553,20 @@ struct LoopAPNSCarbsView: View { } private func setAbsorptionTime(hours: Int, minutes: Int) { - absorptionHours = min(max(hours, 0), 8) + absorptionHours = min(max(hours, 0), maximumAbsorptionHours) absorptionMinutes = minutes normalizeAbsorptionTime() } private func normalizeAbsorptionTime() { - if absorptionHours <= 0 && absorptionMinutes <= 0 { - absorptionHours = 0 - absorptionMinutes = 30 + if absorptionTimeValue < minAllowedAbsorptionTime { + absorptionHours = minimumAbsorptionPreset.hours + absorptionMinutes = minimumAbsorptionPreset.minutes return } - if absorptionHours >= 8 { - absorptionHours = 8 + if absorptionTimeValue >= maxAllowedAbsorptionTime { + absorptionHours = maximumAbsorptionHours absorptionMinutes = 0 return }