DefaultBolusCalcRootView.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import SwiftUI
  2. import Swinject
  3. extension Bolus {
  4. struct DefaultBolusCalcRootView: BaseView {
  5. let resolver: Resolver
  6. let waitForSuggestion: Bool
  7. let fetch: Bool
  8. @StateObject var state = StateModel()
  9. @State private var isAddInsulinAlertPresented = false
  10. @State private var presentInfo = false
  11. @State private var displayError = false
  12. @State private var keepForNextWiew: Bool = false
  13. @Environment(\.colorScheme) var colorScheme
  14. @FetchRequest(
  15. entity: Meals.entity(),
  16. sortDescriptors: [NSSortDescriptor(key: "createdAt", ascending: false)]
  17. ) var meal: FetchedResults<Meals>
  18. private var formatter: NumberFormatter {
  19. let formatter = NumberFormatter()
  20. formatter.numberStyle = .decimal
  21. formatter.maximumFractionDigits = 2
  22. return formatter
  23. }
  24. private var fractionDigits: Int {
  25. if state.units == .mmolL {
  26. return 1
  27. } else { return 0 }
  28. }
  29. var body: some View {
  30. Form {
  31. if fetch {
  32. Section {
  33. mealEntries
  34. } header: { Text("Meal Summary") }
  35. }
  36. Section {
  37. if state.waitForSuggestion {
  38. HStack {
  39. Text("Wait please").foregroundColor(.secondary)
  40. Spacer()
  41. ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
  42. }
  43. } else {
  44. HStack {
  45. Text("Insulin recommended")
  46. Image(systemName: "info.bubble")
  47. .symbolRenderingMode(.palette)
  48. .foregroundStyle(.primary, .blue)
  49. .onTapGesture {
  50. presentInfo.toggle()
  51. }
  52. Spacer()
  53. Text(
  54. formatter
  55. .string(from: state.insulinRecommended as NSNumber)! +
  56. NSLocalizedString(" U", comment: "Insulin unit")
  57. ).foregroundColor((state.error && state.insulinRecommended > 0) ? .red : .secondary)
  58. .onTapGesture {
  59. if state.error, state.insulinRecommended > 0 { displayError = true }
  60. else { state.amount = state.insulinRecommended }
  61. }
  62. }.contentShape(Rectangle())
  63. HStack {
  64. Text("Amount")
  65. Spacer()
  66. DecimalTextField(
  67. "0",
  68. value: $state.amount,
  69. formatter: formatter,
  70. autofocus: true,
  71. cleanInput: true
  72. )
  73. Text(!(state.amount > state.maxBolus) ? "U" : "😵").foregroundColor(.secondary)
  74. }
  75. }
  76. } header: { Text("Bolus") }
  77. if !state.waitForSuggestion {
  78. if state.amount > 0 {
  79. Section {
  80. Button {
  81. keepForNextWiew = true
  82. state.add()
  83. }
  84. label: { Text(!(state.amount > state.maxBolus) ? "Enact bolus" : "Max Bolus exceeded!") }
  85. .frame(maxWidth: .infinity, alignment: .center)
  86. .disabled(
  87. state.amount <= 0 || state.amount > state.maxBolus
  88. )
  89. }
  90. }
  91. }
  92. if state.amount <= 0 {
  93. Section {
  94. Button {
  95. keepForNextWiew = true
  96. state.showModal(for: nil)
  97. }
  98. label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
  99. }
  100. }
  101. }
  102. .alert(isPresented: $displayError) {
  103. Alert(
  104. title: Text("Warning!"),
  105. message: Text("\n" + alertString() + "\n"),
  106. primaryButton: .destructive(
  107. Text("Add"),
  108. action: {
  109. state.amount = state.insulinRecommended
  110. displayError = false
  111. }
  112. ),
  113. secondaryButton: .cancel()
  114. )
  115. }.onAppear {
  116. configureView {
  117. state.waitForSuggestionInitial = waitForSuggestion
  118. state.waitForSuggestion = waitForSuggestion
  119. }
  120. }
  121. .onDisappear {
  122. if fetch, hasFatOrProtein, !keepForNextWiew, !state.useCalc {
  123. state.delete(deleteTwice: true, id: meal.first?.id ?? "")
  124. } else if fetch, !keepForNextWiew, !state.useCalc {
  125. state.delete(deleteTwice: false, id: meal.first?.id ?? "")
  126. }
  127. }
  128. .navigationTitle("Enact Bolus")
  129. .navigationBarTitleDisplayMode(.inline)
  130. .navigationBarItems(
  131. leading: Button {
  132. carbssView()
  133. }
  134. label: { Text(fetch ? "Back" : "Meal") },
  135. trailing: Button { state.hideModal() }
  136. label: { Text("Close") }
  137. )
  138. .popup(isPresented: presentInfo, alignment: .center, direction: .bottom) {
  139. bolusInfo
  140. }
  141. }
  142. var changed: Bool {
  143. ((meal.first?.carbs ?? 0) > 0) || ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
  144. }
  145. var hasFatOrProtein: Bool {
  146. ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
  147. }
  148. func carbssView() {
  149. let id_ = meal.first?.id ?? ""
  150. if fetch {
  151. keepForNextWiew = true
  152. state.backToCarbsView(complexEntry: fetch, id_)
  153. } else {
  154. state.showModal(for: .addCarbs(editMode: false))
  155. }
  156. }
  157. var mealEntries: some View {
  158. VStack {
  159. if let carbs = meal.first?.carbs, carbs > 0 {
  160. HStack {
  161. Text("Carbs")
  162. Spacer()
  163. Text(carbs.formatted())
  164. Text("g")
  165. }.foregroundColor(.secondary)
  166. }
  167. if let fat = meal.first?.fat, fat > 0 {
  168. HStack {
  169. Text("Fat")
  170. Spacer()
  171. Text(fat.formatted())
  172. Text("g")
  173. }.foregroundColor(.secondary)
  174. }
  175. if let protein = meal.first?.protein, protein > 0 {
  176. HStack {
  177. Text("Protein")
  178. Spacer()
  179. Text(protein.formatted())
  180. Text("g")
  181. }.foregroundColor(.secondary)
  182. }
  183. if let note = meal.first?.note, note != "" {
  184. HStack {
  185. Text("Note")
  186. Spacer()
  187. Text(note)
  188. }.foregroundColor(.secondary)
  189. }
  190. }
  191. }
  192. var bolusInfo: some View {
  193. VStack {
  194. // Variables
  195. VStack(spacing: 3) {
  196. HStack {
  197. Text("Eventual Glucose").foregroundColor(.secondary)
  198. let evg = state.units == .mmolL ? Decimal(state.evBG).asMmolL : Decimal(state.evBG)
  199. Text(evg.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
  200. Text(state.units.rawValue).foregroundColor(.secondary)
  201. }
  202. HStack {
  203. Text("Target Glucose").foregroundColor(.secondary)
  204. let target = state.units == .mmolL ? state.target.asMmolL : state.target
  205. Text(target.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
  206. Text(state.units.rawValue).foregroundColor(.secondary)
  207. }
  208. HStack {
  209. Text("ISF").foregroundColor(.secondary)
  210. let isf = state.isf
  211. Text(isf.formatted())
  212. Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
  213. .foregroundColor(.secondary)
  214. }
  215. HStack {
  216. Text("ISF:")
  217. Text("Insulin Sensitivity")
  218. }.foregroundColor(.secondary).italic()
  219. if state.percentage != 100 {
  220. HStack {
  221. Text("Percentage setting").foregroundColor(.secondary)
  222. let percentage = state.percentage
  223. Text(percentage.formatted())
  224. Text("%").foregroundColor(.secondary)
  225. }
  226. }
  227. HStack {
  228. Text("Formula:")
  229. Text("(Eventual Glucose - Target) / ISF")
  230. }.foregroundColor(.secondary).italic().padding(.top, 5)
  231. }
  232. .font(.footnote)
  233. .padding(.top, 10)
  234. Divider()
  235. // Formula
  236. VStack(spacing: 5) {
  237. let unit = NSLocalizedString(
  238. " U",
  239. comment: "Unit in number of units delivered (keep the space character!)"
  240. )
  241. let color: Color = (state.percentage != 100 && state.insulin > 0) ? .secondary : .blue
  242. let fontWeight: Font.Weight = (state.percentage != 100 && state.insulin > 0) ? .regular : .bold
  243. HStack {
  244. Text(NSLocalizedString("Insulin recommended", comment: "") + ":").font(.callout)
  245. Text(state.insulin.formatted() + unit).font(.callout).foregroundColor(color).fontWeight(fontWeight)
  246. }
  247. if state.percentage != 100, state.insulin > 0 {
  248. Divider()
  249. HStack { Text(state.percentage.formatted() + " % ->").font(.callout).foregroundColor(.secondary)
  250. Text(
  251. state.insulinRecommended.formatted() + unit
  252. ).font(.callout).foregroundColor(.blue).bold()
  253. }
  254. }
  255. }
  256. // Warning
  257. if state.error, state.insulinRecommended > 0 {
  258. VStack(spacing: 5) {
  259. Divider()
  260. Text("Warning!").font(.callout).bold().foregroundColor(.orange)
  261. Text(alertString()).font(.footnote)
  262. Divider()
  263. }.padding(.horizontal, 10)
  264. }
  265. // Footer
  266. if !(state.error && state.insulinRecommended > 0) {
  267. VStack {
  268. Text(
  269. "Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended."
  270. ).font(.caption2).foregroundColor(.secondary)
  271. }.padding(20)
  272. }
  273. // Hide button
  274. VStack {
  275. Button { presentInfo = false }
  276. label: { Text("Hide") }.frame(maxWidth: .infinity, alignment: .center).font(.callout)
  277. .foregroundColor(.blue)
  278. }.padding(.bottom, 10)
  279. }
  280. .background(
  281. RoundedRectangle(cornerRadius: 8, style: .continuous)
  282. .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4))
  283. )
  284. }
  285. // Localize the Oref0 error/warning strings. The default should never be returned
  286. private func alertString() -> String {
  287. switch state.errorString {
  288. case 1,
  289. 2:
  290. return NSLocalizedString(
  291. "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
  292. comment: "Bolus pop-up / Alert string. Make translations concise!"
  293. ) + state.minGuardBG
  294. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state.units
  295. .rawValue + ", " +
  296. NSLocalizedString(
  297. "which is below your Threshold (",
  298. comment: "Bolus pop-up / Alert string. Make translations concise!"
  299. ) + state
  300. .threshold.formatted() + " " + state.units.rawValue + ")"
  301. case 3:
  302. return NSLocalizedString(
  303. "Eventual Glucose > Target Glucose, but glucose is climbing slower than expected. Expected: ",
  304. comment: "Bolus pop-up / Alert string. Make translations concise!"
  305. ) +
  306. state.expectedDelta
  307. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  308. NSLocalizedString(". Climbing: ", comment: "Bolus pop-up / Alert string. Make translatons concise!") + state
  309. .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  310. case 4:
  311. return NSLocalizedString(
  312. "Eventual Glucose > Target Glucose, but glucose is falling faster than expected. Expected: ",
  313. comment: "Bolus pop-up / Alert string. Make translations concise!"
  314. ) +
  315. state.expectedDelta
  316. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  317. NSLocalizedString(". Falling: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
  318. .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  319. case 5:
  320. return NSLocalizedString(
  321. "Eventual Glucose > Target Glucose, but glucose is changing faster than expected. Expected: ",
  322. comment: "Bolus pop-up / Alert string. Make translations concise!"
  323. ) +
  324. state.expectedDelta
  325. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  326. NSLocalizedString(". Changing: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
  327. .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  328. case 6:
  329. return NSLocalizedString(
  330. "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
  331. comment: "Bolus pop-up / Alert string. Make translations concise!"
  332. ) + state
  333. .minPredBG
  334. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state
  335. .units
  336. .rawValue
  337. default:
  338. return "Ignore Warning..."
  339. }
  340. }
  341. }
  342. }