HomeRootView.swift 33 KB

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