| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975 |
- import SwiftUI
- // MARK: - Style Extensions
- // View modifiers that provide consistent styling across calculation components.
- // These extensions establish a visual hierarchy through font sizing, coloring, and layout priorities.
- private extension View {
- /// Applies secondary label styling for descriptive text elements.
- /// Uses a smaller font with secondary color to visually distinguish labels from values.
- /// Layout priority ensures these elements maintain appropriate space.
- func secondaryStyle() -> some View {
- font(.footnote)
- .foregroundStyle(.secondary)
- .allowsTightening(true)
- .minimumScaleFactor(0.5)
- .layoutPriority(1)
- }
- /// Applies unit label styling for measurement units (mg/dL, mmol/L, U, g, etc.)
- /// Uses the smallest font size with secondary color to de-emphasize units.
- /// Low layout priority ensures units don't compete for space with values.
- func unitStyle() -> some View {
- font(.caption2)
- .foregroundStyle(.secondary)
- .allowsTightening(true)
- .minimumScaleFactor(0.5)
- .layoutPriority(-1)
- }
- /// Applies mathematical operator label styling (+, -, ×, ÷, =, etc.)
- /// Medium priority ensures operators maintain proper spacing between values
- /// while allowing compression when space is limited.
- func operatorStyle() -> some View {
- font(.body)
- .foregroundStyle(.secondary)
- .allowsTightening(true)
- .minimumScaleFactor(0.5)
- .layoutPriority(3)
- }
- /// Applies styling for numeric values in calculations.
- /// Higher layout priority (5) ensures values maintain visibility when space is constrained.
- /// Minimum width prevents values from becoming too compressed.
- func valueStyle() -> some View {
- font(.headline)
- .frame(minWidth: 50)
- .allowsTightening(true)
- .minimumScaleFactor(0.5)
- .lineLimit(1)
- .layoutPriority(5)
- }
- /// Applies styling for calculation results with dynamic coloring based on value.
- /// - Parameter value: The numeric value to display, which determines color:
- /// - Negative values: Red (indicating insulin reduction)
- /// - Zero: Primary color
- /// - Positive values: Green (indicating insulin addition)
- /// Highest layout priority (10) ensures results remain visible even in constrained layouts.
- func solutionStyle(_ value: Decimal = 0) -> some View {
- let solutionColor: Color
- switch value {
- case ..<0:
- solutionColor = .red
- case 0:
- solutionColor = .primary
- default:
- solutionColor = .green
- }
- return font(.system(.headline, weight: .bold))
- .frame(minWidth: 45, alignment: .center)
- .foregroundStyle(solutionColor)
- .allowsTightening(true)
- .fixedSize(horizontal: true, vertical: true)
- .minimumScaleFactor(0.5)
- .layoutPriority(10)
- .lineLimit(1)
- }
- /// Applies styling for the final recommendation value.
- /// Uses larger font size than regular solutions to emphasize the final result.
- /// Maintains highest layout priority to ensure visibility.
- func largeSolutionStyle() -> some View {
- font(.system(.title3, weight: .bold))
- .allowsTightening(true)
- .fixedSize(horizontal: true, vertical: true)
- .minimumScaleFactor(0.5)
- .layoutPriority(10)
- .lineLimit(1)
- }
- /// Applies styling for warning labels.
- /// - Parameter warningColor: The color of the text.
- func warningStyle(_ warningColor: Color) -> some View {
- font(.subheadline)
- .foregroundStyle(warningColor)
- .allowsTightening(true)
- .minimumScaleFactor(0.5)
- }
- /// Reduces the default inset padding of List Sections for more compact presentation.
- /// Creates tighter spacing in the calculation cards.
- func listRowStyle() -> some View {
- listRowInsets(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10))
- }
- }
- // MARK: - Main PopupView
- // A detailed view presenting all components of the bolus calculation.
- // Displays breakdown of calculations in separate cards within a scrollable list,
- // with a sticky recommendation card at the bottom.
- struct PopupView: View {
- @Environment(\.colorScheme) var colorScheme
- /// State model containing all calculation parameters and results.
- var state: Treatments.StateModel
- /// Controls the preferred presentation size of the popup.
- @State private var calcPopupDetent = PresentationDetent.large
- /// Trigger for flashing scroll indicators when view appears.
- /// Helps users discover scrollable content.
- @State private var shouldFlashScroll = false
- var body: some View {
- NavigationStack {
- VStack(alignment: .center) {
- // List of calculation cards organized in sections.
- // Each section represents a component of the final calculation.
- List {
- Section("Glucose Calculation") {
- glucoseCardContent.listRowStyle()
- }
- Section("Insulin On Board (IOB)") {
- iobCardContent.listRowStyle()
- }
- Section("Carbs On Board (COB)") {
- cobCardContent.listRowStyle()
- }
- Section("Glucose Trend (15 min)") {
- deltaCardContent.listRowStyle()
- }
- Section("Full Bolus") {
- fullBolusCardContent.listRowStyle()
- }
- // Conditional sections based on user's selection of the "Super Bolus" option.
- if state.useSuperBolus {
- Section("Super Bolus") {
- superBolusCardContent.listRowStyle()
- }
- }
- // If the solution of this card does not recommend any insulin,
- // there's no point in showing it
- if state.factoredInsulin > 0 {
- Section("Applied Factors") {
- factorsCardContent.listRowStyle()
- }
- }
- }
- .frame(maxWidth: .infinity)
- .listStyle(InsetGroupedListStyle())
- .listSectionSpacing(0)
- .scrollIndicatorsFlash(trigger: shouldFlashScroll)
- .onAppear {
- // Flash scroll indicators after a short delay to help users discover scrollable content.
- // The delay allows the sheet presentation animation to complete first.
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
- shouldFlashScroll = true
- }
- }
- // Sticky footer with recommendation and dismiss button.
- // Remains visible regardless of scroll position.
- VStack(alignment: .center, spacing: 10) {
- recommendedBolusCard
- Button {
- state.showInfo = false
- } label: {
- Text("Got it!").bold()
- .frame(maxWidth: .infinity, minHeight: 30)
- }
- .buttonStyle(.bordered)
- }
- .padding([.horizontal, .bottom])
- }
- .navigationBarTitle(String(localized: "Bolus Calculator Details"), displayMode: .inline)
- .presentationDetents(
- [.fraction(0.9), .large],
- selection: $calcPopupDetent
- )
- }
- }
- // MARK: - Calculation Card Contents
- // Each card visualizes a specific component of the bolus calculation.
- // The cards use Grid layout to show mathematical formulas with proper alignment
- // of the variable's name as the header and the units used as the footer.
- // Inifinity frame on "=" operator aligns the formula to the left and the solution to the right of the row.
- /// Card showing insulin required to get current glucose to the target glucose based on insulin sensitivity.
- /// Formula: (Current Glucose - Target Glucose) / ISF = Glucose Correction Dose
- private var glucoseCardContent: some View {
- Grid(alignment: .center) {
- // Row 1: Column headers for the calculation components
- GridRow(alignment: .lastTextBaseline) {
- Text("Current")
- .gridCellColumns(3) // Allows label to expand above operators.
- Text("Target")
- Text("")
- .layoutPriority(-15)
- .gridCellColumns(2)
- Text("ISF")
- }
- .secondaryStyle()
- // Row 2: The calculation formula with values and operators
- GridRow {
- Text("(")
- .operatorStyle()
- Text(state.units == .mmolL ? state.currentBG.formattedAsMmolL : state.currentBG.description)
- .valueStyle()
- Text("−")
- .operatorStyle()
- Text(state.units == .mmolL ? state.target.formattedAsMmolL : state.target.description)
- .valueStyle()
- Text(")")
- .operatorStyle()
- Text("/")
- .operatorStyle()
- Text(state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description)
- .valueStyle()
- Text("=")
- .operatorStyle()
- .frame(idealWidth: 10, maxWidth: .infinity, alignment: .trailing)
- .layoutPriority(-15)
- Text(insulinFormatter(state.targetDifferenceInsulin))
- .solutionStyle(state.targetDifferenceInsulin)
- }
- // Row 3: Units for each value
- GridRow(alignment: .firstTextBaseline) {
- Text(state.units.rawValue)
- .gridCellColumns(3) // Allows cell to expand below operators.
- Text(state.units.rawValue)
- Text("")
- .layoutPriority(-15)
- .gridCellColumns(2)
- Text("\(state.units.rawValue)/U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- }
- .unitStyle()
- }
- .multilineTextAlignment(.center)
- }
- /// Card showing offset of current insulin on board (IOB).
- /// If current IOB is already positive, reduce the insulin recommendation,
- /// but if negative then increase the insulin recommendation.
- /// Formula: -1 × Current IOB = IOB Correction Dose
- private var iobCardContent: some View {
- Grid(alignment: .center) {
- // Row 1: Column header
- GridRow(alignment: .lastTextBaseline) {
- Text("")
- .layoutPriority(-15)
- .gridCellColumns(2)
- Text("IOB")
- }
- .secondaryStyle()
- // Row 2: The IOB calculation formula
- GridRow {
- Text("-1")
- .valueStyle()
- Text("×")
- .operatorStyle()
- Text(insulinFormatter(state.iob, .plain)) // Use .plain rounding to match inverted value.
- .valueStyle()
- Text("=").operatorStyle()
- .frame(idealWidth: 10, maxWidth: .infinity, alignment: .trailing)
- .layoutPriority(-15)
- Text(insulinFormatter(-1 * state.iob, .plain)) // Use .plain rounding to match inverted value.
- .solutionStyle(-1 * state.iob)
- }
- // Row 3: Units
- GridRow(alignment: .firstTextBaseline) {
- Text("")
- .layoutPriority(-15)
- .gridCellColumns(2)
- Text("U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- }
- .unitStyle()
- }
- .multilineTextAlignment(.center)
- }
- /// Card showing insulin required to offset meals. Combine current carbs on board (COB)
- /// with new carbs entered in the Treatment view and divide by the carb ratio.
- /// Don't allow total carbs to exceed Max IOB setting.
- /// Formula: (Current COB + New Carbs) / Carb Ratio = COB Correction Dose
- private var cobCardContent: some View {
- // Check if this is a backdated entry by comparing with the default date using a tolerance
- let isBackdated = abs(state.date.timeIntervalSince(state.defaultDate)) > 1.0
- // Determine COB and carbs to display based on backdating status
- let displayedCOB = isBackdated ? (state.simulatedDetermination?.cob ?? Decimal(state.cob)) : Decimal(state.cob)
- let displayedCarbs = isBackdated ? 0 : state.carbs
- let hasExceededMaxCOB: Bool = displayedCOB + displayedCarbs > state.maxCOB
- return Group {
- Grid(alignment: .center) {
- // Row 1: Column headers for the COB calculation
- GridRow(alignment: .lastTextBaseline) {
- Text("")
- .layoutPriority(-15)
- Text("COB")
- Text("Carbs")
- .gridCellColumns(3) // Allows label to expand above operators.
- Text("")
- .layoutPriority(-15)
- Text("CR")
- }
- .secondaryStyle()
- // Row 2: The full COB calculation formula
- // Don't include solution when Max IOB has been exceeded
- GridRow {
- Text("(")
- .operatorStyle()
- Text(Int(displayedCOB).description)
- .valueStyle()
- Text("+")
- .operatorStyle()
- Text(Int(displayedCarbs).description)
- .valueStyle()
- Text(")")
- .operatorStyle()
- Text("/")
- .operatorStyle()
- Text(state.carbRatio.formatted())
- .valueStyle()
- Text("=")
- .operatorStyle()
- .frame(idealWidth: 10, maxWidth: .infinity, alignment: .trailing)
- .layoutPriority(-15)
- if !hasExceededMaxCOB {
- Text(insulinFormatter(state.wholeCobInsulin))
- .solutionStyle(state.wholeCobInsulin)
- }
- }
- // Row 3: Units for each component
- // Don't show solution's unit if Max COB has been exceeded
- GridRow(alignment: .firstTextBaseline) {
- Text("")
- .layoutPriority(-15)
- Text("g")
- Text("")
- .layoutPriority(-15)
- Text("g")
- Text("")
- .layoutPriority(-15)
- .gridCellColumns(2)
- Text("g/U")
- if !hasExceededMaxCOB {
- Text("")
- .layoutPriority(-15)
- Text("U")
- }
- }
- .unitStyle()
- }
- .multilineTextAlignment(.center)
- if isBackdated {
- Text("Backdated carbs (\(Int(state.carbs)) g) included in COB calculation")
- .font(.caption)
- .foregroundStyle(.orange)
- .padding(.top, 4)
- }
- // Additional grid only displayed when Max COB limit has been exceeded
- if hasExceededMaxCOB {
- Grid(alignment: .center) {
- // Row 4: Alternative calculation headers (max COB)
- GridRow(alignment: .lastTextBaseline) {
- Text("Max COB")
- Text("")
- .layoutPriority(-15)
- Text("CR")
- }
- .secondaryStyle()
- // Row 5: Alternative calculation with max COB
- // Shows: Max COB / Carb Ratio = Limited COB Insulin
- GridRow {
- Text(Int(state.wholeCob).description)
- .valueStyle()
- .foregroundStyle(.orange)
- Text("/")
- .operatorStyle()
- Text(state.carbRatio.formatted())
- .valueStyle()
- Text("=").operatorStyle()
- .frame(idealWidth: 10, maxWidth: .infinity, alignment: .trailing)
- .layoutPriority(-15)
- Text(insulinFormatter(state.wholeCobInsulin))
- .solutionStyle(state.wholeCobInsulin)
- }
- // Row 6: Units for max COB calculation
- GridRow(alignment: .firstTextBaseline) {
- Text("g")
- Text("")
- .layoutPriority(-15)
- Text("g/U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- }
- .unitStyle()
- }
- .multilineTextAlignment(.center)
- }
- }
- }
- /// Card showing inslin required to offset glucose trend from past 15 minutes
- /// Formula: Change in Glucose / ISF = Glucose Trend Correction Dose
- private var deltaCardContent: some View {
- Grid(alignment: .center) {
- // Row 1: Column headers
- GridRow(alignment: .lastTextBaseline) {
- Text("Delta")
- Text("")
- .layoutPriority(-15)
- Text("ISF")
- }
- .secondaryStyle()
- // Row 2: The delta calculation formula
- GridRow {
- Text(state.units == .mmolL ? state.deltaBG.formattedAsMmolL : state.deltaBG.description)
- .valueStyle()
- Text("/")
- .operatorStyle()
- Text(state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description)
- .valueStyle()
- Text("=")
- .operatorStyle()
- .frame(idealWidth: 10, maxWidth: .infinity, alignment: .trailing)
- .layoutPriority(-15)
- Text(insulinFormatter(state.fifteenMinInsulin))
- .solutionStyle(state.fifteenMinInsulin)
- }
- // Row 3: Units for each component
- GridRow(alignment: .firstTextBaseline) {
- Text(state.units.rawValue)
- Text("")
- .layoutPriority(-15)
- Text("\(state.units.rawValue)/U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- }
- .unitStyle()
- }
- .multilineTextAlignment(.center)
- }
- /// Card showing combined calculation for full bolus (before factors)
- /// Combines all four individual components into a single dose.
- /// Formula: Glucose Dose + IOB Dose + COB Dose + Delta Dose = Full Bolus
- private var fullBolusCardContent: some View {
- Group {
- Grid(alignment: .center, horizontalSpacing: 1) {
- // Row 1: Column headers
- GridRow(alignment: .lastTextBaseline) {
- Text("Glucose")
- Text("")
- .layoutPriority(-15)
- Text("IOB")
- Text("")
- .layoutPriority(-15)
- Text("COB")
- Text("")
- .layoutPriority(-15)
- Text("Delta")
- }
- .secondaryStyle()
- // Row 2: The full bolus calculation formula components. (Values only.)
- // Infinity frames on operators distributes the formula across the entire row.
- GridRow {
- Text(wrapNegative(state.targetDifferenceInsulin))
- .valueStyle()
- Text("+")
- .operatorStyle()
- .frame(maxWidth: .infinity)
- Text(wrapNegative(-1 * state.iob, .plain))
- .valueStyle()
- Text("+")
- .operatorStyle()
- .frame(maxWidth: .infinity)
- Text(wrapNegative(state.wholeCobInsulin))
- .valueStyle()
- Text("+")
- .operatorStyle()
- .frame(maxWidth: .infinity)
- Text(wrapNegative(state.fifteenMinInsulin))
- .valueStyle()
- }
- // Row 3: Units for each component.
- GridRow(alignment: .firstTextBaseline) {
- Text("U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- }
- .unitStyle()
- }
- .multilineTextAlignment(.center)
- // Row 4: Sum/total of all components, aligned right.
- HStack(alignment: .center, spacing: 4) {
- Spacer()
- Text("=")
- .operatorStyle()
- HStack(alignment: .firstTextBaseline, spacing: 4) {
- Text(insulinFormatter(state.wholeCalc))
- .solutionStyle(state.wholeCalc)
- Text("U")
- .secondaryStyle()
- }
- }
- }
- }
- /// Card showing Super Bolus calculation (if selected by user).
- /// Converts a portion of basal insulin into immediate bolus for stronger bolus recommendation.
- /// Formula: Basal Rate × Super Bolus % = Super Bolus Insulin
- private var superBolusCardContent: some View {
- Grid(alignment: .center) {
- // Row 1: Column headers.
- GridRow(alignment: .lastTextBaseline) {
- Text("Basal Rate")
- Text("")
- .layoutPriority(-15)
- Text("Super Bolus %")
- .frame(minWidth: 90) // Discourages wrapping this cell into multiple lines.
- }
- .secondaryStyle()
- // Row 2: The super bolus calculation formula.
- GridRow {
- Text("\(state.currentBasal)")
- .valueStyle()
- Text("×")
- .operatorStyle()
- Text((100 * state.sweetMealFactor).formatted() + " %")
- .valueStyle()
- Text("=").operatorStyle()
- .frame(idealWidth: 10, maxWidth: .infinity, alignment: .trailing)
- .layoutPriority(-15)
- Text(insulinFormatter(state.superBolusInsulin))
- .solutionStyle(state.superBolusInsulin)
- }
- // Row 3: Units for each component.
- GridRow(alignment: .firstTextBaseline) {
- Text("U/hr")
- Text("")
- .layoutPriority(-15)
- .gridCellColumns(3)
- Text("U")
- }
- .unitStyle()
- }
- .multilineTextAlignment(.center)
- }
- /// Card showing applied factors to the final insulin calculation.
- /// Dynamically changes card based on user's selection in the Treatment view.
- /// User can choose Reduced Bolus, Super Bolus, or neither, but not both.
- private var factorsCardContent: some View {
- Grid(alignment: .center) {
- // Choose the layout based on which options are selected
- switch (state.useSuperBolus, state.useFattyMealCorrectionFactor) {
- // Simple case: just Full Bolus × Rec. Bolus %
- case (false, false):
- // Row 1: Header.
- GridRow(alignment: .lastTextBaseline) {
- Text("Full Bolus")
- Text("")
- .layoutPriority(-15)
- Text("Rec. Bolus %")
- }
- .secondaryStyle()
- // Row 2: Formula.
- GridRow {
- Text(insulinFormatter(state.wholeCalc))
- .valueStyle()
- Text("×")
- .operatorStyle()
- Text((100 * state.fraction).formatted() + " %")
- .valueStyle()
- Text("=")
- .operatorStyle()
- .frame(idealWidth: 10, maxWidth: .infinity, alignment: .trailing)
- .layoutPriority(-15)
- Text(insulinFormatter(state.factoredInsulin))
- .solutionStyle(state.factoredInsulin)
- }
- // Row 3: Units.
- GridRow(alignment: .firstTextBaseline) {
- Text("U")
- Text("")
- .layoutPriority(-15)
- .gridCellColumns(3)
- Text("U")
- }
- .unitStyle()
- // Case: Full Bolus × Rec. Bolus % × Reduced Bolus %
- case (false, true):
- // Row 1: Header.
- GridRow(alignment: .lastTextBaseline) {
- Text("Full Bolus")
- Text("")
- .layoutPriority(-15)
- Text("Rec. Bolus %")
- Text("")
- .layoutPriority(-15)
- Text("Red. Bolus %")
- }
- .secondaryStyle()
- // Row 2: Formula.
- GridRow {
- Text(insulinFormatter(state.wholeCalc)).valueStyle()
- Text("×")
- .operatorStyle()
- Text((100 * state.fraction).formatted() + " %")
- .valueStyle()
- Text("×")
- .operatorStyle()
- Text((100 * state.fattyMealFactor).formatted() + " %")
- .valueStyle()
- Text("=")
- .operatorStyle()
- .frame(idealWidth: 10, maxWidth: .infinity, alignment: .trailing)
- .layoutPriority(-15)
- Text(insulinFormatter(state.factoredInsulin))
- .solutionStyle(state.factoredInsulin)
- }
- // Row 3: Units.
- GridRow(alignment: .firstTextBaseline) {
- Text("U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- }
- .unitStyle()
- // Case: (Full Bolus × Rec. Bolus %) + Super Bolus
- case (true, false):
- if state.wholeCalc > 0 {
- // Row 1: Header.
- GridRow(alignment: .lastTextBaseline) {
- Text("Full Bolus")
- .gridCellColumns(3) // Allows label to expand above operators.
- Text("Rec. %")
- Text("")
- .layoutPriority(-15)
- .gridCellColumns(2)
- Text("Super Bolus")
- }
- .secondaryStyle()
- // Row 2: Formula.
- GridRow {
- Text("(")
- .operatorStyle()
- Text(insulinFormatter(state.wholeCalc)).valueStyle()
- Text("×")
- .operatorStyle()
- Text((100 * state.fraction).formatted() + " %")
- .valueStyle()
- Text(")")
- .operatorStyle()
- Text("+")
- .operatorStyle()
- Text(insulinFormatter(state.superBolusInsulin))
- .valueStyle()
- Text("=")
- .operatorStyle()
- .frame(idealWidth: 10, maxWidth: .infinity, alignment: .trailing)
- .layoutPriority(-15)
- Text(insulinFormatter(state.factoredInsulin))
- .solutionStyle(state.factoredInsulin)
- }
- // Row 3: Units.
- GridRow(alignment: .firstTextBaseline) {
- Text("")
- .layoutPriority(-15)
- Text("U")
- Text("")
- .layoutPriority(-15)
- .gridCellColumns(4)
- Text("U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- }
- .unitStyle()
- } else {
- // Row 1: Header.
- GridRow(alignment: .lastTextBaseline) {
- Text("Full Bolus")
- Text("")
- .layoutPriority(-15)
- Text("Super Bolus")
- }
- .secondaryStyle()
- // Row 2: Formula.
- GridRow {
- Text(insulinFormatter(state.wholeCalc)).valueStyle()
- Text("+")
- .operatorStyle()
- Text(insulinFormatter(state.superBolusInsulin))
- .valueStyle()
- Text("=")
- .operatorStyle()
- .frame(idealWidth: 10, maxWidth: .infinity, alignment: .trailing)
- .layoutPriority(-15)
- Text(insulinFormatter(state.factoredInsulin))
- .solutionStyle(state.factoredInsulin)
- }
- // Row 3: Units.
- GridRow(alignment: .firstTextBaseline) {
- Text("U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- Text("")
- .layoutPriority(-15)
- Text("U")
- }
- .unitStyle()
- }
- // This case should never occur as you can't apply a Super Bolus to a Fatty Meal
- // Per app logic, these options are mutually exclusive
- case (true, true):
- Text("")
- .layoutPriority(-15)
- }
- }
- .multilineTextAlignment(.center)
- }
- // MARK: - Result Section
- // Final recommendation display with warning conditions and limitations
- /// Recommended bolus card that stays fixed at bottom of the view
- /// Displays final calculated insulin amount with warnings based on various conditions:
- /// - Loop staleness
- /// - Very low glucose (current or forecasted)
- /// - Max bolus limits
- /// - Available IOB limits
- private var recommendedBolusCard: some View {
- /// Amount of insulin that can be dosed without exceeding Max IOB.
- let iobAvailable: Decimal = state.maxIOB - state.iob
- /// Checks if last loop was over 15 minutes ago.
- let isLoopStale = state.lastLoopDate == nil ||
- Date().timeIntervalSince(state.lastLoopDate!) > 15 * 60
- /// Computed property to determine if pump-compatible rounding was applied.
- /// Only relevant for positive insulin amounts.
- var isRoundedForPump: Bool {
- // Only check for rounding when we have a positive recommendation amount.
- if state.factoredInsulin > 0 {
- if state.factoredInsulin > iobAvailable {
- // Check if calculated insulin appears different from available IOB (limited by Max IOB)
- return insulinFormatter(state.insulinCalculated) != insulinFormatter(iobAvailable)
- } else {
- // Check if calculated insulin appears different from factored insulin (normal case)
- return insulinFormatter(state.insulinCalculated) != insulinFormatter(state.factoredInsulin)
- }
- }
- return false
- }
- return VStack(alignment: .center, spacing: 4) {
- let warningColor: Color = colorScheme == .dark ? .orange : .accentColor
- // Display appropriate warnings based on current conditions as a header on this card.
- // Each warning indicates a specific safety concern.
- if isLoopStale {
- Text("Last loop was > 15 m ago.")
- .warningStyle(warningColor)
- } else if state.currentBG < 54 {
- Text("Glucose is very low.")
- .warningStyle(.red)
- } else if state.minPredBG < 54 {
- Text("Glucose forecast is very low.")
- .warningStyle(warningColor)
- } else if state.factoredInsulin > state.maxBolus, state.maxBolus <= iobAvailable {
- Text("Max Bolus = \(insulinFormatter(state.maxBolus)) U")
- .warningStyle(warningColor)
- } else if state.factoredInsulin > 0, state.factoredInsulin > iobAvailable {
- // Available IOB warning with detailed breakdown.
- // Shows calculation: Max IOB - IOB = Available IOB
- if state.iob > state.maxIOB {
- Text("Current IOB (\(insulinFormatter(state.iob)) U) > Max IOB (\(insulinFormatter(state.maxIOB)) U)")
- .warningStyle(warningColor)
- } else {
- Text("Limited by Max IOB.")
- .warningStyle(warningColor)
- ViewThatFits(in: .horizontal) {
- // Option 1: Everything on one line (preferred if it fits)
- HStack(alignment: .firstTextBaseline, spacing: 0) {
- Text("Max IOB (")
- Text(insulinFormatter(state.maxIOB))
- .foregroundStyle(.primary)
- Text(" U) - Current IOB (")
- Text(insulinFormatter(state.iob))
- .foregroundStyle(.primary)
- Text(" U) = ")
- Text(insulinFormatter(iobAvailable))
- .foregroundStyle(.orange)
- Text(" U")
- }
- // Option 2: Two lines
- Grid {
- GridRow {
- Text("Max IOB")
- Text("")
- Text("IOB")
- Text("")
- Text("Limit")
- }
- GridRow {
- HStack(alignment: .firstTextBaseline, spacing: 0) {
- Text(insulinFormatter(state.maxIOB))
- .foregroundStyle(.primary)
- Text(" U")
- }
- Text("-")
- HStack(alignment: .firstTextBaseline, spacing: 0) {
- Text(wrapNegative(state.iob))
- .foregroundStyle(.primary)
- Text(" U")
- }
- Text("=")
- HStack(alignment: .firstTextBaseline, spacing: 0) {
- Text(insulinFormatter(iobAvailable))
- .foregroundStyle(.orange)
- Text(" U")
- }
- }
- }
- }
- .secondaryStyle()
- }
- }
- // Recommended Bolus card with accent-colored background
- ZStack {
- RoundedRectangle(cornerRadius: 12)
- .fill(Color.accentColor.opacity(0.1))
- HStack {
- VStack(alignment: .leading, spacing: 4) {
- Text("Recommended Bolus").font(.headline)
- // Only show "Rounded for pump" text when rounding was applied.
- if isRoundedForPump {
- Text("Rounded for pump")
- .secondaryStyle()
- }
- }
- .fixedSize(horizontal: true, vertical: true)
- Spacer()
- // Final insulin recommendation
- HStack(alignment: .firstTextBaseline, spacing: 4) {
- Text(insulinFormatter(state.insulinCalculated))
- .largeSolutionStyle()
- .foregroundStyle(state.insulinCalculated > 0 ? Color.accentColor : .primary)
- Text("U")
- .font(.subheadline)
- .foregroundStyle(.secondary)
- }
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
- }
- .fixedSize(horizontal: false, vertical: true)
- }
- }
- // MARK: - Helper Formatters
- // Functions for consistent number formatting throughout the view
- /// Formats insulin values with consistent decimal places
- /// - Parameters:
- /// - value: The insulin value to format
- /// - roundingMode: The rounding mode to apply (default: .down for conservative dosing)
- /// - Returns: A formatted string with 2 decimal places
- private func insulinFormatter(_ value: Decimal, _ roundingMode: NSDecimalNumber.RoundingMode = .down) -> String {
- let formatter = NumberFormatter()
- formatter.numberStyle = .decimal
- formatter.minimumFractionDigits = 2
- formatter.maximumFractionDigits = 2
- formatter.locale = Locale.current
- // Create a decimal handler with the specified rounding behavior.
- // Always rounds to 2 decimal places (0.01 U precision).
- let handler = NSDecimalNumberHandler(
- roundingMode: roundingMode,
- scale: 2,
- raiseOnExactness: false,
- raiseOnOverflow: false,
- raiseOnUnderflow: false,
- raiseOnDivideByZero: false
- )
- let roundedValue = NSDecimalNumber(decimal: value).rounding(accordingToBehavior: handler)
- return formatter.string(from: roundedValue) ?? "\(value)"
- }
- /// Wraps negative values in parentheses for clearer display in full bolus card.
- /// - Parameters:
- /// - value: The decimal value to format
- /// - roundingMode: The rounding mode to apply (default: .down)
- /// - Returns: A formatted string with parentheses for negative values
- private func wrapNegative(_ value: Decimal, _ roundingMode: NSDecimalNumber.RoundingMode = .down) -> String {
- value < 0 ? "(" + insulinFormatter(value, roundingMode) + ")" : insulinFormatter(value, roundingMode)
- }
- }
|