HomeRootView.swift 31 KB

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