PopupView.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. import SwiftUI
  2. struct PopupView: View {
  3. @StateObject var state: Bolus.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()
  70. calcFullBolusRow
  71. if state.useSuperBolus {
  72. DividerCustom()
  73. calcSuperBolusRow
  74. }
  75. DividerDouble()
  76. calcResultRow
  77. calcResultFormulaRow
  78. }
  79. Spacer()
  80. Button { state.showInfo = false }
  81. label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
  82. .buttonStyle(.bordered)
  83. .padding(.top)
  84. }
  85. .padding([.horizontal, .bottom])
  86. .font(.subheadline)
  87. }
  88. }
  89. var calcSettingsFirstRow: some View {
  90. GridRow {
  91. Group {
  92. Text("Carb Ratio:")
  93. .foregroundColor(.secondary)
  94. }.gridCellAnchor(.leading)
  95. Group {
  96. Text("ISF:")
  97. .foregroundColor(.secondary)
  98. }.gridCellAnchor(.leading)
  99. VStack {
  100. Text("Target:")
  101. .foregroundColor(.secondary)
  102. }.gridCellAnchor(.leading)
  103. }
  104. }
  105. var calcSettingsSecondRow: some View {
  106. GridRow {
  107. Text(state.carbRatio.formatted() + " " + NSLocalizedString("g/U", comment: " grams per Unit"))
  108. .gridCellAnchor(.leading)
  109. Text(
  110. state.isf.formatted() + " " + state.units
  111. .rawValue + NSLocalizedString("/U", comment: "/Insulin unit")
  112. ).gridCellAnchor(.leading)
  113. let target = state.units == .mmolL ? state.target.asMmolL : state.target
  114. Text(
  115. target
  116. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  117. " " + state.units.rawValue
  118. ).gridCellAnchor(.leading)
  119. }
  120. }
  121. var calcGlucoseFirstRow: some View {
  122. GridRow(alignment: .center) {
  123. let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
  124. let target = state.units == .mmolL ? state.target.asMmolL : state.target
  125. Text("Glucose:").foregroundColor(.secondary)
  126. let targetDifference = state.units == .mmolL ? state.targetDifference.asMmolL : state.targetDifference
  127. let firstRow = currentBG
  128. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  129. + " - " +
  130. target
  131. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  132. + " = " +
  133. targetDifference
  134. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  135. Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
  136. .gridColumnAlignment(.leading)
  137. HStack {
  138. Text(
  139. self.insulinRounder(state.targetDifferenceInsulin).formatted()
  140. )
  141. Text("U").foregroundColor(.secondary)
  142. }.fontWeight(.bold)
  143. .gridColumnAlignment(.trailing)
  144. }
  145. }
  146. var calcGlucoseSecondRow: some View {
  147. GridRow(alignment: .center) {
  148. let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
  149. Text(
  150. currentBG
  151. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  152. " " +
  153. state.units.rawValue
  154. )
  155. let targetDifference = state.units == .mmolL ? state.targetDifference.asMmolL : state.targetDifference
  156. let secondRow = targetDifference
  157. .formatted(
  158. .number.grouping(.never).rounded()
  159. .precision(.fractionLength(fractionDigits))
  160. )
  161. + " / " +
  162. state.isf.formatted()
  163. + " ≈ " +
  164. self.insulinRounder(state.targetDifferenceInsulin).formatted()
  165. Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
  166. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  167. }
  168. }
  169. var calcGlucoseFormulaRow: some View {
  170. GridRow(alignment: .top) {
  171. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  172. Text("(Current - Target) / ISF").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  173. .gridColumnAlignment(.leading)
  174. .gridCellColumns(2)
  175. }
  176. .font(.caption)
  177. }
  178. var calcIOBRow: some View {
  179. GridRow(alignment: .center) {
  180. HStack {
  181. Text("IOB:").foregroundColor(.secondary)
  182. Text(
  183. self.insulinRounder(state.iob).formatted()
  184. )
  185. }
  186. Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
  187. let iobFormatted = self.insulinRounder(state.iob).formatted()
  188. HStack {
  189. Text((state.iob >= 0 ? "-" : "") + (state.iob >= 0 ? iobFormatted : "(" + iobFormatted + ")"))
  190. Text("U").foregroundColor(.secondary)
  191. }.fontWeight(.bold)
  192. .gridColumnAlignment(.trailing)
  193. }
  194. }
  195. var calcCOBRow: some View {
  196. GridRow(alignment: .center) {
  197. HStack {
  198. Text("COB:").foregroundColor(.secondary)
  199. Text(
  200. state.wholeCob
  201. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  202. NSLocalizedString(" g", comment: "grams")
  203. )
  204. }
  205. Text(
  206. state.wholeCob
  207. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  208. + " / " +
  209. state.carbRatio.formatted()
  210. + " ≈ " +
  211. self.insulinRounder(state.wholeCobInsulin).formatted()
  212. )
  213. .foregroundColor(.secondary)
  214. .gridColumnAlignment(.leading)
  215. HStack {
  216. Text(
  217. self.insulinRounder(state.wholeCobInsulin).formatted()
  218. )
  219. Text("U").foregroundColor(.secondary)
  220. }.fontWeight(.bold)
  221. .gridColumnAlignment(.trailing)
  222. }
  223. }
  224. var calcCOBFormulaRow: some View {
  225. GridRow(alignment: .center) {
  226. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  227. Text("COB / Carb Ratio").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  228. .gridColumnAlignment(.leading)
  229. .gridCellColumns(2)
  230. }
  231. .font(.caption)
  232. }
  233. var calcDeltaRow: some View {
  234. GridRow(alignment: .center) {
  235. Text("Delta:").foregroundColor(.secondary)
  236. let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
  237. Text(
  238. deltaBG
  239. .formatted(
  240. .number.grouping(.never).rounded()
  241. .precision(.fractionLength(fractionDigits))
  242. )
  243. + " / " +
  244. state.isf.formatted()
  245. + " ≈ " +
  246. self.insulinRounder(state.fifteenMinInsulin).formatted()
  247. )
  248. .foregroundColor(.secondary)
  249. .gridColumnAlignment(.leading)
  250. HStack {
  251. Text(
  252. self.insulinRounder(state.fifteenMinInsulin).formatted()
  253. )
  254. Text("U").foregroundColor(.secondary)
  255. }.fontWeight(.bold)
  256. .gridColumnAlignment(.trailing)
  257. }
  258. }
  259. var calcDeltaFormulaRow: some View {
  260. GridRow(alignment: .center) {
  261. let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
  262. Text(
  263. deltaBG
  264. .formatted(
  265. .number.grouping(.never).rounded()
  266. .precision(.fractionLength(fractionDigits))
  267. ) + " " +
  268. state.units.rawValue
  269. )
  270. Text("15min Delta / ISF").font(.caption).foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  271. .gridColumnAlignment(.leading)
  272. .gridCellColumns(2).padding(.top, 5)
  273. }
  274. }
  275. var calcFullBolusRow: some View {
  276. GridRow(alignment: .center) {
  277. Text("Full Bolus")
  278. .foregroundColor(.secondary)
  279. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  280. HStack {
  281. Text(self.insulinRounder(state.wholeCalc).formatted())
  282. .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
  283. Text("U").foregroundColor(.secondary)
  284. }.gridColumnAlignment(.trailing)
  285. .fontWeight(.bold)
  286. }
  287. }
  288. var calcSuperBolusRow: some View {
  289. GridRow(alignment: .center) {
  290. Text("Super Bolus")
  291. .foregroundColor(.secondary)
  292. Text("Added to Result").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
  293. HStack {
  294. Text("+" + self.insulinRounder(state.superBolusInsulin).formatted())
  295. .foregroundStyle(Color.loopRed)
  296. Text("U").foregroundColor(.secondary)
  297. }.gridColumnAlignment(.trailing)
  298. .fontWeight(.bold)
  299. }
  300. }
  301. var calcResultRow: some View {
  302. GridRow(alignment: .center) {
  303. Text("Result").fontWeight(.bold)
  304. HStack {
  305. Text(state.useSuperBolus ? "(" : "")
  306. .foregroundColor(.loopRed)
  307. + Text(state.fraction.formatted())
  308. + Text(" x ")
  309. .foregroundColor(.secondary)
  310. // if fatty meal is chosen
  311. + Text(state.useFattyMealCorrectionFactor ? state.fattyMealFactor.formatted() : "")
  312. .foregroundColor(.orange)
  313. + Text(state.useFattyMealCorrectionFactor ? " x " : "")
  314. .foregroundColor(.secondary)
  315. // endif fatty meal is chosen
  316. + Text(self.insulinRounder(state.wholeCalc).formatted())
  317. .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
  318. // if superbolus is chosen
  319. + Text(state.useSuperBolus ? ")" : "")
  320. .foregroundColor(.loopRed)
  321. + Text(state.useSuperBolus ? " + " : "")
  322. .foregroundColor(.secondary)
  323. + Text(state.useSuperBolus ? state.superBolusInsulin.formatted() : "")
  324. .foregroundColor(.loopRed)
  325. // endif superbolus is chosen
  326. + Text(" ≈ ")
  327. .foregroundColor(.secondary)
  328. }
  329. .gridColumnAlignment(.leading)
  330. HStack {
  331. Text(self.insulinRounder(state.insulinCalculated).formatted())
  332. .fontWeight(.bold)
  333. .foregroundColor(state.wholeCalc >= state.maxBolus ? Color.loopRed : Color.blue)
  334. Text("U").foregroundColor(.secondary)
  335. }
  336. .gridColumnAlignment(.trailing)
  337. .fontWeight(.bold)
  338. }
  339. }
  340. var calcResultFormulaRow: some View {
  341. GridRow(alignment: .bottom) {
  342. if state.useFattyMealCorrectionFactor {
  343. Group {
  344. Text("Factor x Fatty Meal Factor x Full Bolus")
  345. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  346. +
  347. Text(state.wholeCalc > state.maxBolus ? " ≈ Max Bolus" : "").foregroundColor(Color.loopRed)
  348. }
  349. .font(.caption)
  350. .gridCellAnchor(.center)
  351. .gridCellColumns(3)
  352. } else if state.useSuperBolus {
  353. Group {
  354. Text("(Factor x Full Bolus) + Super Bolus")
  355. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  356. +
  357. Text(state.wholeCalc > state.maxBolus ? " ≈ Max Bolus" : "").foregroundColor(Color.loopRed)
  358. }
  359. .font(.caption)
  360. .gridCellAnchor(.center)
  361. .gridCellColumns(3)
  362. } else {
  363. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  364. Group {
  365. Text("Factor x Full Bolus")
  366. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  367. +
  368. Text(state.wholeCalc > state.maxBolus ? " ≈ Max Bolus" : "").foregroundColor(Color.loopRed)
  369. }
  370. .font(.caption)
  371. .padding(.top, 5)
  372. .gridCellAnchor(.leading)
  373. .gridCellColumns(2)
  374. }
  375. }
  376. }
  377. private func insulinRounder(_ value: Decimal) -> Decimal {
  378. let toRound = NSDecimalNumber(decimal: value).doubleValue
  379. return Decimal(floor(100 * toRound) / 100)
  380. }
  381. struct DividerDouble: View {
  382. var body: some View {
  383. VStack(spacing: 2) {
  384. Rectangle()
  385. .frame(height: 1)
  386. .foregroundColor(.gray.opacity(0.65))
  387. Rectangle()
  388. .frame(height: 1)
  389. .foregroundColor(.gray.opacity(0.65))
  390. }
  391. .frame(height: 4)
  392. .padding(.vertical)
  393. }
  394. }
  395. struct DividerCustom: View {
  396. var body: some View {
  397. Rectangle()
  398. .frame(height: 1)
  399. .foregroundColor(.gray.opacity(0.65))
  400. .padding(.vertical)
  401. }
  402. }
  403. }
  404. // #Preview {
  405. // PopupView()
  406. // }