MealPresetView.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import CoreData
  2. import Foundation
  3. import SwiftUI
  4. struct MealPresetView: View {
  5. @StateObject var state: Bolus.StateModel
  6. @Environment(\.colorScheme) var colorScheme
  7. @Environment(\.dismiss) var dismiss
  8. @Environment(\.managedObjectContext) var moc
  9. @State private var showAlert = false
  10. @State private var dish: String = ""
  11. @State private var showAddNewPresetSheet = false
  12. @State private var presetCarbs: Decimal = 0
  13. @State private var presetFat: Decimal = 0
  14. @State private var presetProtein: Decimal = 0
  15. @State private var carbs: Decimal = 0
  16. @State private var fat: Decimal = 0
  17. @State private var protein: Decimal = 0
  18. @FetchRequest(
  19. entity: MealPresetStored.entity(),
  20. sortDescriptors: [NSSortDescriptor(key: "dish", ascending: true)]
  21. ) var carbPresets: FetchedResults<MealPresetStored>
  22. private var mealFormatter: NumberFormatter {
  23. let formatter = NumberFormatter()
  24. formatter.numberStyle = .decimal
  25. formatter.maximumFractionDigits = 1
  26. return formatter
  27. }
  28. private var color: LinearGradient {
  29. colorScheme == .dark ? LinearGradient(
  30. gradient: Gradient(colors: [
  31. Color.bgDarkBlue,
  32. Color.bgDarkerDarkBlue
  33. ]),
  34. startPoint: .top,
  35. endPoint: .bottom
  36. )
  37. :
  38. LinearGradient(
  39. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  40. startPoint: .top,
  41. endPoint: .bottom
  42. )
  43. }
  44. var body: some View {
  45. NavigationStack {
  46. Form {
  47. mealPresets
  48. dishInfos()
  49. addPresetToTreatmentsButton
  50. }
  51. .scrollContentBackground(.hidden).background(color)
  52. .navigationTitle("Meal Presets")
  53. .navigationBarTitleDisplayMode(.automatic)
  54. .toolbar(content: {
  55. ToolbarItem(placement: .topBarLeading) {
  56. Button {
  57. dismiss()
  58. resetValues()
  59. } label: {
  60. Text("Close")
  61. }
  62. }
  63. ToolbarItem(placement: .topBarTrailing) {
  64. Button(action: {
  65. showAddNewPresetSheet.toggle()
  66. resetValues()
  67. }, label: {
  68. HStack {
  69. Text("New Preset")
  70. Image(systemName: "plus")
  71. }
  72. })
  73. }
  74. })
  75. .sheet(isPresented: $showAddNewPresetSheet) {
  76. AddMealPresetView(
  77. dish: $dish,
  78. presetCarbs: $presetCarbs,
  79. presetFat: $presetFat,
  80. presetProtein: $presetProtein,
  81. onSave: savePreset,
  82. onCancel: {
  83. showAddNewPresetSheet.toggle()
  84. resetValues()
  85. }
  86. )
  87. }
  88. .onDisappear {
  89. resetValues()
  90. }
  91. }
  92. }
  93. private var mealPresets: some View {
  94. Section {
  95. HStack {
  96. if state.selection != nil {
  97. minusButton
  98. }
  99. Picker("Preset", selection: $state.selection) {
  100. Text("Saved Food").tag(nil as MealPresetStored?)
  101. ForEach(carbPresets, id: \.self) { (preset: MealPresetStored) in
  102. Text(preset.dish ?? "").tag(preset as MealPresetStored?)
  103. }
  104. }
  105. .labelsHidden()
  106. .frame(maxWidth: .infinity, alignment: .center)
  107. if state.selection != nil {
  108. plusButton
  109. }
  110. }
  111. HStack {
  112. Spacer()
  113. Button("Delete Preset") {
  114. showAlert.toggle()
  115. }
  116. .disabled(state.selection == nil)
  117. .tint(.orange)
  118. .buttonStyle(.borderless)
  119. .alert(
  120. "Delete preset '\(state.selection?.dish ?? "")'?",
  121. isPresented: $showAlert,
  122. actions: {
  123. Button("No", role: .cancel) {}
  124. Button("Yes", role: .destructive) {
  125. if let selection = state.selection {
  126. let previousSelection = state.selection
  127. let count = state.summation.filter { $0 == selection.dish }.count
  128. state.summation.removeAll { $0 == selection.dish }
  129. carbs -= (((selection.carbs ?? 0) as NSDecimalNumber) as Decimal) * Decimal(count)
  130. fat -= (((selection.fat ?? 0) as NSDecimalNumber) as Decimal) * Decimal(count)
  131. protein -= (((selection.protein ?? 0) as NSDecimalNumber) as Decimal) * Decimal(count)
  132. state.deletePreset()
  133. state.selection = previousSelection
  134. }
  135. }
  136. }
  137. )
  138. Spacer()
  139. }
  140. }.listRowBackground(Color.chart)
  141. }
  142. private var addPresetToTreatmentsButton: some View {
  143. Button {
  144. state.carbs += carbs
  145. state.fat += fat
  146. state.protein += protein
  147. dismiss()
  148. }
  149. label: {
  150. Text("Add to treatments")
  151. .font(.headline)
  152. .foregroundStyle(Color.white)
  153. .frame(maxWidth: .infinity, alignment: .center)
  154. }
  155. .disabled(noPresetChosen)
  156. .listRowBackground(noPresetChosen ? Color(.systemGray3) : Color(.systemBlue))
  157. .shadow(radius: 3)
  158. .clipShape(RoundedRectangle(cornerRadius: 8))
  159. }
  160. private var noPresetChosen: Bool {
  161. state.selection == nil || carbs == 0 || fat == 0 || protein == 0
  162. }
  163. @ViewBuilder private func dishInfos() -> some View {
  164. if !state.summation.isEmpty {
  165. let presetSummary = generatePresetSummary()
  166. Section(header: Text("Summary")) {
  167. presetSummary
  168. .lineLimit(nil) // In case the text is too long, allow it to wrap to the next line
  169. LazyVGrid(columns: [
  170. GridItem(.flexible(), alignment: .leading),
  171. GridItem(.flexible(), alignment: .trailing)
  172. ], spacing: 0) {
  173. Group {
  174. Text("Carbs: ")
  175. .font(.footnote)
  176. .foregroundStyle(.secondary)
  177. HStack(spacing: 2) {
  178. Text("\(carbs as NSNumber, formatter: mealFormatter)")
  179. .font(.footnote)
  180. Text(" g")
  181. .font(.footnote)
  182. .foregroundStyle(.secondary)
  183. }
  184. }
  185. Group {
  186. Text("Fat: ")
  187. .font(.footnote)
  188. .foregroundStyle(.secondary)
  189. HStack(spacing: 2) {
  190. Text("\(fat as NSNumber, formatter: mealFormatter)")
  191. .font(.footnote)
  192. Text(" g")
  193. .font(.footnote)
  194. .foregroundStyle(.secondary)
  195. }
  196. }
  197. Group {
  198. Text("Protein: ")
  199. .font(.footnote)
  200. .foregroundStyle(.secondary)
  201. HStack(spacing: 2) {
  202. Text("\(protein as NSNumber, formatter: mealFormatter)")
  203. .font(.footnote)
  204. Text(" g")
  205. .font(.footnote)
  206. .foregroundStyle(.secondary)
  207. }
  208. }
  209. }
  210. }.listRowBackground(Color.chart)
  211. }
  212. }
  213. private func generatePresetSummary() -> some View {
  214. var counts = [String: Int]()
  215. for preset in state.summation {
  216. counts[preset, default: 0] += 1
  217. }
  218. return VStack(alignment: .leading) {
  219. ForEach(counts.sorted(by: { $0.key < $1.key }), id: \.key) { key, value in
  220. if value > 0 {
  221. HStack {
  222. Text("\(value) x")
  223. .foregroundColor(.blue)
  224. Text(key)
  225. }
  226. }
  227. }
  228. }
  229. }
  230. private func resetValues() {
  231. dish = ""
  232. presetCarbs = 0
  233. presetFat = 0
  234. presetProtein = 0
  235. state.selection = nil
  236. state.summation.removeAll()
  237. }
  238. private var minusButton: some View {
  239. Button {
  240. if carbs != 0 {
  241. carbs -= (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal)
  242. } else { carbs = 0 }
  243. if fat != 0,
  244. (fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
  245. {
  246. fat -= (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal)
  247. } else { fat = 0 }
  248. if protein != 0,
  249. (protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
  250. {
  251. protein -= (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal)
  252. } else { protein = 0 }
  253. state.removePresetFromNewMeal()
  254. if carbs == 0, fat == 0, protein == 0 { state.summation = [] }
  255. }
  256. label: { Image(systemName: "minus.circle.fill")
  257. .font(.system(size: 20))
  258. }
  259. .disabled(
  260. state
  261. .selection == nil ||
  262. (
  263. !state.summation
  264. .contains(state.selection?.dish ?? "") && (state.selection?.dish ?? "") != ""
  265. )
  266. )
  267. .buttonStyle(.borderless)
  268. .tint(.blue)
  269. }
  270. private var plusButton: some View {
  271. Button {
  272. carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
  273. fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
  274. protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
  275. state.addPresetToNewMeal()
  276. }
  277. label: { Image(systemName: "plus.circle.fill")
  278. .font(.system(size: 20))
  279. }
  280. .disabled(state.selection == nil)
  281. .buttonStyle(.borderless)
  282. .tint(.blue)
  283. }
  284. private func savePreset() {
  285. if dish != "" {
  286. let preset = MealPresetStored(context: moc)
  287. preset.dish = dish
  288. preset.fat = presetFat as NSDecimalNumber
  289. preset.protein = presetProtein as NSDecimalNumber
  290. preset.carbs = presetCarbs as NSDecimalNumber
  291. do {
  292. guard moc.hasChanges else { return }
  293. try moc.save()
  294. showAddNewPresetSheet.toggle()
  295. resetValues()
  296. } catch let error as NSError {
  297. debugPrint("\(DebuggingIdentifiers.failed) Failed to save Meal Preset with error: \(error.userInfo)")
  298. }
  299. }
  300. }
  301. }