HomeRootView.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. import SpriteKit
  2. import SwiftDate
  3. import SwiftUI
  4. import Swinject
  5. extension Home {
  6. struct RootView: BaseView {
  7. let resolver: Resolver
  8. @StateObject var state = StateModel()
  9. @State var isStatusPopupPresented = false
  10. @State var selectedState: durationState
  11. @State var hba1c_all: String
  12. @State var average_: String
  13. @State var median_: String
  14. @State var tir_low: String
  15. @State var tir_high: String
  16. @State var tir_: String
  17. @State var hba1c_: String
  18. @State var sd_: String
  19. @State var cv_: String
  20. private var numberFormatter: NumberFormatter {
  21. let formatter = NumberFormatter()
  22. formatter.numberStyle = .decimal
  23. formatter.maximumFractionDigits = 2
  24. return formatter
  25. }
  26. private var targetFormatter: NumberFormatter {
  27. let formatter = NumberFormatter()
  28. formatter.numberStyle = .decimal
  29. formatter.maximumFractionDigits = 1
  30. return formatter
  31. }
  32. private var tirFormatter: NumberFormatter {
  33. let formatter = NumberFormatter()
  34. formatter.numberStyle = .decimal
  35. formatter.maximumFractionDigits = 0
  36. return formatter
  37. }
  38. private var dateFormatter: DateFormatter {
  39. let dateFormatter = DateFormatter()
  40. dateFormatter.timeStyle = .short
  41. return dateFormatter
  42. }
  43. private var spriteScene: SKScene {
  44. let scene = SnowScene()
  45. scene.scaleMode = .resizeFill
  46. scene.backgroundColor = .clear
  47. return scene
  48. }
  49. @ViewBuilder func header(_ geo: GeometryProxy) -> some View {
  50. HStack(alignment: .bottom) {
  51. Spacer()
  52. cobIobView
  53. Spacer()
  54. glucoseView
  55. Spacer()
  56. pumpView
  57. Spacer()
  58. loopView
  59. Spacer()
  60. }
  61. .frame(maxWidth: .infinity)
  62. .frame(maxHeight: 70)
  63. .padding(.top, geo.safeAreaInsets.top)
  64. .background(Color.gray.opacity(0.2))
  65. }
  66. var cobIobView: some View {
  67. VStack(alignment: .leading, spacing: 12) {
  68. HStack {
  69. Text("IOB").font(.caption2).foregroundColor(.secondary)
  70. Text(
  71. (numberFormatter.string(from: (state.suggestion?.iob ?? 0) as NSNumber) ?? "0") +
  72. NSLocalizedString(" U", comment: "Insulin unit")
  73. )
  74. .font(.system(size: 12, weight: .bold))
  75. }
  76. HStack {
  77. Text("COB").font(.caption2).foregroundColor(.secondary)
  78. Text(
  79. (numberFormatter.string(from: (state.suggestion?.cob ?? 0) as NSNumber) ?? "0") +
  80. NSLocalizedString(" g", comment: "gram of carbs")
  81. )
  82. .font(.system(size: 12, weight: .bold))
  83. }
  84. }
  85. }
  86. var glucoseView: some View {
  87. CurrentGlucoseView(
  88. recentGlucose: $state.recentGlucose,
  89. delta: $state.glucoseDelta,
  90. units: $state.units,
  91. alarm: $state.alarm
  92. )
  93. .onTapGesture {
  94. if state.alarm == nil {
  95. state.openCGM()
  96. } else {
  97. state.showModal(for: .snooze)
  98. }
  99. }
  100. .onLongPressGesture {
  101. let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
  102. impactHeavy.impactOccurred()
  103. if state.alarm == nil {
  104. state.showModal(for: .snooze)
  105. } else {
  106. state.openCGM()
  107. }
  108. }
  109. }
  110. var pumpView: some View {
  111. PumpView(
  112. reservoir: $state.reservoir,
  113. battery: $state.battery,
  114. name: $state.pumpName,
  115. expiresAtDate: $state.pumpExpiresAtDate,
  116. timerDate: $state.timerDate
  117. )
  118. .onTapGesture {
  119. if state.pumpDisplayState != nil {
  120. state.setupPump = true
  121. }
  122. }
  123. }
  124. var loopView: some View {
  125. LoopView(
  126. suggestion: $state.suggestion,
  127. enactedSuggestion: $state.enactedSuggestion,
  128. closedLoop: $state.closedLoop,
  129. timerDate: $state.timerDate,
  130. isLooping: $state.isLooping,
  131. lastLoopDate: $state.lastLoopDate,
  132. manualTempBasal: $state.manualTempBasal
  133. ).onTapGesture {
  134. isStatusPopupPresented = true
  135. }.onLongPressGesture {
  136. let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
  137. impactHeavy.impactOccurred()
  138. state.runLoop()
  139. }
  140. }
  141. var infoPanel: some View {
  142. HStack(alignment: .center) {
  143. if state.pumpSuspended {
  144. Text("Pump suspended")
  145. .font(.system(size: 12, weight: .bold)).foregroundColor(.loopGray)
  146. .padding(.leading, 8)
  147. } else if let tempRate = state.tempRate {
  148. if state.apsManager.isManualTempBasal {
  149. Text(
  150. (numberFormatter.string(from: tempRate as NSNumber) ?? "0") +
  151. NSLocalizedString(" U/hr", comment: "Unit per hour with space") +
  152. NSLocalizedString(" - Manual Basal ⚠️", comment: "Manual Temp basal")
  153. )
  154. .font(.system(size: 12, weight: .bold)).foregroundColor(.insulin)
  155. .padding(.leading, 8)
  156. } else {
  157. Text(
  158. (numberFormatter.string(from: tempRate as NSNumber) ?? "0") +
  159. NSLocalizedString(" U/hr", comment: "Unit per hour with space")
  160. )
  161. .font(.system(size: 12, weight: .bold)).foregroundColor(.insulin)
  162. .padding(.leading, 8)
  163. }
  164. }
  165. if let tempTarget = state.tempTarget {
  166. Text(tempTarget.displayName).font(.caption).foregroundColor(.secondary)
  167. if state.units == .mmolL {
  168. Text(
  169. targetFormatter
  170. .string(from: (tempTarget.targetBottom?.asMmolL ?? 0) as NSNumber)!
  171. )
  172. .font(.caption)
  173. .foregroundColor(.secondary)
  174. if tempTarget.targetBottom != tempTarget.targetTop {
  175. Text("-").font(.caption)
  176. .foregroundColor(.secondary)
  177. Text(
  178. targetFormatter
  179. .string(from: (tempTarget.targetTop?.asMmolL ?? 0) as NSNumber)! +
  180. " \(state.units.rawValue)"
  181. )
  182. .font(.caption)
  183. .foregroundColor(.secondary)
  184. } else {
  185. Text(state.units.rawValue).font(.caption)
  186. .foregroundColor(.secondary)
  187. }
  188. } else {
  189. Text(targetFormatter.string(from: (tempTarget.targetBottom ?? 0) as NSNumber)!)
  190. .font(.caption)
  191. .foregroundColor(.secondary)
  192. if tempTarget.targetBottom != tempTarget.targetTop {
  193. Text("-").font(.caption)
  194. .foregroundColor(.secondary)
  195. Text(
  196. targetFormatter
  197. .string(from: (tempTarget.targetTop ?? 0) as NSNumber)! + " \(state.units.rawValue)"
  198. )
  199. .font(.caption)
  200. .foregroundColor(.secondary)
  201. } else {
  202. Text(state.units.rawValue).font(.caption)
  203. .foregroundColor(.secondary)
  204. }
  205. }
  206. }
  207. Spacer()
  208. if let progress = state.bolusProgress {
  209. Text("Bolusing")
  210. .font(.system(size: 12, weight: .bold)).foregroundColor(.insulin)
  211. ProgressView(value: Double(progress))
  212. .progressViewStyle(BolusProgressViewStyle())
  213. .padding(.trailing, 8)
  214. .onTapGesture {
  215. state.cancelBolus()
  216. }
  217. }
  218. }
  219. .frame(maxWidth: .infinity, maxHeight: 30)
  220. }
  221. @ViewBuilder private func statPanel() -> some View {
  222. if state.displayStatistics {
  223. VStack(alignment: .center, spacing: 5) {
  224. HStack {
  225. Group {
  226. durationButton(states: durationState.allCases, selectedState: $selectedState)
  227. Text("Updated").font(.caption2).foregroundColor(.secondary)
  228. Text(dateFormatter.string(from: state.statistics?.created_at ?? Date())).font(.system(size: 12))
  229. }
  230. }
  231. switch selectedState {
  232. case .day:
  233. let hba1c_all = numberFormatter
  234. .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
  235. let average_ = numberFormatter
  236. .string(from: (state.statistics?.Statistics.Glucose.Average.day ?? 0) as NSNumber) ?? ""
  237. let median_ = numberFormatter
  238. .string(from: (state.statistics?.Statistics.Glucose.Median.day ?? 0) as NSNumber) ?? ""
  239. let tir_low = numberFormatter
  240. .string(from: (state.statistics?.Statistics.Distribution.Hypos.day ?? 0) as NSNumber) ?? ""
  241. let tir_high = numberFormatter
  242. .string(from: (state.statistics?.Statistics.Distribution.Hypers.day ?? 0) as NSNumber) ?? ""
  243. let tir_ = numberFormatter
  244. .string(from: (state.statistics?.Statistics.Distribution.TIR.day ?? 0) as NSNumber) ?? ""
  245. let hba1c_ = numberFormatter
  246. .string(from: (state.statistics?.Statistics.HbA1c.day ?? 0) as NSNumber) ?? ""
  247. let sd_ = numberFormatter
  248. .string(from: (state.statistics?.Statistics.Variance.SD.day ?? 0) as NSNumber) ?? ""
  249. let cv_ = numberFormatter
  250. .string(from: (state.statistics?.Statistics.Variance.CV.day ?? 0) as NSNumber) ?? ""
  251. averageTIRhca1c(hba1c_all, average_, median_, tir_low, tir_high, tir_, hba1c_, sd_, cv_)
  252. case .week:
  253. let hba1c_all = numberFormatter
  254. .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
  255. let average_ = numberFormatter
  256. .string(from: (state.statistics?.Statistics.Glucose.Average.week ?? 0) as NSNumber) ?? ""
  257. let median_ = numberFormatter
  258. .string(from: (state.statistics?.Statistics.Glucose.Median.week ?? 0) as NSNumber) ?? ""
  259. let tir_low = numberFormatter
  260. .string(from: (state.statistics?.Statistics.Distribution.Hypos.week ?? 0) as NSNumber) ?? ""
  261. let tir_high = numberFormatter
  262. .string(from: (state.statistics?.Statistics.Distribution.Hypers.week ?? 0) as NSNumber) ?? ""
  263. let tir_ = numberFormatter
  264. .string(from: (state.statistics?.Statistics.Distribution.TIR.week ?? 0) as NSNumber) ?? ""
  265. let hba1c_ = numberFormatter
  266. .string(from: (state.statistics?.Statistics.HbA1c.week ?? 0) as NSNumber) ?? ""
  267. let sd_ = numberFormatter
  268. .string(from: (state.statistics?.Statistics.Variance.SD.week ?? 0) as NSNumber) ?? ""
  269. let cv_ = numberFormatter
  270. .string(from: (state.statistics?.Statistics.Variance.CV.week ?? 0) as NSNumber) ?? ""
  271. averageTIRhca1c(hba1c_all, average_, median_, tir_low, tir_high, tir_, hba1c_, sd_, cv_)
  272. case .month:
  273. let hba1c_all = numberFormatter
  274. .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
  275. let average_ = numberFormatter
  276. .string(from: (state.statistics?.Statistics.Glucose.Average.month ?? 0) as NSNumber) ?? ""
  277. let median_ = numberFormatter
  278. .string(from: (state.statistics?.Statistics.Glucose.Median.month ?? 0) as NSNumber) ?? ""
  279. let tir_low = numberFormatter
  280. .string(from: (state.statistics?.Statistics.Distribution.Hypos.month ?? 0) as NSNumber) ?? ""
  281. let tir_high = numberFormatter
  282. .string(from: (state.statistics?.Statistics.Distribution.Hypers.month ?? 0) as NSNumber) ??
  283. ""
  284. let tir_ = numberFormatter
  285. .string(from: (state.statistics?.Statistics.Distribution.TIR.month ?? 0) as NSNumber) ?? ""
  286. let hba1c_ = numberFormatter
  287. .string(from: (state.statistics?.Statistics.HbA1c.month ?? 0) as NSNumber) ?? ""
  288. let sd_ = numberFormatter
  289. .string(from: (state.statistics?.Statistics.Variance.SD.month ?? 0) as NSNumber) ?? ""
  290. let cv_ = numberFormatter
  291. .string(from: (state.statistics?.Statistics.Variance.CV.month ?? 0) as NSNumber) ?? ""
  292. averageTIRhca1c(hba1c_all, average_, median_, tir_low, tir_high, tir_, hba1c_, sd_, cv_)
  293. case .ninetyDays:
  294. let hba1c_all = numberFormatter
  295. .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
  296. let average_ = numberFormatter
  297. .string(from: (state.statistics?.Statistics.Glucose.Average.ninetyDays ?? 0) as NSNumber) ??
  298. ""
  299. let median_ = numberFormatter
  300. .string(from: (state.statistics?.Statistics.Glucose.Median.ninetyDays ?? 0) as NSNumber) ??
  301. ""
  302. let tir_low = numberFormatter
  303. .string(
  304. from: (state.statistics?.Statistics.Distribution.Hypos.ninetyDays ?? 0) as NSNumber
  305. ) ??
  306. ""
  307. let tir_high = numberFormatter
  308. .string(
  309. from: (state.statistics?.Statistics.Distribution.Hypers.ninetyDays ?? 0) as NSNumber
  310. ) ??
  311. ""
  312. let tir_ = numberFormatter
  313. .string(from: (state.statistics?.Statistics.Distribution.TIR.ninetyDays ?? 0) as NSNumber) ??
  314. ""
  315. let hba1c_ = numberFormatter
  316. .string(from: (state.statistics?.Statistics.HbA1c.ninetyDays ?? 0) as NSNumber) ?? ""
  317. let sd_ = numberFormatter
  318. .string(from: (state.statistics?.Statistics.Variance.SD.ninetyDays ?? 0) as NSNumber) ?? ""
  319. let cv_ = numberFormatter
  320. .string(from: (state.statistics?.Statistics.Variance.CV.ninetyDays ?? 0) as NSNumber) ?? ""
  321. averageTIRhca1c(hba1c_all, average_, median_, tir_low, tir_high, tir_, hba1c_, sd_, cv_)
  322. case .total:
  323. let hba1c_all = numberFormatter
  324. .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
  325. let average_ = numberFormatter
  326. .string(from: (state.statistics?.Statistics.Glucose.Average.total ?? 0) as NSNumber) ?? ""
  327. let median_ = numberFormatter
  328. .string(from: (state.statistics?.Statistics.Glucose.Median.total ?? 0) as NSNumber) ?? ""
  329. let tir_low = numberFormatter
  330. .string(from: (state.statistics?.Statistics.Distribution.Hypos.total ?? 0) as NSNumber) ?? ""
  331. let tir_high = numberFormatter
  332. .string(from: (state.statistics?.Statistics.Distribution.Hypers.total ?? 0) as NSNumber) ??
  333. ""
  334. let tir_ = numberFormatter
  335. .string(from: (state.statistics?.Statistics.Distribution.TIR.total ?? 0) as NSNumber) ?? ""
  336. let hba1c_ = numberFormatter
  337. .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
  338. let sd_ = numberFormatter
  339. .string(from: (state.statistics?.Statistics.Variance.SD.total ?? 0) as NSNumber) ?? ""
  340. let cv_ = numberFormatter
  341. .string(from: (state.statistics?.Statistics.Variance.CV.total ?? 0) as NSNumber) ?? ""
  342. averageTIRhca1c(hba1c_all, average_, median_, tir_low, tir_high, tir_, hba1c_, sd_, cv_)
  343. }
  344. }
  345. .frame(maxWidth: .infinity, maxHeight: 120, alignment: .center)
  346. }
  347. }
  348. @ViewBuilder private func averageTIRhca1c(
  349. _ hba1c_all: String,
  350. _ average_: String,
  351. _ median_: String,
  352. _ tir_low: String,
  353. _ tir_high: String,
  354. _ tir_: String,
  355. _ hba1c_: String,
  356. _ sd_: String,
  357. _ cv_: String
  358. ) -> some View {
  359. HStack {
  360. Group {
  361. Text(NSLocalizedString("Average", comment: "")).font(.caption2).foregroundColor(.secondary)
  362. Text(average_).font(.system(size: 12))
  363. Text("Median")
  364. .font(.caption2).foregroundColor(.secondary)
  365. Text(median_).font(.system(size: 12))
  366. }
  367. }
  368. HStack {
  369. Group {
  370. Text(
  371. NSLocalizedString("Low (<", comment: " ") +
  372. (
  373. targetFormatter
  374. .string(from: state.settingsManager.preferences.low as NSNumber) ?? ""
  375. ) + ")"
  376. ).font(.caption2)
  377. .foregroundColor(.secondary)
  378. Text(tir_low + " %").font(.system(size: 12)).foregroundColor(.loopRed)
  379. Text("Normal").font(.caption2).foregroundColor(.secondary)
  380. Text(tir_ + " %").font(.system(size: 12)).foregroundColor(.loopGreen)
  381. Text(
  382. NSLocalizedString("High (>", comment: " ") +
  383. (
  384. targetFormatter
  385. .string(from: state.settingsManager.preferences.high as NSNumber) ?? ""
  386. ) + ")"
  387. )
  388. .font(.caption2).foregroundColor(.secondary)
  389. Text(tir_high + " %").font(.system(size: 12)).foregroundColor(.loopYellow)
  390. }
  391. }
  392. HStack {
  393. Group {
  394. Text("HbA1c").font(.caption2).foregroundColor(.secondary)
  395. Text(hba1c_).font(.system(size: 12))
  396. if !state.settingsManager.preferences.displaySD {
  397. Text(
  398. NSLocalizedString("CV", comment: "CV")
  399. ).font(.caption2).foregroundColor(.secondary)
  400. Text(cv_).font(.system(size: 12))
  401. } else {
  402. Text(
  403. NSLocalizedString("SD", comment: "SD")
  404. ).font(.caption2).foregroundColor(.secondary)
  405. Text(sd_).font(.system(size: 12))
  406. }
  407. Text(
  408. NSLocalizedString("All ", comment: "") +
  409. // getString(state.statistics?.GlucoseStorage_Days, false) +
  410. NSLocalizedString(" days", comment: "")
  411. ).font(.caption2).foregroundColor(.secondary)
  412. Text(hba1c_all).font(.system(size: 12))
  413. }
  414. }
  415. if state.settingsManager.preferences.displayLoops {
  416. HStack {
  417. Group {
  418. Text("Loops").font(.caption2).foregroundColor(.secondary)
  419. Text(
  420. tirFormatter
  421. .string(from: (state.statistics?.Statistics.LoopCycles.loops ?? 0) as NSNumber) ?? ""
  422. ).font(.system(size: 12))
  423. Text("Average Interval").font(.caption2)
  424. .foregroundColor(.secondary)
  425. Text(
  426. targetFormatter
  427. .string(from: (state.statistics?.Statistics.LoopCycles.avg_interval ?? 0) as NSNumber) ??
  428. ""
  429. ).font(.system(size: 12))
  430. Text("Median Duration").font(.caption2)
  431. .foregroundColor(.secondary)
  432. Text(
  433. numberFormatter
  434. .string(
  435. from: (state.statistics?.Statistics.LoopCycles.median_duration ?? 0) as NSNumber
  436. ) ?? ""
  437. ).font(.system(size: 12))
  438. }
  439. }
  440. }
  441. }
  442. var legendPanel: some View {
  443. ZStack {
  444. HStack(alignment: .center) {
  445. Group {
  446. Circle().fill(Color.loopGreen).frame(width: 8, height: 8)
  447. Text("BG")
  448. .font(.system(size: 12, weight: .bold)).foregroundColor(.loopGreen)
  449. }
  450. Group {
  451. Circle().fill(Color.insulin).frame(width: 8, height: 8)
  452. .padding(.leading, 8)
  453. Text("IOB")
  454. .font(.system(size: 12, weight: .bold)).foregroundColor(.insulin)
  455. }
  456. Group {
  457. Circle().fill(Color.zt).frame(width: 8, height: 8)
  458. .padding(.leading, 8)
  459. Text("ZT")
  460. .font(.system(size: 12, weight: .bold)).foregroundColor(.zt)
  461. }
  462. Group {
  463. Circle().fill(Color.loopYellow).frame(width: 8, height: 8)
  464. .padding(.leading, 8)
  465. Text("COB")
  466. .font(.system(size: 12, weight: .bold)).foregroundColor(.loopYellow)
  467. }
  468. Group {
  469. Circle().fill(Color.uam).frame(width: 8, height: 8)
  470. .padding(.leading, 8)
  471. Text("UAM")
  472. .font(.system(size: 12, weight: .bold)).foregroundColor(.uam)
  473. }
  474. if let eventualBG = state.eventualBG {
  475. Text(
  476. "⇢ " + numberFormatter.string(
  477. from: (state.units == .mmolL ? eventualBG.asMmolL : Decimal(eventualBG)) as NSNumber
  478. )!
  479. )
  480. .font(.system(size: 12, weight: .bold)).foregroundColor(.secondary)
  481. }
  482. }
  483. .frame(maxWidth: .infinity)
  484. }
  485. }
  486. var mainChart: some View {
  487. ZStack {
  488. if state.animatedBackground {
  489. SpriteView(scene: spriteScene, options: [.allowsTransparency])
  490. .ignoresSafeArea()
  491. .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
  492. }
  493. MainChartView(
  494. glucose: $state.glucose,
  495. suggestion: $state.suggestion,
  496. statistcs: $state.statistics,
  497. tempBasals: $state.tempBasals,
  498. boluses: $state.boluses,
  499. suspensions: $state.suspensions,
  500. hours: .constant(state.filteredHours),
  501. maxBasal: $state.maxBasal,
  502. autotunedBasalProfile: $state.autotunedBasalProfile,
  503. basalProfile: $state.basalProfile,
  504. tempTargets: $state.tempTargets,
  505. carbs: $state.carbs,
  506. timerDate: $state.timerDate,
  507. units: $state.units
  508. )
  509. }
  510. // .padding(.bottom)
  511. .modal(for: .dataTable, from: self)
  512. }
  513. @ViewBuilder private func bottomPanel(_ geo: GeometryProxy) -> some View {
  514. ZStack {
  515. Rectangle().fill(Color.gray.opacity(0.2)).frame(height: 50 + geo.safeAreaInsets.bottom)
  516. HStack {
  517. Button { state.showModal(for: .addCarbs) }
  518. label: {
  519. ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
  520. Image("carbs")
  521. .renderingMode(.template)
  522. .resizable()
  523. .frame(width: 24, height: 24)
  524. .foregroundColor(.loopGreen)
  525. .padding(8)
  526. if let carbsReq = state.carbsRequired {
  527. Text(numberFormatter.string(from: carbsReq as NSNumber)!)
  528. .font(.caption)
  529. .foregroundColor(.white)
  530. .padding(4)
  531. .background(Capsule().fill(Color.red))
  532. }
  533. }
  534. }
  535. Spacer()
  536. Button { state.showModal(for: .addTempTarget) }
  537. label: {
  538. Image("target")
  539. .renderingMode(.template)
  540. .resizable()
  541. .frame(width: 24, height: 24)
  542. .padding(8)
  543. }.foregroundColor(.loopYellow)
  544. Spacer()
  545. Button { state.showModal(for: .bolus(waitForSuggestion: false)) }
  546. label: {
  547. Image("bolus")
  548. .renderingMode(.template)
  549. .resizable()
  550. .frame(width: 24, height: 24)
  551. .padding(8)
  552. }.foregroundColor(.insulin)
  553. Spacer()
  554. if state.allowManualTemp {
  555. Button { state.showModal(for: .manualTempBasal) }
  556. label: {
  557. Image("bolus1")
  558. .renderingMode(.template)
  559. .resizable()
  560. .frame(width: 24, height: 24)
  561. .padding(8)
  562. }.foregroundColor(.insulin)
  563. Spacer()
  564. }
  565. Button { state.showModal(for: .settings) }
  566. label: {
  567. Image("settings1")
  568. .renderingMode(.template)
  569. .resizable()
  570. .frame(width: 24, height: 24)
  571. .padding(8)
  572. }.foregroundColor(.loopGray)
  573. }
  574. .padding(.horizontal, 24)
  575. .padding(.bottom, geo.safeAreaInsets.bottom)
  576. }
  577. }
  578. var body: some View {
  579. GeometryReader { geo in
  580. VStack(spacing: 0) {
  581. header(geo)
  582. infoPanel
  583. mainChart
  584. legendPanel
  585. statPanel()
  586. bottomPanel(geo)
  587. }
  588. .edgesIgnoringSafeArea(.vertical)
  589. }
  590. .onAppear(perform: configureView)
  591. .navigationTitle("Home")
  592. .navigationBarHidden(true)
  593. .ignoresSafeArea(.keyboard)
  594. .popup(isPresented: isStatusPopupPresented, alignment: .top, direction: .top) {
  595. popup
  596. .padding()
  597. .background(
  598. RoundedRectangle(cornerRadius: 8, style: .continuous)
  599. .fill(Color(UIColor.darkGray))
  600. )
  601. .onTapGesture {
  602. isStatusPopupPresented = false
  603. }
  604. .gesture(
  605. DragGesture(minimumDistance: 10, coordinateSpace: .local)
  606. .onEnded { value in
  607. if value.translation.height < 0 {
  608. isStatusPopupPresented = false
  609. }
  610. }
  611. )
  612. }
  613. }
  614. private var popup: some View {
  615. VStack(alignment: .leading, spacing: 4) {
  616. Text(state.statusTitle).font(.headline).foregroundColor(.white)
  617. .padding(.bottom, 4)
  618. if let suggestion = state.suggestion {
  619. TagCloudView(tags: suggestion.reasonParts).animation(.none, value: false)
  620. Text(suggestion.reasonConclusion.capitalizingFirstLetter()).font(.caption).foregroundColor(.white)
  621. } else {
  622. Text("No sugestion found").font(.body).foregroundColor(.white)
  623. }
  624. if let errorMessage = state.errorMessage, let date = state.errorDate {
  625. Text("Error at \(dateFormatter.string(from: date))")
  626. .foregroundColor(.white)
  627. .font(.headline)
  628. .padding(.bottom, 4)
  629. .padding(.top, 8)
  630. Text(errorMessage).font(.caption).foregroundColor(.loopRed)
  631. }
  632. }
  633. }
  634. private func colorOfGlucose(_ glucose: Decimal) -> Color {
  635. switch glucose {
  636. case 4 ... 8,
  637. 30 ... 46,
  638. 72 ... 144:
  639. return .loopGreen
  640. case 0 ... 4,
  641. 20 ... 71:
  642. return .loopRed
  643. default:
  644. return .loopYellow
  645. }
  646. }
  647. private func getString(_ stat: Decimal?, _ test: Bool) -> String {
  648. var string: String = targetFormatter.string(from: (stat ?? 0) as NSNumber) ?? ""
  649. if state.units != .mmolL, test == true {
  650. string = tirFormatter.string(from: (stat ?? 0) as NSNumber) ?? ""
  651. }
  652. return string
  653. }
  654. private func updateStats() {
  655. hba1c_all = getString(state.statistics?.Statistics.HbA1c.total, false)
  656. switch selectedState {
  657. case .day:
  658. average_ = getString(state.statistics?.Statistics.Glucose.Average.day, true)
  659. median_ = getString(state.statistics?.Statistics.Glucose.Median.day, true)
  660. tir_low = getString(state.statistics?.Statistics.Distribution.Hypos.day, false)
  661. tir_high = getString(state.statistics?.Statistics.Distribution.Hypers.day, false)
  662. tir_ = getString(state.statistics?.Statistics.Distribution.TIR.day, false)
  663. hba1c_ = getString(state.statistics?.Statistics.HbA1c.day, false)
  664. sd_ = getString(state.statistics?.Statistics.Variance.SD.day, true)
  665. cv_ = getString(state.statistics?.Statistics.Variance.CV.day, false)
  666. case .week:
  667. average_ = getString(state.statistics?.Statistics.Glucose.Average.week, true)
  668. median_ = getString(state.statistics?.Statistics.Glucose.Median.week, true)
  669. tir_low = getString(state.statistics?.Statistics.Distribution.Hypos.week, false)
  670. tir_high = getString(state.statistics?.Statistics.Distribution.Hypers.week, false)
  671. tir_ = getString(state.statistics?.Statistics.Distribution.TIR.week, false)
  672. hba1c_ = getString(state.statistics?.Statistics.HbA1c.week, false)
  673. sd_ = getString(state.statistics?.Statistics.Variance.SD.week, true)
  674. cv_ = getString(state.statistics?.Statistics.Variance.CV.week, false)
  675. case .month:
  676. average_ = getString(state.statistics?.Statistics.Glucose.Average.month, true)
  677. median_ = getString(state.statistics?.Statistics.Glucose.Median.month, true)
  678. tir_low = getString(state.statistics?.Statistics.Distribution.Hypos.month, false)
  679. tir_high = getString(state.statistics?.Statistics.Distribution.Hypers.month, false)
  680. tir_ = getString(state.statistics?.Statistics.Distribution.TIR.month, false)
  681. hba1c_ = getString(state.statistics?.Statistics.HbA1c.month, false)
  682. sd_ = getString(state.statistics?.Statistics.Variance.SD.month, true)
  683. cv_ = getString(state.statistics?.Statistics.Variance.CV.month, false)
  684. case .ninetyDays:
  685. average_ = getString(state.statistics?.Statistics.Glucose.Average.ninetyDays, true)
  686. median_ = getString(state.statistics?.Statistics.Glucose.Median.ninetyDays, true)
  687. tir_low = getString(state.statistics?.Statistics.Distribution.Hypos.ninetyDays, false)
  688. tir_high = getString(state.statistics?.Statistics.Distribution.Hypers.ninetyDays, false)
  689. tir_ = getString(state.statistics?.Statistics.Distribution.TIR.ninetyDays, false)
  690. hba1c_ = getString(state.statistics?.Statistics.HbA1c.ninetyDays, false)
  691. sd_ = getString(state.statistics?.Statistics.Variance.SD.ninetyDays, true)
  692. cv_ = getString(state.statistics?.Statistics.Variance.CV.ninetyDays, false)
  693. case .total:
  694. average_ = getString(state.statistics?.Statistics.Glucose.Average.total, true)
  695. median_ = getString(state.statistics?.Statistics.Glucose.Median.total, true)
  696. tir_low = getString(state.statistics?.Statistics.Distribution.Hypos.total, false)
  697. tir_high = getString(state.statistics?.Statistics.Distribution.Hypers.total, false)
  698. tir_ = getString(state.statistics?.Statistics.Distribution.TIR.total, false)
  699. hba1c_ = getString(state.statistics?.Statistics.HbA1c.total, false)
  700. sd_ = getString(state.statistics?.Statistics.Variance.SD.total, true)
  701. cv_ = getString(state.statistics?.Statistics.Variance.CV.total, false)
  702. }
  703. }
  704. }
  705. }