HomeRootView.swift 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  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. if averageOrMedianTitle == textAverageTitle {
  376. Text(averageOrmedian == "" ? average_ : average_).font(.footnote)
  377. } else {
  378. Text(averageOrmedian == "" ? median_ : median_).font(.footnote)
  379. }
  380. }.onTapGesture {
  381. if averageOrMedianTitle == textAverageTitle {
  382. averageOrMedianTitle = textMedianTitle
  383. averageOrmedian = median_
  384. } else {
  385. averageOrMedianTitle = textAverageTitle
  386. averageOrmedian = average_
  387. }
  388. }
  389. .frame(minWidth: 110)
  390. // CV as default. Changes to SD when clicking
  391. let text_CV_Title = NSLocalizedString("CV", comment: "")
  392. let text_SD_Title = NSLocalizedString("SD", comment: "")
  393. HStack {
  394. Text(CV_or_SD_Title).font(.footnote).foregroundColor(.secondary)
  395. if CV_or_SD_Title == text_CV_Title {
  396. Text(CVorSD == "" ? cv_ : cv_).font(.footnote)
  397. } else {
  398. Text(CVorSD == "" ? sd_ : sd_).font(.footnote)
  399. }
  400. }.onTapGesture {
  401. if CV_or_SD_Title == text_CV_Title {
  402. CV_or_SD_Title = text_SD_Title
  403. CVorSD = sd_
  404. } else {
  405. CV_or_SD_Title = text_CV_Title
  406. CVorSD = cv_
  407. }
  408. }
  409. }
  410. }
  411. HStack {
  412. Group {
  413. HStack {
  414. Text(
  415. NSLocalizedString("Low", comment: " ")
  416. )
  417. .font(.footnote)
  418. .foregroundColor(.secondary)
  419. Text(tir_low + " %").font(.footnote).foregroundColor(.loopRed)
  420. }
  421. HStack {
  422. Text("Normal").font(.footnote).foregroundColor(.secondary)
  423. Text(tir_ + " %").font(.footnote).foregroundColor(.loopGreen)
  424. }
  425. HStack {
  426. Text(
  427. NSLocalizedString("High", comment: " ")
  428. )
  429. .font(.footnote).foregroundColor(.secondary)
  430. Text(tir_high + " %").font(.footnote).foregroundColor(.loopYellow)
  431. }
  432. }
  433. }
  434. if state.settingsManager.preferences.displayLoops {
  435. HStack {
  436. Group {
  437. HStack {
  438. Text("Loops").font(.footnote).foregroundColor(.secondary)
  439. Text(
  440. tirFormatter
  441. .string(from: (state.statistics?.Statistics.LoopCycles.loops ?? 0) as NSNumber) ?? ""
  442. ).font(.footnote)
  443. }
  444. HStack {
  445. Text("Interval").font(.footnote)
  446. .foregroundColor(.secondary)
  447. Text(
  448. targetFormatter
  449. .string(from: (state.statistics?.Statistics.LoopCycles.avg_interval ?? 0) as NSNumber) ??
  450. ""
  451. ).font(.footnote)
  452. }
  453. HStack {
  454. Text("Duration").font(.footnote)
  455. .foregroundColor(.secondary)
  456. Text(
  457. numberFormatter
  458. .string(
  459. from: (state.statistics?.Statistics.LoopCycles.median_duration ?? 0) as NSNumber
  460. ) ?? ""
  461. ).font(.footnote)
  462. }
  463. }
  464. }
  465. }
  466. }
  467. var legendPanel: some View {
  468. ZStack {
  469. HStack(alignment: .center) {
  470. Group {
  471. Circle().fill(Color.loopGreen).frame(width: 8, height: 8)
  472. Text("BG")
  473. .font(.system(size: 12, weight: .bold)).foregroundColor(.loopGreen)
  474. }
  475. Group {
  476. Circle().fill(Color.insulin).frame(width: 8, height: 8)
  477. .padding(.leading, 8)
  478. Text("IOB")
  479. .font(.system(size: 12, weight: .bold)).foregroundColor(.insulin)
  480. }
  481. Group {
  482. Circle().fill(Color.zt).frame(width: 8, height: 8)
  483. .padding(.leading, 8)
  484. Text("ZT")
  485. .font(.system(size: 12, weight: .bold)).foregroundColor(.zt)
  486. }
  487. Group {
  488. Circle().fill(Color.loopYellow).frame(width: 8, height: 8)
  489. .padding(.leading, 8)
  490. Text("COB")
  491. .font(.system(size: 12, weight: .bold)).foregroundColor(.loopYellow)
  492. }
  493. Group {
  494. Circle().fill(Color.uam).frame(width: 8, height: 8)
  495. .padding(.leading, 8)
  496. Text("UAM")
  497. .font(.system(size: 12, weight: .bold)).foregroundColor(.uam)
  498. }
  499. if let eventualBG = state.eventualBG {
  500. Text(
  501. "⇢ " + numberFormatter.string(
  502. from: (state.units == .mmolL ? eventualBG.asMmolL : Decimal(eventualBG)) as NSNumber
  503. )!
  504. )
  505. .font(.system(size: 12, weight: .bold)).foregroundColor(.secondary)
  506. }
  507. }
  508. .frame(maxWidth: .infinity)
  509. .padding([.bottom], 20)
  510. }
  511. }
  512. var mainChart: some View {
  513. ZStack {
  514. if state.animatedBackground {
  515. SpriteView(scene: spriteScene, options: [.allowsTransparency])
  516. .ignoresSafeArea()
  517. .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
  518. }
  519. MainChartView(
  520. glucose: $state.glucose,
  521. suggestion: $state.suggestion,
  522. statistcs: $state.statistics,
  523. tempBasals: $state.tempBasals,
  524. boluses: $state.boluses,
  525. suspensions: $state.suspensions,
  526. hours: .constant(state.filteredHours),
  527. maxBasal: $state.maxBasal,
  528. autotunedBasalProfile: $state.autotunedBasalProfile,
  529. basalProfile: $state.basalProfile,
  530. tempTargets: $state.tempTargets,
  531. carbs: $state.carbs,
  532. timerDate: $state.timerDate,
  533. units: $state.units
  534. )
  535. }
  536. .padding(.bottom)
  537. .modal(for: .dataTable, from: self)
  538. }
  539. @ViewBuilder private func bottomPanel(_ geo: GeometryProxy) -> some View {
  540. ZStack {
  541. Rectangle().fill(Color.gray.opacity(0.2)).frame(height: 50 + geo.safeAreaInsets.bottom)
  542. HStack {
  543. Button { state.showModal(for: .addCarbs) }
  544. label: {
  545. ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
  546. Image("carbs")
  547. .renderingMode(.template)
  548. .resizable()
  549. .frame(width: 24, height: 24)
  550. .foregroundColor(.loopGreen)
  551. .padding(8)
  552. if let carbsReq = state.carbsRequired {
  553. Text(numberFormatter.string(from: carbsReq as NSNumber)!)
  554. .font(.caption)
  555. .foregroundColor(.white)
  556. .padding(4)
  557. .background(Capsule().fill(Color.red))
  558. }
  559. }
  560. }
  561. Spacer()
  562. Button { state.showModal(for: .addTempTarget) }
  563. label: {
  564. Image("target")
  565. .renderingMode(.template)
  566. .resizable()
  567. .frame(width: 24, height: 24)
  568. .padding(8)
  569. }.foregroundColor(.loopYellow)
  570. Spacer()
  571. Button { state.showModal(for: .bolus(waitForSuggestion: false)) }
  572. label: {
  573. Image("bolus")
  574. .renderingMode(.template)
  575. .resizable()
  576. .frame(width: 24, height: 24)
  577. .padding(8)
  578. }.foregroundColor(.insulin)
  579. Spacer()
  580. if state.allowManualTemp {
  581. Button { state.showModal(for: .manualTempBasal) }
  582. label: {
  583. Image("bolus1")
  584. .renderingMode(.template)
  585. .resizable()
  586. .frame(width: 24, height: 24)
  587. .padding(8)
  588. }.foregroundColor(.insulin)
  589. Spacer()
  590. }
  591. Button { state.showModal(for: .settings) }
  592. label: {
  593. Image("settings1")
  594. .renderingMode(.template)
  595. .resizable()
  596. .frame(width: 24, height: 24)
  597. .padding(8)
  598. }.foregroundColor(.loopGray)
  599. }
  600. .padding(.horizontal, 24)
  601. .padding(.bottom, geo.safeAreaInsets.bottom)
  602. }
  603. }
  604. var body: some View {
  605. GeometryReader { geo in
  606. VStack(spacing: 0) {
  607. header(geo)
  608. infoPanel
  609. mainChart
  610. legendPanel
  611. statPanel()
  612. bottomPanel(geo)
  613. }
  614. .edgesIgnoringSafeArea(.vertical)
  615. }
  616. .onAppear(perform: configureView)
  617. .navigationTitle("Home")
  618. .navigationBarHidden(true)
  619. .ignoresSafeArea(.keyboard)
  620. .popup(isPresented: isStatusPopupPresented, alignment: .top, direction: .top) {
  621. popup
  622. .padding()
  623. .background(
  624. RoundedRectangle(cornerRadius: 8, style: .continuous)
  625. .fill(Color(UIColor.darkGray))
  626. )
  627. .onTapGesture {
  628. isStatusPopupPresented = false
  629. }
  630. .gesture(
  631. DragGesture(minimumDistance: 10, coordinateSpace: .local)
  632. .onEnded { value in
  633. if value.translation.height < 0 {
  634. isStatusPopupPresented = false
  635. }
  636. }
  637. )
  638. }
  639. }
  640. private var popup: some View {
  641. VStack(alignment: .leading, spacing: 4) {
  642. Text(state.statusTitle).font(.headline).foregroundColor(.white)
  643. .padding(.bottom, 4)
  644. if let suggestion = state.suggestion {
  645. TagCloudView(tags: suggestion.reasonParts).animation(.none, value: false)
  646. Text(suggestion.reasonConclusion.capitalizingFirstLetter()).font(.caption).foregroundColor(.white)
  647. } else {
  648. Text("No sugestion found").font(.body).foregroundColor(.white)
  649. }
  650. if let errorMessage = state.errorMessage, let date = state.errorDate {
  651. Text(NSLocalizedString("Error at", comment: "") + " " + dateFormatter.string(from: date))
  652. .foregroundColor(.white)
  653. .font(.headline)
  654. .padding(.bottom, 4)
  655. .padding(.top, 8)
  656. Text(errorMessage).font(.caption).foregroundColor(.loopRed)
  657. }
  658. }
  659. }
  660. private func colorOfGlucose(_ glucose: Decimal) -> Color {
  661. switch glucose {
  662. case 4 ... 8,
  663. 30 ... 46,
  664. 72 ... 144:
  665. return .loopGreen
  666. case 0 ... 4,
  667. 20 ... 71:
  668. return .loopRed
  669. default:
  670. return .loopYellow
  671. }
  672. }
  673. }
  674. }