CarbRatioEditorRootView.swift 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import Charts
  2. import SwiftUI
  3. import Swinject
  4. extension CarbRatioEditor {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. @StateObject var state = StateModel()
  8. @State private var refreshUI = UUID()
  9. @State private var now = Date()
  10. @Namespace private var bottomID
  11. @Environment(\.colorScheme) var colorScheme
  12. @Environment(AppState.self) var appState
  13. private var formatter: NumberFormatter {
  14. let formatter = NumberFormatter()
  15. formatter.numberStyle = .decimal
  16. formatter.maximumFractionDigits = 1
  17. return formatter
  18. }
  19. var saveButton: some View {
  20. ZStack {
  21. let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
  22. Rectangle()
  23. .frame(width: UIScreen.main.bounds.width, height: 65)
  24. .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
  25. .background(.thinMaterial)
  26. .opacity(0.8)
  27. .clipShape(Rectangle())
  28. Group {
  29. HStack {
  30. HStack {
  31. Button {
  32. let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
  33. impactHeavy.impactOccurred()
  34. state.save()
  35. // deactivate saving display after 1.25 seconds
  36. DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
  37. state.shouldDisplaySaving = false
  38. }
  39. } label: {
  40. HStack {
  41. if state.shouldDisplaySaving {
  42. ProgressView().padding(.trailing, 10)
  43. }
  44. Text(state.shouldDisplaySaving ? "Saving..." : "Save")
  45. }
  46. .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
  47. .padding(10)
  48. }
  49. }
  50. .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
  51. .disabled(shouldDisableButton)
  52. .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
  53. .tint(.white)
  54. .clipShape(RoundedRectangle(cornerRadius: 8))
  55. }
  56. }.padding(5)
  57. }
  58. }
  59. var body: some View {
  60. ScrollViewReader { proxy in
  61. VStack(spacing: 0) {
  62. ScrollView {
  63. LazyVStack {
  64. VStack(alignment: .leading, spacing: 0) {
  65. // Chart visualization
  66. if !state.items.isEmpty {
  67. VStack(alignment: .leading) {
  68. carbRatioChart
  69. .frame(height: 180)
  70. .padding(.horizontal)
  71. }
  72. .padding(.vertical)
  73. .background(Color.chart.opacity(0.65))
  74. .clipShape(
  75. .rect(
  76. topLeadingRadius: 10,
  77. bottomLeadingRadius: 0,
  78. bottomTrailingRadius: 0,
  79. topTrailingRadius: 10
  80. )
  81. )
  82. .padding(.horizontal)
  83. .padding(.top)
  84. }
  85. // Carb ratio list
  86. TherapySettingEditorView(
  87. items: $state.therapyItems,
  88. unit: .gramPerUnit,
  89. timeOptions: state.timeValues,
  90. valueOptions: state.rateValues,
  91. validateOnDelete: state.validate,
  92. onItemAdded: {
  93. withAnimation {
  94. proxy.scrollTo(bottomID, anchor: .bottom)
  95. }
  96. }
  97. )
  98. .padding(.horizontal)
  99. .id(bottomID)
  100. }
  101. }
  102. }
  103. saveButton
  104. }
  105. .background(appState.trioBackgroundColor(for: colorScheme))
  106. .onAppear(perform: configureView)
  107. .navigationTitle("Carb Ratios")
  108. .navigationBarTitleDisplayMode(.automatic)
  109. .onAppear {
  110. state.validate()
  111. state.therapyItems = state.getTherapyItems()
  112. }
  113. .onChange(of: state.therapyItems) { _, newItems in
  114. state.updateFromTherapyItems(newItems)
  115. refreshUI = UUID()
  116. }
  117. }
  118. }
  119. // Chart for visualizing carb ratios
  120. private var carbRatioChart: some View {
  121. Chart {
  122. ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
  123. let displayValue = state.rateValues[item.rateIndex]
  124. let startDate = Calendar.current
  125. .startOfDay(for: now)
  126. .addingTimeInterval(state.timeValues[item.timeIndex])
  127. var offset: TimeInterval {
  128. if state.items.count > index + 1 {
  129. return state.timeValues[state.items[index + 1].timeIndex]
  130. } else {
  131. return state.timeValues.last! + 30 * 60
  132. }
  133. }
  134. let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
  135. RectangleMark(
  136. xStart: .value("start", startDate),
  137. xEnd: .value("end", endDate),
  138. yStart: .value("rate-start", displayValue),
  139. yEnd: .value("rate-end", 0)
  140. ).foregroundStyle(
  141. .linearGradient(
  142. colors: [
  143. Color.orange.opacity(0.6),
  144. Color.orange.opacity(0.1)
  145. ],
  146. startPoint: .bottom,
  147. endPoint: .top
  148. )
  149. ).alignsMarkStylesWithPlotArea()
  150. LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
  151. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
  152. LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
  153. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
  154. }
  155. }
  156. .id(refreshUI) // Force chart update
  157. .chartXAxis {
  158. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  159. AxisValueLabel(format: .dateTime.hour())
  160. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  161. }
  162. }
  163. .chartXScale(
  164. domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
  165. .addingTimeInterval(60 * 60 * 24)
  166. )
  167. .chartYAxis {
  168. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  169. AxisValueLabel()
  170. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  171. }
  172. }
  173. }
  174. }
  175. }