PopupView.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import SwiftUI
  2. struct PopupView: View {
  3. var state: Treatments.StateModel
  4. @Environment(\.colorScheme) var colorScheme
  5. private var fractionDigits: Int {
  6. if state.units == .mmolL {
  7. return 1
  8. } else { return 0 }
  9. }
  10. var body: some View {
  11. NavigationStack {
  12. ScrollView {
  13. Grid(alignment: .topLeading, horizontalSpacing: 3, verticalSpacing: 0) {
  14. GridRow {
  15. Text("Calculations").fontWeight(.bold).gridCellColumns(3).gridCellAnchor(.center).padding(.vertical)
  16. }
  17. calcSettingsFirstRow
  18. calcSettingsSecondRow
  19. DividerCustom()
  20. // meal entries as grid rows
  21. if state.carbs > 0 {
  22. GridRow {
  23. Text("Carbs").foregroundColor(.secondary)
  24. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  25. HStack {
  26. Text(state.carbs.formatted())
  27. Text("g").foregroundColor(.secondary)
  28. }.gridCellAnchor(.trailing)
  29. }
  30. }
  31. if state.fat > 0 {
  32. GridRow {
  33. Text("Fat").foregroundColor(.secondary)
  34. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  35. HStack {
  36. Text(state.fat.formatted())
  37. Text("g").foregroundColor(.secondary)
  38. }.gridCellAnchor(.trailing)
  39. }
  40. }
  41. if state.protein > 0 {
  42. GridRow {
  43. Text("Protein").foregroundColor(.secondary)
  44. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  45. HStack {
  46. Text(state.protein.formatted())
  47. Text("g").foregroundColor(.secondary)
  48. }.gridCellAnchor(.trailing)
  49. }
  50. }
  51. if state.carbs > 0 || state.protein > 0 || state.fat > 0 {
  52. DividerCustom()
  53. }
  54. GridRow {
  55. Text("Detailed Calculation Steps").gridCellColumns(3).gridCellAnchor(.center)
  56. .padding(.bottom, 10)
  57. }
  58. calcGlucoseFirstRow
  59. calcGlucoseSecondRow.padding(.bottom, 5)
  60. calcGlucoseFormulaRow
  61. DividerCustom()
  62. calcIOBRow
  63. DividerCustom()
  64. calcCOBRow.padding(.bottom, 5)
  65. calcCOBFormulaRow
  66. DividerCustom()
  67. calcDeltaRow
  68. calcDeltaFormulaRow
  69. DividerCustom(2)
  70. calcFullBolusRow
  71. if state.useSuperBolus {
  72. DividerCustom()
  73. calcSuperBolusRow
  74. calcSuperBolusFormulaRow
  75. }
  76. DividerDouble()
  77. if state.factoredInsulin > 0 {
  78. calcResultRow
  79. calcResultFormulaRow
  80. DividerCustom()
  81. }
  82. GridRow {
  83. Text("Recommended Bolus")
  84. .gridCellColumns(3)
  85. .gridCellAnchor(.center)
  86. .padding(.bottom, 10)
  87. }
  88. limitsRow
  89. }
  90. Spacer()
  91. Button { state.showInfo = false }
  92. label: { Text("Got it!").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center) }
  93. .buttonStyle(.bordered)
  94. .padding(.top)
  95. }
  96. .padding([.horizontal, .bottom])
  97. .font(.subheadline)
  98. }
  99. }
  100. var calcSettingsFirstRow: some View {
  101. GridRow {
  102. Group {
  103. Text("Carb Ratio:")
  104. .foregroundColor(.secondary)
  105. }.gridCellAnchor(.leading)
  106. Group {
  107. Text("ISF:")
  108. .foregroundColor(.secondary)
  109. }.gridCellAnchor(.leading)
  110. VStack {
  111. Text("Target:")
  112. .foregroundColor(.secondary)
  113. }.gridCellAnchor(.leading)
  114. }
  115. }
  116. var calcSettingsSecondRow: some View {
  117. GridRow {
  118. Text(state.carbRatio.formatted() + " " + String(localized: "g/U", comment: " grams per Unit"))
  119. .gridCellAnchor(.leading)
  120. let isf = state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description
  121. Text(
  122. isf + " " + state.units
  123. .rawValue + String(localized: "/U", comment: "/Insulin unit")
  124. ).gridCellAnchor(.leading)
  125. let target = state.units == .mmolL ? state.target.formattedAsMmolL : state.target.description
  126. Text(
  127. target +
  128. " " + state.units.rawValue
  129. ).gridCellAnchor(.leading)
  130. }
  131. }
  132. var calcGlucoseFirstRow: some View {
  133. GridRow(alignment: .center) {
  134. let currentBG = state.units == .mmolL ? state.currentBG.formattedAsMmolL : state.currentBG.description
  135. let target = state.units == .mmolL ? state.target.formattedAsMmolL : state.target.description
  136. Text("Glucose:").foregroundColor(.secondary)
  137. let targetDifference = state.units == .mmolL ? state.targetDifference.formattedAsMmolL : state.targetDifference
  138. .description
  139. let firstRow = currentBG
  140. + " - " +
  141. target
  142. + " = " +
  143. targetDifference
  144. Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
  145. .gridColumnAlignment(.leading)
  146. HStack {
  147. Text(
  148. self.insulinFormatter(state.targetDifferenceInsulin)
  149. )
  150. Text("U").foregroundColor(.secondary)
  151. }.fontWeight(.bold)
  152. .gridColumnAlignment(.trailing)
  153. }
  154. }
  155. var calcGlucoseSecondRow: some View {
  156. GridRow(alignment: .center) {
  157. let currentBG = state.units == .mmolL ? state.currentBG.formattedAsMmolL : state.currentBG.description
  158. Text(
  159. currentBG
  160. + " " +
  161. state.units.rawValue
  162. )
  163. let targetDifference = state.units == .mmolL ? state.targetDifference.formattedAsMmolL : state.targetDifference
  164. .description
  165. let secondRow = targetDifference + " / " +
  166. (state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description)
  167. .description + " ≈ " + self.insulinFormatter(state.targetDifferenceInsulin)
  168. Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
  169. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  170. }
  171. }
  172. var calcGlucoseFormulaRow: some View {
  173. GridRow(alignment: .top) {
  174. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  175. Text("(Current - Target) / ISF").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  176. .gridColumnAlignment(.leading)
  177. .gridCellColumns(2)
  178. }
  179. .font(.caption)
  180. }
  181. var calcIOBRow: some View {
  182. GridRow(alignment: .center) {
  183. HStack {
  184. Text("IOB:").foregroundColor(.secondary)
  185. Text(self.insulinFormatter(state.iob, .plain) + " U")
  186. }
  187. Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
  188. HStack {
  189. Text(self.insulinFormatter(-1 * state.iob, .plain))
  190. Text("U").foregroundColor(.secondary)
  191. }.fontWeight(.bold)
  192. .gridColumnAlignment(.trailing)
  193. }
  194. }
  195. var calcCOBRow: some View {
  196. GridRow(alignment: .center) {
  197. let maxCobReached: Bool = state.wholeCob >= state.maxCOB
  198. Text(maxCobReached ? "Max COB:" : "COB:")
  199. .foregroundColor(maxCobReached ? Color.loopRed : .secondary)
  200. // Middle column
  201. Text(
  202. state.wholeCob
  203. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  204. + " / " +
  205. state.carbRatio.formatted()
  206. + " ≈ " +
  207. self.insulinFormatter(state.wholeCobInsulin)
  208. )
  209. .foregroundColor(.secondary)
  210. .gridColumnAlignment(.leading)
  211. // Right column
  212. HStack {
  213. Text(self.insulinFormatter(state.wholeCobInsulin))
  214. Text("U").foregroundColor(.secondary)
  215. }
  216. .fontWeight(.bold)
  217. .gridColumnAlignment(.trailing)
  218. }
  219. }
  220. var calcCOBFormulaRow: some View {
  221. GridRow(alignment: .center) {
  222. Text(
  223. state.wholeCob
  224. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  225. String(localized: " g", comment: "grams")
  226. )
  227. Text("COB / Carb Ratio").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  228. .gridColumnAlignment(.leading).font(.caption)
  229. }
  230. }
  231. var calcDeltaRow: some View {
  232. GridRow(alignment: .center) {
  233. Text("Delta:").foregroundColor(.secondary)
  234. let deltaBG = state.units == .mmolL ? state.deltaBG.formattedAsMmolL : state.deltaBG.description
  235. let isf = state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description
  236. let fifteenMinInsulinFormatted = self.insulinFormatter(state.fifteenMinInsulin)
  237. Text(
  238. deltaBG + " / " + isf + " ≈ " + fifteenMinInsulinFormatted
  239. )
  240. .foregroundColor(.secondary)
  241. .gridColumnAlignment(.leading)
  242. HStack {
  243. Text(fifteenMinInsulinFormatted)
  244. Text("U").foregroundColor(.secondary)
  245. }.fontWeight(.bold)
  246. .gridColumnAlignment(.trailing)
  247. }
  248. }
  249. var calcDeltaFormulaRow: some View {
  250. GridRow(alignment: .center) {
  251. let deltaBG = state.units == .mmolL ? state.deltaBG.formattedAsMmolL : state.deltaBG.description
  252. Text(
  253. deltaBG
  254. + " " +
  255. state.units.rawValue
  256. )
  257. Text("15 min Delta / ISF").font(.caption).foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  258. .gridColumnAlignment(.leading)
  259. .gridCellColumns(2).padding(.top, 5)
  260. }
  261. }
  262. var calcFullBolusRow: some View {
  263. GridRow(alignment: .center) {
  264. Text("Full Bolus")
  265. .foregroundColor(.secondary)
  266. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  267. HStack {
  268. Text("≈").foregroundColor(.secondary)
  269. Text(self.insulinFormatter(state.wholeCalc))
  270. .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
  271. Text("U").foregroundColor(.secondary)
  272. }.gridColumnAlignment(.trailing)
  273. .fontWeight(.bold)
  274. }
  275. }
  276. var calcSuperBolusRow: some View {
  277. GridRow(alignment: .center) {
  278. Text("Super Bolus")
  279. .foregroundColor(.secondary)
  280. Text(
  281. "\(state.currentBasal) × \(100 * state.sweetMealFactor)% ≈ \(state.superBolusInsulin) "
  282. )
  283. .foregroundColor(.secondary)
  284. .gridColumnAlignment(.leading)
  285. HStack {
  286. Text("+" + self.insulinFormatter(state.superBolusInsulin))
  287. .foregroundStyle(Color.loopRed)
  288. Text("U").foregroundColor(.secondary)
  289. }.gridColumnAlignment(.trailing)
  290. .fontWeight(.bold)
  291. }
  292. }
  293. var calcSuperBolusFormulaRow: some View {
  294. GridRow(alignment: .center) {
  295. Text("\(state.currentBasal) U/hr")
  296. Text("Basal Rate × Super Bolus %").font(.caption)
  297. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  298. .gridColumnAlignment(.leading)
  299. .gridCellColumns(2).padding(.top, 5)
  300. }
  301. }
  302. var calcResultRow: some View {
  303. GridRow(alignment: .center) {
  304. Text("Factors").foregroundColor(.secondary)
  305. HStack {
  306. Text(state.useSuperBolus ? "(" : "")
  307. .foregroundColor(.loopRed)
  308. + Text(self.insulinFormatter(state.wholeCalc))
  309. .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.secondary)
  310. + Text(" × ")
  311. + Text((100 * state.fraction).formatted() + "%")
  312. // if fatty meal is chosen
  313. + Text(state.useFattyMealCorrectionFactor ? " × " : "")
  314. + Text(state.useFattyMealCorrectionFactor ? (100 * state.fattyMealFactor).formatted() + "%" : "")
  315. .foregroundColor(.orange)
  316. // endif fatty meal is chosen
  317. // if superbolus is chosen
  318. + Text(state.useSuperBolus ? ")" : "")
  319. .foregroundColor(.loopRed)
  320. + Text(state.useSuperBolus ? " + " : "")
  321. + Text(state.useSuperBolus ? self.insulinFormatter(state.superBolusInsulin) : "")
  322. .foregroundColor(.loopRed)
  323. // endif superbolus is chosen
  324. + Text(" ≈ ")
  325. }
  326. .gridColumnAlignment(.leading)
  327. .foregroundColor(.secondary)
  328. HStack {
  329. Text(self.insulinFormatter(state.factoredInsulin))
  330. .fontWeight(.bold)
  331. Text("U").foregroundColor(.secondary)
  332. }
  333. .gridColumnAlignment(.trailing)
  334. .fontWeight(.bold)
  335. }
  336. }
  337. var calcResultFormulaRow: some View {
  338. GridRow(alignment: .bottom) {
  339. if state.useFattyMealCorrectionFactor {
  340. Group {
  341. Text("Full Bolus x Rec. Bolus % x Fatty Meal %")
  342. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  343. }
  344. .font(.caption)
  345. .gridCellAnchor(.center)
  346. .gridCellColumns(3)
  347. } else if state.useSuperBolus {
  348. Group {
  349. Text("(Full Bolus x Rec. Bolus %) + Super Bolus")
  350. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  351. }
  352. .font(.caption)
  353. .gridCellAnchor(.center)
  354. .gridCellColumns(3)
  355. } else {
  356. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  357. Group {
  358. Text("Full Bolus x Rec. Bolus %")
  359. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  360. }
  361. .font(.caption)
  362. .padding(.top, 5)
  363. .gridCellAnchor(.leading)
  364. .gridCellColumns(2)
  365. }
  366. }
  367. }
  368. var limitsRow: some View {
  369. GridRow(alignment: .top) {
  370. Text("Limits").foregroundColor(.secondary)
  371. VStack {
  372. let iobAvailable: Decimal = state.maxIOB - state.iob
  373. if state.factoredInsulin < 0 {
  374. Text("No insulin recommended.")
  375. } else if state.currentBG < 54 {
  376. Text("Glucose is very low.")
  377. } else if state.minPredBG < 54 {
  378. Text("Glucose forecast is very low.")
  379. } else if state.maxBolus <= iobAvailable && state.factoredInsulin > state.maxBolus {
  380. Text("Max Bolus = \(insulinFormatter(state.maxBolus)) U")
  381. } else if state.factoredInsulin > iobAvailable {
  382. let iobFormatted = state.iob < 0 ? "(" + insulinFormatter(state.iob) + ")" : insulinFormatter(state.iob)
  383. Text(
  384. "\(insulinFormatter(state.maxIOB)) - \(iobFormatted) = \(insulinFormatter(iobAvailable)) U"
  385. )
  386. Text("Max IOB - Current IOB")
  387. .font(.caption)
  388. .foregroundColor(.secondary)
  389. }
  390. }
  391. .foregroundColor(Color.loopRed)
  392. HStack {
  393. Text(insulinFormatter(state.insulinCalculated))
  394. .foregroundColor(state.insulinCalculated > 0 ? Color.insulin : .primary)
  395. Text("U").foregroundColor(.secondary)
  396. }
  397. .fontWeight(.bold)
  398. .gridColumnAlignment(.trailing)
  399. }
  400. }
  401. private func insulinFormatter(_ value: Decimal, _ roundingMode: NSDecimalNumber.RoundingMode = .down) -> String {
  402. let formatter = NumberFormatter()
  403. formatter.numberStyle = .decimal
  404. formatter.minimumFractionDigits = 2
  405. formatter.maximumFractionDigits = 2
  406. formatter.locale = Locale.current
  407. let handler = NSDecimalNumberHandler(
  408. roundingMode: roundingMode,
  409. scale: 2,
  410. raiseOnExactness: false,
  411. raiseOnOverflow: false,
  412. raiseOnUnderflow: false,
  413. raiseOnDivideByZero: false
  414. )
  415. let roundedValue = NSDecimalNumber(decimal: value).rounding(accordingToBehavior: handler)
  416. return formatter.string(from: roundedValue) ?? "\(value)"
  417. }
  418. struct DividerDouble: View {
  419. var body: some View {
  420. VStack(spacing: 2) {
  421. Rectangle()
  422. .frame(height: 1)
  423. .foregroundColor(.gray.opacity(0.65))
  424. Rectangle()
  425. .frame(height: 1)
  426. .foregroundColor(.gray.opacity(0.65))
  427. }
  428. .frame(height: 4)
  429. .padding(.vertical)
  430. }
  431. }
  432. struct DividerCustom: View {
  433. var height: CGFloat
  434. init(_ height: CGFloat = 1) {
  435. self.height = height
  436. }
  437. var body: some View {
  438. Rectangle()
  439. .frame(height: height)
  440. .foregroundColor(.gray.opacity(0.65))
  441. .padding(.vertical)
  442. }
  443. }
  444. }