AlternativeBolusCalcRootView.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  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. private enum Config {
  15. static let dividerHeight: CGFloat = 2
  16. static let overlayColour: Color = .white // Currently commented out
  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. if fetch {
  52. Section {
  53. VStack {
  54. if let carbs = meal.first?.carbs, carbs > 0 {
  55. HStack {
  56. Text("Carbs")
  57. Spacer()
  58. Text(carbs.formatted())
  59. Text("g")
  60. }.foregroundColor(.secondary)
  61. }
  62. if let fat = meal.first?.fat, fat > 0 {
  63. HStack {
  64. Text("Fat")
  65. Spacer()
  66. Text(fat.formatted())
  67. Text("g")
  68. }.foregroundColor(.secondary)
  69. }
  70. if let protein = meal.first?.protein, protein > 0 {
  71. HStack {
  72. Text("Protein")
  73. Spacer()
  74. Text(protein.formatted())
  75. Text("g")
  76. }.foregroundColor(.secondary)
  77. }
  78. if let note = meal.first?.note, note != "" {
  79. HStack {
  80. Text("Note")
  81. Spacer()
  82. Text(note)
  83. }.foregroundColor(.secondary)
  84. }
  85. }
  86. } header: { Text("Meal Summary") }
  87. }
  88. Section {
  89. Button {
  90. let id_ = meal.first?.id ?? ""
  91. if fetch {
  92. keepForNextWiew = true
  93. state.backToCarbsView(complexEntry: fetch, id_)
  94. } else {
  95. state.showModal(for: .addCarbs(editMode: false))
  96. }
  97. }
  98. label: { Text(fetch ? "Edit Meal" : "Add Meal") }.frame(maxWidth: .infinity, alignment: .center)
  99. } header: { Text(!fetch ? "Meal Summary" : "") }
  100. Section {
  101. HStack {
  102. Button(action: {
  103. showInfo.toggle()
  104. }, label: {
  105. Image(systemName: "info.circle")
  106. Text("Calculations")
  107. })
  108. .foregroundStyle(.blue)
  109. .font(.footnote)
  110. .buttonStyle(PlainButtonStyle())
  111. .frame(maxWidth: .infinity, alignment: .leading)
  112. if state.fattyMeals {
  113. Spacer()
  114. Toggle(isOn: $state.useFattyMealCorrectionFactor) {
  115. Text("Fatty Meal")
  116. }
  117. .toggleStyle(CheckboxToggleStyle())
  118. .font(.footnote)
  119. .onChange(of: state.useFattyMealCorrectionFactor) { _ in
  120. state.insulinCalculated = state.calculateInsulin()
  121. }
  122. }
  123. }
  124. if state.waitForSuggestion {
  125. HStack {
  126. Text("Wait please").foregroundColor(.secondary)
  127. Spacer()
  128. ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
  129. }
  130. } else {
  131. HStack {
  132. Text("Recommended Bolus")
  133. Spacer()
  134. Text(
  135. formatter
  136. .string(from: Double(state.insulinCalculated) as NSNumber) ?? ""
  137. )
  138. Text(
  139. NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
  140. ).foregroundColor(.secondary)
  141. }.contentShape(Rectangle())
  142. .onTapGesture { state.amount = state.insulinCalculated }
  143. }
  144. if !state.waitForSuggestion {
  145. HStack {
  146. Text("Bolus")
  147. Spacer()
  148. DecimalTextField(
  149. "0",
  150. value: $state.amount,
  151. formatter: formatter,
  152. autofocus: false,
  153. cleanInput: true
  154. )
  155. Text(exceededMaxBolus ? "😵" : " U").foregroundColor(.secondary)
  156. }
  157. .onChange(of: state.amount) { newValue in
  158. if newValue > state.maxBolus {
  159. exceededMaxBolus = true
  160. } else {
  161. exceededMaxBolus = false
  162. }
  163. }
  164. }
  165. } header: { Text("Bolus Summary") }
  166. if state.amount > 0 {
  167. Section {
  168. Button {
  169. keepForNextWiew = true
  170. state.add()
  171. }
  172. label: { Text(exceededMaxBolus ? "Max Bolus exceeded!" : "Enact bolus") }
  173. .frame(maxWidth: .infinity, alignment: .center)
  174. .foregroundColor(exceededMaxBolus ? .loopRed : .accentColor)
  175. .disabled(
  176. state.amount <= 0 || state.amount > state.maxBolus
  177. )
  178. }
  179. }
  180. Section {
  181. Button {
  182. keepForNextWiew = true
  183. state.showModal(for: nil)
  184. }
  185. label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
  186. }
  187. }
  188. .blur(radius: showInfo ? 3 : 0)
  189. .navigationTitle("Enact Bolus")
  190. .navigationBarTitleDisplayMode(.inline)
  191. .navigationBarItems(
  192. leading: Button { state.hideModal() }
  193. label: { Text("Close") }
  194. )
  195. .onAppear {
  196. configureView {
  197. state.waitForSuggestionInitial = waitForSuggestion
  198. state.waitForSuggestion = waitForSuggestion
  199. state.insulinCalculated = state.calculateInsulin()
  200. }
  201. }
  202. .onDisappear {
  203. if fetch, hasFatOrProtein, !keepForNextWiew {
  204. state.delete(deleteTwice: true, id: meal.first?.id ?? "")
  205. } else if fetch, !keepForNextWiew {
  206. state.delete(deleteTwice: false, id: meal.first?.id ?? "")
  207. }
  208. }
  209. .popup(isPresented: showInfo) {
  210. bolusInfoAlternativeCalculator
  211. }
  212. }
  213. var changed: Bool {
  214. ((meal.first?.carbs ?? 0) > 0) || ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
  215. }
  216. var hasFatOrProtein: Bool {
  217. ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
  218. }
  219. // Pop-up
  220. var bolusInfoAlternativeCalculator: some View {
  221. VStack {
  222. let unit = NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
  223. VStack {
  224. VStack(spacing: Config.spacing) {
  225. HStack {
  226. Text("Calculations")
  227. .font(.title3).frame(maxWidth: .infinity, alignment: .center)
  228. }.padding(10)
  229. if fetch {
  230. VStack {
  231. if let note = meal.first?.note, note != "" {
  232. HStack {
  233. Text("Note")
  234. .foregroundColor(.secondary)
  235. Spacer()
  236. Text(note)
  237. }
  238. }
  239. if let carbs = meal.first?.carbs, carbs > 0 {
  240. HStack {
  241. Text("Carbs")
  242. .foregroundColor(.secondary)
  243. Spacer()
  244. Text(mealFormatter.string(from: carbs as NSNumber) ?? "")
  245. Text("g").foregroundColor(.secondary)
  246. }
  247. }
  248. if let protein = meal.first?.protein, protein > 0 {
  249. HStack {
  250. Text("Protein")
  251. .foregroundColor(.secondary)
  252. Spacer()
  253. Text(mealFormatter.string(from: protein as NSNumber) ?? "")
  254. Text("g").foregroundColor(.secondary)
  255. }
  256. }
  257. if let fat = meal.first?.fat, fat > 0 {
  258. HStack {
  259. Text("Fat")
  260. .foregroundColor(.secondary)
  261. Spacer()
  262. Text(mealFormatter.string(from: fat as NSNumber) ?? "")
  263. Text("g").foregroundColor(.secondary)
  264. }
  265. }
  266. }.padding()
  267. }
  268. if fetch { Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
  269. }
  270. VStack {
  271. HStack {
  272. Text("Carb Ratio")
  273. .foregroundColor(.secondary)
  274. Spacer()
  275. Text(state.carbRatio.formatted())
  276. Text(NSLocalizedString(" g/U", comment: " grams per Unit"))
  277. .foregroundColor(.secondary)
  278. }
  279. HStack {
  280. Text("ISF")
  281. .foregroundColor(.secondary)
  282. Spacer()
  283. let isf = state.isf
  284. Text(isf.formatted())
  285. Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
  286. .foregroundColor(.secondary)
  287. }
  288. HStack {
  289. Text("Target Glucose")
  290. .foregroundColor(.secondary)
  291. Spacer()
  292. let target = state.units == .mmolL ? state.target.asMmolL : state.target
  293. Text(
  294. target
  295. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  296. )
  297. Text(state.units.rawValue)
  298. .foregroundColor(.secondary)
  299. }
  300. HStack {
  301. Text("Basal")
  302. .foregroundColor(.secondary)
  303. Spacer()
  304. let basal = state.basal
  305. Text(basal.formatted())
  306. Text(NSLocalizedString(" U/h", comment: " Units per hour"))
  307. .foregroundColor(.secondary)
  308. }
  309. HStack {
  310. Text("Fraction")
  311. .foregroundColor(.secondary)
  312. Spacer()
  313. let fraction = state.fraction
  314. Text(fraction.formatted())
  315. }
  316. if state.useFattyMealCorrectionFactor {
  317. HStack {
  318. Text("Fatty Meal Factor")
  319. .foregroundColor(.orange)
  320. Spacer()
  321. let fraction = state.fattyMealFactor
  322. Text(fraction.formatted())
  323. .foregroundColor(.orange)
  324. }
  325. }
  326. }.padding()
  327. }
  328. Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
  329. VStack(spacing: Config.spacing) {
  330. HStack {
  331. Text("Glucose")
  332. .foregroundColor(.secondary)
  333. Spacer()
  334. let glucose = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
  335. Text(glucose.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
  336. Text(state.units.rawValue)
  337. .foregroundColor(.secondary)
  338. Spacer()
  339. Image(systemName: "arrow.right")
  340. Spacer()
  341. let targetDifferenceInsulin = state.targetDifferenceInsulin
  342. // rounding
  343. let targetDifferenceInsulinAsDouble = NSDecimalNumber(decimal: targetDifferenceInsulin).doubleValue
  344. let roundedTargetDifferenceInsulin = Decimal(round(100 * targetDifferenceInsulinAsDouble) / 100)
  345. Text(roundedTargetDifferenceInsulin.formatted())
  346. Text(unit)
  347. .foregroundColor(.secondary)
  348. }
  349. HStack {
  350. Text("IOB")
  351. .foregroundColor(.secondary)
  352. Spacer()
  353. let iob = state.iob
  354. // rounding
  355. let iobAsDouble = NSDecimalNumber(decimal: iob).doubleValue
  356. let roundedIob = Decimal(round(100 * iobAsDouble) / 100)
  357. Text(roundedIob.formatted())
  358. Text(unit)
  359. .foregroundColor(.secondary)
  360. Spacer()
  361. Image(systemName: "arrow.right")
  362. Spacer()
  363. let iobCalc = state.iobInsulinReduction
  364. // rounding
  365. let iobCalcAsDouble = NSDecimalNumber(decimal: iobCalc).doubleValue
  366. let roundedIobCalc = Decimal(round(100 * iobCalcAsDouble) / 100)
  367. Text(roundedIobCalc.formatted())
  368. Text(unit).foregroundColor(.secondary)
  369. }
  370. HStack {
  371. Text("Trend")
  372. .foregroundColor(.secondary)
  373. Spacer()
  374. let trend = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
  375. Text(trend.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
  376. Text(state.units.rawValue).foregroundColor(.secondary)
  377. Spacer()
  378. Image(systemName: "arrow.right")
  379. Spacer()
  380. let trendInsulin = state.fifteenMinInsulin
  381. // rounding
  382. let trendInsulinAsDouble = NSDecimalNumber(decimal: trendInsulin).doubleValue
  383. let roundedTrendInsulin = Decimal(round(100 * trendInsulinAsDouble) / 100)
  384. Text(roundedTrendInsulin.formatted())
  385. Text(unit)
  386. .foregroundColor(.secondary)
  387. }
  388. HStack {
  389. Text("COB")
  390. .foregroundColor(.secondary)
  391. Spacer()
  392. let cob = state.cob
  393. Text(cob.formatted())
  394. let unitGrams = NSLocalizedString(" g", comment: "grams")
  395. Text(unitGrams).foregroundColor(.secondary)
  396. Spacer()
  397. Image(systemName: "arrow.right")
  398. Spacer()
  399. let insulinCob = state.wholeCobInsulin
  400. // rounding
  401. let insulinCobAsDouble = NSDecimalNumber(decimal: insulinCob).doubleValue
  402. let roundedInsulinCob = Decimal(round(100 * insulinCobAsDouble) / 100)
  403. Text(roundedInsulinCob.formatted())
  404. Text(unit)
  405. .foregroundColor(.secondary)
  406. }
  407. }.padding()
  408. Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
  409. VStack {
  410. HStack {
  411. Text("Full Bolus")
  412. .foregroundColor(.secondary)
  413. Spacer()
  414. let insulin = state.roundedWholeCalc
  415. Text(insulin.formatted()).foregroundStyle(state.roundedWholeCalc < 0 ? Color.loopRed : Color.primary)
  416. Text(unit)
  417. .foregroundColor(.secondary)
  418. }
  419. }.padding(.horizontal)
  420. Divider().frame(height: Config.dividerHeight)
  421. VStack {
  422. HStack {
  423. Text("Result")
  424. .fontWeight(.bold)
  425. Spacer()
  426. let fraction = state.fraction
  427. Text(fraction.formatted())
  428. Text(" x ")
  429. .foregroundColor(.secondary)
  430. // if fatty meal is chosen
  431. if state.useFattyMealCorrectionFactor {
  432. let fattyMealFactor = state.fattyMealFactor
  433. Text(fattyMealFactor.formatted())
  434. .foregroundColor(.orange)
  435. Text(" x ")
  436. .foregroundColor(.secondary)
  437. }
  438. let insulin = state.roundedWholeCalc
  439. Text(insulin.formatted()).foregroundStyle(state.roundedWholeCalc < 0 ? Color.loopRed : Color.primary)
  440. Text(unit)
  441. .foregroundColor(.secondary)
  442. Text(" = ")
  443. .foregroundColor(.secondary)
  444. let result = state.insulinCalculated
  445. // rounding
  446. let resultAsDouble = NSDecimalNumber(decimal: result).doubleValue
  447. let roundedResult = Decimal(round(100 * resultAsDouble) / 100)
  448. Text(roundedResult.formatted())
  449. .fontWeight(.bold)
  450. .font(.system(size: 16))
  451. .foregroundColor(.blue)
  452. Text(unit)
  453. .foregroundColor(.secondary)
  454. }
  455. }.padding()
  456. Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
  457. if exceededMaxBolus {
  458. HStack {
  459. let maxBolus = state.maxBolus
  460. let maxBolusFormatted = maxBolus.formatted()
  461. Text("Your entered amount was limited by your max Bolus setting of \(maxBolusFormatted)\(unit)!")
  462. }
  463. .padding()
  464. .fontWeight(.semibold)
  465. .foregroundStyle(Color.loopRed)
  466. }
  467. }
  468. .padding(.top, 10)
  469. .padding(.bottom, 15)
  470. // Hide pop-up
  471. VStack {
  472. Button {
  473. showInfo = false
  474. }
  475. label: {
  476. Text("OK")
  477. }
  478. .frame(maxWidth: .infinity, alignment: .center)
  479. .font(.system(size: 16))
  480. .fontWeight(.semibold)
  481. .foregroundColor(.blue)
  482. }
  483. .padding(.bottom, 20)
  484. }
  485. .font(.footnote)
  486. .background(
  487. RoundedRectangle(cornerRadius: 10, style: .continuous)
  488. .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4).opacity(0.9))
  489. )
  490. }
  491. }
  492. }