AlternativeBolusCalcRootView.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. import Charts
  2. import CoreData
  3. import SwiftUI
  4. import Swinject
  5. extension Bolus {
  6. struct AlternativeBolusCalcRootView: BaseView {
  7. let resolver: Resolver
  8. let waitForSuggestion: Bool
  9. let fetch: Bool
  10. @StateObject var state: StateModel
  11. @State private var showInfo = false
  12. @State private var exceededMaxBolus = false
  13. @State private var keepForNextWiew: Bool = false
  14. @State private var calculatorDetent = PresentationDetent.medium
  15. private enum Config {
  16. static let dividerHeight: CGFloat = 2
  17. static let spacing: CGFloat = 3
  18. }
  19. @Environment(\.colorScheme) var colorScheme
  20. @FetchRequest(
  21. entity: Meals.entity(),
  22. sortDescriptors: [NSSortDescriptor(key: "createdAt", ascending: false)]
  23. ) var meal: FetchedResults<Meals>
  24. private var formatter: NumberFormatter {
  25. let formatter = NumberFormatter()
  26. formatter.numberStyle = .decimal
  27. formatter.maximumFractionDigits = 2
  28. return formatter
  29. }
  30. private var mealFormatter: NumberFormatter {
  31. let formatter = NumberFormatter()
  32. formatter.numberStyle = .decimal
  33. formatter.maximumFractionDigits = 1
  34. return formatter
  35. }
  36. private var gluoseFormatter: NumberFormatter {
  37. let formatter = NumberFormatter()
  38. formatter.numberStyle = .decimal
  39. if state.units == .mmolL {
  40. formatter.maximumFractionDigits = 1
  41. } else { formatter.maximumFractionDigits = 0 }
  42. return formatter
  43. }
  44. private var fractionDigits: Int {
  45. if state.units == .mmolL {
  46. return 1
  47. } else { return 0 }
  48. }
  49. var body: some View {
  50. Form {
  51. Section {
  52. if state.waitForSuggestion {
  53. Text("Please wait")
  54. } else {
  55. predictionChart
  56. }
  57. } header: { Text("Predictions") }
  58. Section {}
  59. if fetch {
  60. Section {
  61. mealEntries
  62. } header: { Text("Meal Summary") }
  63. }
  64. Section {
  65. HStack {
  66. Button(action: {
  67. showInfo.toggle()
  68. }, label: {
  69. Image(systemName: "info.circle")
  70. Text("Calculations")
  71. })
  72. .foregroundStyle(.blue)
  73. .font(.footnote)
  74. .buttonStyle(PlainButtonStyle())
  75. .frame(maxWidth: .infinity, alignment: .leading)
  76. if state.fattyMeals {
  77. Spacer()
  78. Toggle(isOn: $state.useFattyMealCorrectionFactor) {
  79. Text("Fatty Meal")
  80. }
  81. .toggleStyle(CheckboxToggleStyle())
  82. .font(.footnote)
  83. .onChange(of: state.useFattyMealCorrectionFactor) { _ in
  84. state.insulinCalculated = state.calculateInsulin()
  85. }
  86. }
  87. }
  88. if state.waitForSuggestion {
  89. HStack {
  90. Text("Wait please").foregroundColor(.secondary)
  91. Spacer()
  92. ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
  93. }
  94. } else {
  95. HStack {
  96. Text("Recommended Bolus")
  97. Spacer()
  98. Text(
  99. formatter
  100. .string(from: Double(state.insulinCalculated) as NSNumber) ?? ""
  101. )
  102. Text(
  103. NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
  104. ).foregroundColor(.secondary)
  105. }.contentShape(Rectangle())
  106. .onTapGesture { state.amount = state.insulinCalculated }
  107. }
  108. HStack {
  109. Text("Bolus")
  110. Spacer()
  111. DecimalTextField(
  112. "0",
  113. value: $state.amount,
  114. formatter: formatter,
  115. autofocus: false,
  116. cleanInput: true
  117. )
  118. Text(exceededMaxBolus ? "😵" : " U").foregroundColor(.secondary)
  119. }
  120. .onChange(of: state.amount) { newValue in
  121. if newValue > state.maxBolus {
  122. exceededMaxBolus = true
  123. } else {
  124. exceededMaxBolus = false
  125. }
  126. }
  127. } header: { Text("Bolus") }
  128. if state.amount > 0 {
  129. Section {
  130. Button {
  131. keepForNextWiew = true
  132. state.add()
  133. }
  134. label: { Text(exceededMaxBolus ? "Max Bolus exceeded!" : "Enact bolus") }
  135. .frame(maxWidth: .infinity, alignment: .center)
  136. .disabled(disabled)
  137. .listRowBackground(!disabled ? Color(.systemBlue) : Color(.systemGray4))
  138. .tint(.white)
  139. }
  140. }
  141. if state.amount <= 0 {
  142. Section {
  143. Button {
  144. keepForNextWiew = true
  145. state.showModal(for: nil)
  146. }
  147. label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
  148. }
  149. }
  150. }
  151. .blur(radius: showInfo ? 3 : 0)
  152. .navigationTitle("Enact Bolus")
  153. .navigationBarTitleDisplayMode(.inline)
  154. .navigationBarItems(
  155. leading: Button {
  156. carbsView()
  157. }
  158. label: {
  159. HStack {
  160. Image(systemName: "chevron.backward")
  161. Text("Meal")
  162. }
  163. },
  164. trailing: Button { state.hideModal() }
  165. label: { Text("Close") }
  166. )
  167. .onAppear {
  168. configureView {
  169. state.waitForSuggestionInitial = waitForSuggestion
  170. state.waitForSuggestion = waitForSuggestion
  171. state.insulinCalculated = state.calculateInsulin()
  172. }
  173. }
  174. .onDisappear {
  175. if fetch, hasFatOrProtein, !keepForNextWiew, state.useCalc {
  176. state.delete(deleteTwice: true, meal: meal)
  177. } else if fetch, !keepForNextWiew, state.useCalc {
  178. state.delete(deleteTwice: false, meal: meal)
  179. }
  180. }
  181. .sheet(isPresented: $showInfo) {
  182. calculationsDetailView
  183. .presentationDetents(
  184. [fetch ? .large : .fraction(0.85), .large],
  185. selection: $calculatorDetent
  186. )
  187. }
  188. }
  189. var predictionChart: some View {
  190. ZStack {
  191. PredictionView(
  192. predictions: $state.predictions, units: $state.units, eventualBG: $state.evBG, target: $state.target,
  193. displayPredictions: $state.displayPredictions
  194. )
  195. }
  196. }
  197. var calcSettingsFirstRow: some View {
  198. GridRow {
  199. Group {
  200. Text("Carb Ratio:")
  201. .foregroundColor(.secondary)
  202. }.gridCellAnchor(.leading)
  203. Group {
  204. Text("ISF:")
  205. .foregroundColor(.secondary)
  206. }.gridCellAnchor(.leading)
  207. VStack {
  208. Text("Target:")
  209. .foregroundColor(.secondary)
  210. }.gridCellAnchor(.leading)
  211. }
  212. }
  213. var calcSettingsSecondRow: some View {
  214. GridRow {
  215. Text(state.carbRatio.formatted() + " " + NSLocalizedString("g/U", comment: " grams per Unit"))
  216. .gridCellAnchor(.leading)
  217. Text(
  218. state.isf.formatted() + " " + state.units
  219. .rawValue + NSLocalizedString("/U", comment: "/Insulin unit")
  220. ).gridCellAnchor(.leading)
  221. let target = state.units == .mmolL ? state.target.asMmolL : state.target
  222. Text(
  223. target
  224. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  225. " " + state.units.rawValue
  226. ).gridCellAnchor(.leading)
  227. }
  228. }
  229. var calcGlucoseFirstRow: some View {
  230. GridRow(alignment: .center) {
  231. let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
  232. let target = state.units == .mmolL ? state.target.asMmolL : state.target
  233. Text("Glucose:").foregroundColor(.secondary)
  234. let firstRow = currentBG
  235. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  236. + " - " +
  237. target
  238. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  239. + " = " +
  240. state.targetDifference
  241. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  242. Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
  243. .gridColumnAlignment(.leading)
  244. HStack {
  245. Text(
  246. self.insulinRounder(state.targetDifferenceInsulin).formatted()
  247. )
  248. Text("U").foregroundColor(.secondary)
  249. }.fontWeight(.bold)
  250. .gridColumnAlignment(.trailing)
  251. }
  252. }
  253. var calcGlucoseSecondRow: some View {
  254. GridRow(alignment: .center) {
  255. let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
  256. Text(
  257. currentBG
  258. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  259. " " +
  260. state.units.rawValue
  261. )
  262. let secondRow = state.targetDifference
  263. .formatted(
  264. .number.grouping(.never).rounded()
  265. .precision(.fractionLength(fractionDigits))
  266. )
  267. + " / " +
  268. state.isf.formatted()
  269. + " ≈ " +
  270. self.insulinRounder(state.targetDifferenceInsulin).formatted()
  271. Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
  272. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  273. }
  274. }
  275. var calcGlucoseFormulaRow: some View {
  276. GridRow(alignment: .top) {
  277. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  278. Text("(Current - Target) / ISF").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).gridColumnAlignment(.leading)
  279. .gridCellColumns(2)
  280. }
  281. .font(.caption)
  282. }
  283. var calcIOBRow: some View {
  284. GridRow(alignment: .center) {
  285. HStack {
  286. Text("IOB:").foregroundColor(.secondary)
  287. Text(
  288. self.insulinRounder(state.iob).formatted()
  289. )
  290. }
  291. Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
  292. HStack {
  293. Text(
  294. "-" + self.insulinRounder(state.iob).formatted()
  295. )
  296. Text("U").foregroundColor(.secondary)
  297. }.fontWeight(.bold)
  298. .gridColumnAlignment(.trailing)
  299. }
  300. }
  301. var calcCOBRow: some View {
  302. GridRow(alignment: .center) {
  303. HStack {
  304. Text("COB:").foregroundColor(.secondary)
  305. Text(
  306. state.cob
  307. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  308. NSLocalizedString(" g", comment: "grams")
  309. )
  310. }
  311. Text(
  312. state.cob
  313. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  314. + " / " +
  315. state.carbRatio.formatted()
  316. + " ≈ " +
  317. self.insulinRounder(state.wholeCobInsulin).formatted()
  318. )
  319. .foregroundColor(.secondary)
  320. .gridColumnAlignment(.leading)
  321. HStack {
  322. Text(
  323. self.insulinRounder(state.wholeCobInsulin).formatted()
  324. )
  325. Text("U").foregroundColor(.secondary)
  326. }.fontWeight(.bold)
  327. .gridColumnAlignment(.trailing)
  328. }
  329. }
  330. var calcCOBFormulaRow: some View {
  331. GridRow(alignment: .center) {
  332. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  333. Text("COB / Carb Ratio").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).gridColumnAlignment(.leading)
  334. .gridCellColumns(2)
  335. }
  336. .font(.caption)
  337. }
  338. var calcDeltaRow: some View {
  339. GridRow(alignment: .center) {
  340. Text("Delta:").foregroundColor(.secondary)
  341. let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
  342. Text(
  343. deltaBG
  344. .formatted(
  345. .number.grouping(.never).rounded()
  346. .precision(.fractionLength(fractionDigits))
  347. )
  348. + " / " +
  349. state.isf.formatted()
  350. + " ≈ " +
  351. self.insulinRounder(state.fifteenMinInsulin).formatted()
  352. )
  353. .foregroundColor(.secondary)
  354. .gridColumnAlignment(.leading)
  355. HStack {
  356. Text(
  357. self.insulinRounder(state.fifteenMinInsulin).formatted()
  358. )
  359. Text("U").foregroundColor(.secondary)
  360. }.fontWeight(.bold)
  361. .gridColumnAlignment(.trailing)
  362. }
  363. }
  364. var calcDeltaFormulaRow: some View {
  365. GridRow(alignment: .center) {
  366. let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
  367. Text(
  368. deltaBG
  369. .formatted(
  370. .number.grouping(.never).rounded()
  371. .precision(.fractionLength(fractionDigits))
  372. ) + " " +
  373. state.units.rawValue
  374. )
  375. Text("15min Delta / ISF").font(.caption).foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).gridColumnAlignment(.leading)
  376. .gridCellColumns(2).padding(.top, 5)
  377. }
  378. }
  379. var calcFullBolusRow: some View {
  380. GridRow(alignment: .center) {
  381. Text("Full Bolus")
  382. .foregroundColor(.secondary)
  383. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  384. HStack {
  385. Text(self.insulinRounder(state.wholeCalc).formatted())
  386. .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
  387. Text("U").foregroundColor(.secondary)
  388. }.gridColumnAlignment(.trailing)
  389. .fontWeight(.bold)
  390. }
  391. }
  392. var calcResultRow: some View {
  393. GridRow(alignment: .center) {
  394. Text("Result").fontWeight(.bold)
  395. HStack {
  396. let fraction = state.fraction
  397. Text(fraction.formatted())
  398. Text("x")
  399. .foregroundColor(.secondary)
  400. // if fatty meal is chosen
  401. if state.useFattyMealCorrectionFactor {
  402. let fattyMealFactor = state.fattyMealFactor
  403. Text(fattyMealFactor.formatted())
  404. .foregroundColor(.orange)
  405. Text("x")
  406. .foregroundColor(.secondary)
  407. }
  408. Text(self.insulinRounder(state.wholeCalc).formatted())
  409. .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
  410. Text("≈").foregroundColor(.secondary)
  411. }
  412. .gridColumnAlignment(.leading)
  413. HStack {
  414. Text(self.insulinRounder(state.insulinCalculated).formatted())
  415. .fontWeight(.bold)
  416. .foregroundColor(.blue)
  417. Text("U").foregroundColor(.secondary)
  418. }
  419. .gridColumnAlignment(.trailing)
  420. .fontWeight(.bold)
  421. }
  422. }
  423. var calcResultFormulaRow: some View {
  424. GridRow(alignment: .bottom) {
  425. if state.useFattyMealCorrectionFactor {
  426. Text("Factor x Fatty Meal Factor x Full Bolus")
  427. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  428. .font(.caption)
  429. .gridCellAnchor(.center)
  430. .gridCellColumns(3)
  431. } else {
  432. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  433. Text("Factor x Full Bolus")
  434. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  435. .font(.caption)
  436. .padding(.top, 5)
  437. .gridCellAnchor(.leading)
  438. .gridCellColumns(2)
  439. }
  440. }
  441. }
  442. var calculationsDetailView: some View {
  443. NavigationStack {
  444. ScrollView {
  445. Grid(alignment: .topLeading, horizontalSpacing: 3, verticalSpacing: 0) {
  446. GridRow {
  447. Text("Calculations").fontWeight(.bold).gridCellColumns(3).gridCellAnchor(.center).padding(.vertical)
  448. }
  449. calcSettingsFirstRow
  450. calcSettingsSecondRow
  451. DividerCustom()
  452. if fetch {
  453. // meal entries as grid rows
  454. GridRow {
  455. if let carbs = meal.first?.carbs, carbs > 0 {
  456. Text("Carbs").foregroundColor(.secondary)
  457. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  458. HStack {
  459. Text(carbs.formatted())
  460. Text("g").foregroundColor(.secondary)
  461. }.gridCellAnchor(.trailing)
  462. }
  463. }
  464. GridRow {
  465. if let fat = meal.first?.fat, fat > 0 {
  466. Text("Fat").foregroundColor(.secondary)
  467. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  468. HStack {
  469. Text(fat.formatted())
  470. Text("g").foregroundColor(.secondary)
  471. }.gridCellAnchor(.trailing)
  472. }
  473. }
  474. GridRow {
  475. if let protein = meal.first?.protein, protein > 0 {
  476. Text("Protein").foregroundColor(.secondary)
  477. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  478. HStack {
  479. Text(protein.formatted())
  480. Text("g").foregroundColor(.secondary)
  481. }.gridCellAnchor(.trailing)
  482. }
  483. }
  484. GridRow {
  485. if let note = meal.first?.note, note != "" {
  486. Text("Note").foregroundColor(.secondary)
  487. Text(note).foregroundColor(.secondary).gridCellColumns(2).gridCellAnchor(.trailing)
  488. }
  489. }
  490. DividerCustom()
  491. }
  492. GridRow {
  493. Text("Detailed Calculation Steps").gridCellColumns(3).gridCellAnchor(.center)
  494. .padding(.bottom, 10)
  495. }
  496. calcGlucoseFirstRow
  497. calcGlucoseSecondRow.padding(.bottom, 5)
  498. calcGlucoseFormulaRow
  499. DividerCustom()
  500. calcIOBRow
  501. DividerCustom()
  502. calcCOBRow.padding(.bottom, 5)
  503. calcCOBFormulaRow
  504. DividerCustom()
  505. calcDeltaRow
  506. calcDeltaFormulaRow
  507. DividerCustom()
  508. calcFullBolusRow
  509. DividerDouble()
  510. calcResultRow
  511. calcResultFormulaRow
  512. }
  513. Spacer()
  514. Button { showInfo = false }
  515. label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
  516. .buttonStyle(.bordered)
  517. .padding(.top)
  518. }
  519. .padding([.horizontal, .bottom])
  520. .font(.system(size: 15))
  521. }
  522. }
  523. private func insulinRounder(_ value: Decimal) -> Decimal {
  524. let toRound = NSDecimalNumber(decimal: value).doubleValue
  525. return Decimal(floor(100 * toRound) / 100)
  526. }
  527. private var disabled: Bool {
  528. state.amount <= 0 || state.amount > state.maxBolus
  529. }
  530. var changed: Bool {
  531. ((meal.first?.carbs ?? 0) > 0) || ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
  532. }
  533. var hasFatOrProtein: Bool {
  534. ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
  535. }
  536. func carbsView() {
  537. if fetch {
  538. keepForNextWiew = true
  539. state.backToCarbsView(complexEntry: true, meal, override: false)
  540. } else {
  541. state.backToCarbsView(complexEntry: false, meal, override: true)
  542. }
  543. }
  544. var mealEntries: some View {
  545. VStack {
  546. if let carbs = meal.first?.carbs, carbs > 0 {
  547. HStack {
  548. Text("Carbs").foregroundColor(.secondary)
  549. Spacer()
  550. Text(carbs.formatted())
  551. Text("g").foregroundColor(.secondary)
  552. }
  553. }
  554. if let fat = meal.first?.fat, fat > 0 {
  555. HStack {
  556. Text("Fat").foregroundColor(.secondary)
  557. Spacer()
  558. Text(fat.formatted())
  559. Text("g").foregroundColor(.secondary)
  560. }
  561. }
  562. if let protein = meal.first?.protein, protein > 0 {
  563. HStack {
  564. Text("Protein").foregroundColor(.secondary)
  565. Spacer()
  566. Text(protein.formatted())
  567. Text("g").foregroundColor(.secondary)
  568. }
  569. }
  570. if let note = meal.first?.note, note != "" {
  571. HStack {
  572. Text("Note").foregroundColor(.secondary)
  573. Spacer()
  574. Text(note).foregroundColor(.secondary)
  575. }
  576. }
  577. }
  578. }
  579. }
  580. struct DividerDouble: View {
  581. var body: some View {
  582. VStack(spacing: 2) {
  583. Rectangle()
  584. .frame(height: 1)
  585. .foregroundColor(.gray.opacity(0.65))
  586. Rectangle()
  587. .frame(height: 1)
  588. .foregroundColor(.gray.opacity(0.65))
  589. }
  590. .frame(height: 4)
  591. .padding(.vertical)
  592. }
  593. }
  594. struct DividerCustom: View {
  595. var body: some View {
  596. Rectangle()
  597. .frame(height: 1)
  598. .foregroundColor(.gray.opacity(0.65))
  599. .padding(.vertical)
  600. }
  601. }
  602. }