AddCarbsRootView.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import CoreData
  2. import SwiftUI
  3. import Swinject
  4. extension AddCarbs {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. let editMode: Bool
  8. @StateObject var state = StateModel()
  9. @State var dish: String = ""
  10. @State var isPromptPresented = false
  11. @State var saved = false
  12. @State var pushed = false
  13. @State private var showAlert = false
  14. @FocusState private var isFocused: Bool
  15. @FetchRequest(
  16. entity: Presets.entity(),
  17. sortDescriptors: [NSSortDescriptor(key: "dish", ascending: true)]
  18. ) var carbPresets: FetchedResults<Presets>
  19. @Environment(\.managedObjectContext) var moc
  20. private var formatter: NumberFormatter {
  21. let formatter = NumberFormatter()
  22. formatter.numberStyle = .decimal
  23. formatter.maximumFractionDigits = 1
  24. return formatter
  25. }
  26. var body: some View {
  27. Form {
  28. if let carbsReq = state.carbsRequired, state.carbs < carbsReq {
  29. Section {
  30. HStack {
  31. Text("Carbs required")
  32. Spacer()
  33. Text((formatter.string(from: carbsReq as NSNumber) ?? "") + " g")
  34. }
  35. }
  36. }
  37. Section {
  38. HStack {
  39. Text("Carbs").fontWeight(.semibold)
  40. Spacer()
  41. DecimalTextField(
  42. "0",
  43. value: $state.carbs,
  44. formatter: formatter,
  45. autofocus: true,
  46. cleanInput: true
  47. )
  48. Text("grams").foregroundColor(.secondary)
  49. }
  50. if state.useFPUconversion {
  51. proteinAndFat()
  52. }
  53. // Summary when combining presets
  54. if state.waitersNotepad() != "" {
  55. HStack {
  56. Text("Total")
  57. let test = state.waitersNotepad().components(separatedBy: ", ").removeDublicates()
  58. HStack(spacing: 0) {
  59. ForEach(test, id: \.self) {
  60. Text($0).foregroundStyle(Color.randomGreen()).font(.footnote)
  61. Text($0 == test[test.count - 1] ? "" : ", ")
  62. }
  63. }.frame(maxWidth: .infinity, alignment: .trailing)
  64. }
  65. }
  66. // Time
  67. HStack {
  68. let now = Date.now
  69. Text("Time")
  70. Spacer()
  71. if !pushed {
  72. Button {
  73. pushed = true
  74. } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary).padding(.trailing, 5)
  75. } else {
  76. Button { state.date = state.date.addingTimeInterval(-10.minutes.timeInterval) }
  77. label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
  78. DatePicker(
  79. "Time",
  80. selection: $state.date,
  81. in: ...now,
  82. displayedComponents: [.hourAndMinute]
  83. ).controlSize(.mini)
  84. .labelsHidden()
  85. Button {
  86. if state.date.addingTimeInterval(5.minutes.timeInterval) < now {
  87. state.date = state.date.addingTimeInterval(10.minutes.timeInterval)
  88. }
  89. }
  90. label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
  91. }
  92. }
  93. // Optional meal note
  94. HStack {
  95. Text("Note").foregroundColor(.secondary)
  96. TextField("", text: $state.note).multilineTextAlignment(.trailing)
  97. if state.note != "", isFocused {
  98. Button { isFocused = false } label: { Image(systemName: "keyboard.chevron.compact.down") }
  99. .controlSize(.mini)
  100. }
  101. }
  102. .focused($isFocused)
  103. .popover(isPresented: $isPromptPresented) {
  104. presetPopover
  105. }
  106. }
  107. Section {
  108. Button { state.add() }
  109. label: { Text(state.skipBolus ? "Save" : "Continue") }
  110. .disabled(state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
  111. .frame(maxWidth: .infinity, alignment: .center)
  112. }.listRowBackground(!empty ? Color(.systemBlue) : Color(.systemGray4))
  113. .tint(.white)
  114. Section {
  115. mealPresets
  116. }
  117. }
  118. .onAppear {
  119. configureView {
  120. state.loadEntries(editMode)
  121. }
  122. }
  123. .navigationTitle("Add Meal")
  124. .navigationBarTitleDisplayMode(.inline)
  125. .navigationBarItems(leading: Button("Close", action: state.hideModal))
  126. }
  127. private var presetPopover: some View {
  128. Form {
  129. Section {
  130. TextField("Name Of Dish", text: $dish)
  131. Button {
  132. saved = true
  133. if dish != "", saved {
  134. let preset = Presets(context: moc)
  135. preset.dish = dish
  136. preset.fat = state.fat as NSDecimalNumber
  137. preset.protein = state.protein as NSDecimalNumber
  138. preset.carbs = state.carbs as NSDecimalNumber
  139. try? moc.save()
  140. state.addNewPresetToWaitersNotepad(dish)
  141. saved = false
  142. isPromptPresented = false
  143. }
  144. }
  145. label: { Text("Save") }
  146. Button {
  147. dish = ""
  148. saved = false
  149. isPromptPresented = false }
  150. label: { Text("Cancel") }
  151. } header: { Text("Enter Meal Preset Name") }
  152. }
  153. }
  154. private var empty: Bool {
  155. state.carbs <= 0 && state.fat <= 0 && state.protein <= 0
  156. }
  157. private var minusButton: some View {
  158. Button {
  159. if state.carbs != 0,
  160. (state.carbs - (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
  161. {
  162. state.carbs -= (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal)
  163. } else { state.carbs = 0 }
  164. if state.fat != 0,
  165. (state.fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
  166. {
  167. state.fat -= (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal)
  168. } else { state.fat = 0 }
  169. if state.protein != 0,
  170. (state.protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
  171. {
  172. state.protein -= (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal)
  173. } else { state.protein = 0 }
  174. state.removePresetFromNewMeal()
  175. if state.carbs == 0, state.fat == 0, state.protein == 0 { state.summation = [] }
  176. }
  177. label: { Image(systemName: "minus.circle") }
  178. .disabled(
  179. state
  180. .selection == nil ||
  181. (
  182. !state.summation
  183. .contains(state.selection?.dish ?? "") && (state.selection?.dish ?? "") != ""
  184. )
  185. )
  186. .buttonStyle(.borderless)
  187. .tint(.blue)
  188. }
  189. private var plusButton: some View {
  190. Button {
  191. state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
  192. state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
  193. state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
  194. state.addPresetToNewMeal()
  195. }
  196. label: { Image(systemName: "plus.circle") }
  197. .disabled(state.selection == nil)
  198. .buttonStyle(.borderless)
  199. .tint(.blue)
  200. }
  201. private var mealPresets: some View {
  202. Section {
  203. HStack {
  204. if state.selection != nil {
  205. minusButton
  206. }
  207. Picker("Preset", selection: $state.selection) {
  208. Text("Saved Food").tag(nil as Presets?)
  209. ForEach(carbPresets, id: \.self) { (preset: Presets) in
  210. Text(preset.dish ?? "").tag(preset as Presets?)
  211. }
  212. }
  213. .labelsHidden()
  214. .frame(maxWidth: .infinity, alignment: .center)
  215. ._onBindingChange($state.selection) { _ in
  216. state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
  217. state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
  218. state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
  219. state.addToSummation()
  220. }
  221. if state.selection != nil {
  222. plusButton
  223. }
  224. }
  225. HStack {
  226. Button("Delete Preset") {
  227. showAlert.toggle()
  228. }
  229. .disabled(state.selection == nil)
  230. .tint(.orange)
  231. .buttonStyle(.borderless)
  232. .alert(
  233. "Delete preset '\(state.selection?.dish ?? "")'?",
  234. isPresented: $showAlert,
  235. actions: {
  236. Button("No", role: .cancel) {}
  237. Button("Yes", role: .destructive) {
  238. state.deletePreset()
  239. state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
  240. state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
  241. state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
  242. state.addPresetToNewMeal()
  243. }
  244. }
  245. )
  246. Spacer()
  247. Button {
  248. isPromptPresented = true
  249. }
  250. label: { Text("Save as Preset") }
  251. .buttonStyle(.borderless)
  252. .disabled(
  253. empty ||
  254. (
  255. (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal) == state
  256. .carbs && (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) == state
  257. .fat && (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) == state
  258. .protein
  259. )
  260. )
  261. }
  262. }
  263. }
  264. @ViewBuilder private func proteinAndFat() -> some View {
  265. HStack {
  266. Text("Fat").foregroundColor(.orange)
  267. Spacer()
  268. DecimalTextField(
  269. "0",
  270. value: $state.fat,
  271. formatter: formatter,
  272. autofocus: false,
  273. cleanInput: true
  274. )
  275. Text("grams").foregroundColor(.secondary)
  276. }
  277. HStack {
  278. Text("Protein").foregroundColor(.red)
  279. Spacer()
  280. DecimalTextField(
  281. "0",
  282. value: $state.protein,
  283. formatter: formatter,
  284. autofocus: false,
  285. cleanInput: true
  286. ).foregroundColor(.loopRed)
  287. Text("grams").foregroundColor(.secondary)
  288. }
  289. }
  290. }
  291. }
  292. public extension Color {
  293. static func randomGreen(randomOpacity: Bool = false) -> Color {
  294. Color(
  295. red: .random(in: 0 ... 1),
  296. green: .random(in: 0.4 ... 0.7),
  297. blue: .random(in: 0.2 ... 1),
  298. opacity: randomOpacity ? .random(in: 0.8 ... 1) : 1
  299. )
  300. }
  301. }