MealPresetView.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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. .onChange(of: state.selection) { newSelection in
  108. guard let selected = newSelection else { return }
  109. carbs += ((selected.carbs ?? 0) as NSDecimalNumber) as Decimal
  110. fat += ((selected.fat ?? 0) as NSDecimalNumber) as Decimal
  111. protein += ((selected.protein ?? 0) as NSDecimalNumber) as Decimal
  112. state.addToSummation()
  113. }
  114. if state.selection != nil {
  115. plusButton
  116. }
  117. }
  118. HStack {
  119. Spacer()
  120. Button("Delete Preset") {
  121. showAlert.toggle()
  122. }
  123. .disabled(state.selection == nil)
  124. .tint(.orange)
  125. .buttonStyle(.borderless)
  126. .alert(
  127. "Delete preset '\(state.selection?.dish ?? "")'?",
  128. isPresented: $showAlert,
  129. actions: {
  130. Button("No", role: .cancel) {}
  131. Button("Yes", role: .destructive) {
  132. if let selection = state.selection {
  133. let previousSelection = state.selection
  134. let count = state.summation.filter { $0 == selection.dish }.count
  135. state.summation.removeAll { $0 == selection.dish }
  136. carbs -= (((selection.carbs ?? 0) as NSDecimalNumber) as Decimal) * Decimal(count)
  137. fat -= (((selection.fat ?? 0) as NSDecimalNumber) as Decimal) * Decimal(count)
  138. protein -= (((selection.protein ?? 0) as NSDecimalNumber) as Decimal) * Decimal(count)
  139. state.deletePreset()
  140. state.selection = previousSelection
  141. }
  142. }
  143. }
  144. )
  145. Spacer()
  146. }
  147. }.listRowBackground(Color.chart)
  148. }
  149. private var addPresetToTreatmentsButton: some View {
  150. Button {
  151. state.carbs += carbs
  152. state.fat += fat
  153. state.protein += protein
  154. dismiss()
  155. }
  156. label: {
  157. Text("Add to treatments")
  158. .font(.headline)
  159. .foregroundStyle(Color.white)
  160. .frame(maxWidth: .infinity, alignment: .center)
  161. }
  162. .disabled(noPresetChosen)
  163. .listRowBackground(noPresetChosen ? Color(.systemGray3) : Color(.systemBlue))
  164. .shadow(radius: 3)
  165. .clipShape(RoundedRectangle(cornerRadius: 8))
  166. }
  167. private var noPresetChosen: Bool {
  168. state.selection == nil || carbs == 0 || fat == 0 || protein == 0
  169. }
  170. @ViewBuilder private func dishInfos() -> some View {
  171. if !state.summation.isEmpty {
  172. let presetSummary = generatePresetSummary()
  173. Section(header: Text("Summary")) {
  174. presetSummary
  175. .lineLimit(nil) // In case the text is too long, allow it to wrap to the next line
  176. LazyVGrid(columns: [
  177. GridItem(.flexible(), alignment: .leading),
  178. GridItem(.flexible(), alignment: .trailing)
  179. ], spacing: 0) {
  180. Group {
  181. Text("Carbs: ")
  182. .font(.footnote)
  183. .foregroundStyle(.secondary)
  184. HStack(spacing: 2) {
  185. Text("\(carbs as NSNumber, formatter: mealFormatter)")
  186. .font(.footnote)
  187. Text(" g")
  188. .font(.footnote)
  189. .foregroundStyle(.secondary)
  190. }
  191. }
  192. Group {
  193. Text("Fat: ")
  194. .font(.footnote)
  195. .foregroundStyle(.secondary)
  196. HStack(spacing: 2) {
  197. Text("\(fat as NSNumber, formatter: mealFormatter)")
  198. .font(.footnote)
  199. Text(" g")
  200. .font(.footnote)
  201. .foregroundStyle(.secondary)
  202. }
  203. }
  204. Group {
  205. Text("Protein: ")
  206. .font(.footnote)
  207. .foregroundStyle(.secondary)
  208. HStack(spacing: 2) {
  209. Text("\(protein as NSNumber, formatter: mealFormatter)")
  210. .font(.footnote)
  211. Text(" g")
  212. .font(.footnote)
  213. .foregroundStyle(.secondary)
  214. }
  215. }
  216. }
  217. }.listRowBackground(Color.chart)
  218. }
  219. }
  220. private func generatePresetSummary() -> some View {
  221. var counts = [String: Int]()
  222. for preset in state.summation {
  223. counts[preset, default: 0] += 1
  224. }
  225. return VStack(alignment: .leading) {
  226. ForEach(counts.sorted(by: { $0.key < $1.key }), id: \.key) { key, value in
  227. if value > 0 {
  228. HStack {
  229. Text("\(value) x")
  230. .foregroundColor(.blue)
  231. Text(key)
  232. }
  233. }
  234. }
  235. }
  236. }
  237. private func resetValues() {
  238. dish = ""
  239. presetCarbs = 0
  240. presetFat = 0
  241. presetProtein = 0
  242. state.selection = nil
  243. state.summation.removeAll()
  244. }
  245. private var minusButton: some View {
  246. Button {
  247. if carbs != 0 {
  248. carbs -= (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal)
  249. } else { carbs = 0 }
  250. if fat != 0,
  251. (fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
  252. {
  253. fat -= (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal)
  254. } else { fat = 0 }
  255. if protein != 0,
  256. (protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
  257. {
  258. protein -= (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal)
  259. } else { protein = 0 }
  260. state.removePresetFromNewMeal()
  261. if carbs == 0, fat == 0, protein == 0 { state.summation = [] }
  262. }
  263. label: { Image(systemName: "minus.circle.fill")
  264. .font(.system(size: 20))
  265. }
  266. .disabled(
  267. state
  268. .selection == nil ||
  269. (
  270. !state.summation
  271. .contains(state.selection?.dish ?? "") && (state.selection?.dish ?? "") != ""
  272. )
  273. )
  274. .buttonStyle(.borderless)
  275. .tint(.blue)
  276. }
  277. private var plusButton: some View {
  278. Button {
  279. carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
  280. fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
  281. protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
  282. state.addPresetToNewMeal()
  283. }
  284. label: { Image(systemName: "plus.circle.fill")
  285. .font(.system(size: 20))
  286. }
  287. .disabled(state.selection == nil)
  288. .buttonStyle(.borderless)
  289. .tint(.blue)
  290. }
  291. private func savePreset() {
  292. if dish != "" {
  293. let preset = MealPresetStored(context: moc)
  294. preset.dish = dish
  295. preset.fat = presetFat as NSDecimalNumber
  296. preset.protein = presetProtein as NSDecimalNumber
  297. preset.carbs = presetCarbs as NSDecimalNumber
  298. do {
  299. guard moc.hasChanges else { return }
  300. try moc.save()
  301. showAddNewPresetSheet.toggle()
  302. resetValues()
  303. } catch let error as NSError {
  304. debugPrint("\(DebuggingIdentifiers.failed) Failed to save Meal Preset with error: \(error.userInfo)")
  305. }
  306. }
  307. }
  308. }