MealSettingsRootView.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import SwiftUI
  2. import Swinject
  3. extension MealSettings {
  4. struct RootView: BaseView {
  5. let resolver: Resolver
  6. @StateObject var state = StateModel()
  7. @State private var shouldDisplayHint: Bool = false
  8. @State var hintDetent = PresentationDetent.large
  9. @State var selectedVerboseHint: AnyView?
  10. @State var hintLabel: String?
  11. @State private var decimalPlaceholder: Decimal = 0.0
  12. @State private var booleanPlaceholder: Bool = false
  13. @State private var displayPickerMaxCarbs: Bool = false
  14. @State private var displayPickerMaxFat: Bool = false
  15. @State private var displayPickerMaxProtein: Bool = false
  16. @Environment(\.colorScheme) var colorScheme
  17. var color: LinearGradient {
  18. colorScheme == .dark ? LinearGradient(
  19. gradient: Gradient(colors: [
  20. Color.bgDarkBlue,
  21. Color.bgDarkerDarkBlue
  22. ]),
  23. startPoint: .top,
  24. endPoint: .bottom
  25. )
  26. :
  27. LinearGradient(
  28. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  29. startPoint: .top,
  30. endPoint: .bottom
  31. )
  32. }
  33. private var conversionFormatter: NumberFormatter {
  34. let formatter = NumberFormatter()
  35. formatter.numberStyle = .decimal
  36. formatter.maximumFractionDigits = 1
  37. return formatter
  38. }
  39. private var intFormater: NumberFormatter {
  40. let formatter = NumberFormatter()
  41. formatter.allowsFloats = false
  42. return formatter
  43. }
  44. private var formatter: NumberFormatter {
  45. let formatter = NumberFormatter()
  46. formatter.numberStyle = .decimal
  47. return formatter
  48. }
  49. var body: some View {
  50. Form {
  51. Section(
  52. header: Text("Limits per Entry"),
  53. content: {
  54. VStack {
  55. VStack {
  56. HStack {
  57. Text("Max Carbs")
  58. Spacer()
  59. Group {
  60. Text(state.maxCarbs.description)
  61. .foregroundColor(!displayPickerMaxCarbs ? .primary : .accentColor)
  62. Text(" g").foregroundColor(.secondary)
  63. }
  64. }
  65. .onTapGesture {
  66. displayPickerMaxCarbs.toggle()
  67. }
  68. }.padding(.top)
  69. if displayPickerMaxCarbs {
  70. let setting = PickerSettingsProvider.shared.settings.maxCarbs
  71. Picker(selection: $state.maxCarbs, label: Text("")) {
  72. ForEach(
  73. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  74. id: \.self
  75. ) { value in
  76. Text("\(value.description)").tag(value)
  77. }
  78. }
  79. .pickerStyle(WheelPickerStyle())
  80. .frame(maxWidth: .infinity)
  81. }
  82. if state.useFPUconversion {
  83. VStack {
  84. HStack {
  85. Text("Max Fat")
  86. Spacer()
  87. Group {
  88. Text(state.maxFat.description)
  89. .foregroundColor(!displayPickerMaxFat ? .primary : .accentColor)
  90. Text(" g").foregroundColor(.secondary)
  91. }
  92. }
  93. .onTapGesture {
  94. displayPickerMaxFat.toggle()
  95. }
  96. }
  97. .padding(.top)
  98. if displayPickerMaxFat {
  99. let setting = PickerSettingsProvider.shared.settings.maxFat
  100. Picker(selection: $state.maxFat, label: Text("")) {
  101. ForEach(
  102. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  103. id: \.self
  104. ) { value in
  105. Text("\(value.description)").tag(value)
  106. }
  107. }
  108. .pickerStyle(WheelPickerStyle())
  109. .frame(maxWidth: .infinity)
  110. }
  111. VStack {
  112. HStack {
  113. Text("Max Protein")
  114. Spacer()
  115. Group {
  116. Text(state.maxProtein.description)
  117. .foregroundColor(!displayPickerMaxProtein ? .primary : .accentColor)
  118. Text(" g").foregroundColor(.secondary)
  119. }
  120. }
  121. .onTapGesture {
  122. displayPickerMaxProtein.toggle()
  123. }
  124. }
  125. .padding(.top)
  126. if displayPickerMaxProtein {
  127. let setting = PickerSettingsProvider.shared.settings.maxProtein
  128. Picker(selection: $state.maxProtein, label: Text("")) {
  129. ForEach(
  130. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  131. id: \.self
  132. ) { value in
  133. Text("\(value.description)").tag(value)
  134. }
  135. }
  136. .pickerStyle(WheelPickerStyle())
  137. .frame(maxWidth: .infinity)
  138. }
  139. }
  140. HStack(alignment: .top) {
  141. Text(
  142. "Set limits for each type of macro per meal entry"
  143. )
  144. .lineLimit(nil)
  145. .font(.footnote)
  146. .foregroundColor(.secondary)
  147. Spacer()
  148. Button(
  149. action: {
  150. hintLabel = "Limits per Entry"
  151. selectedVerboseHint =
  152. AnyView(Text("""
  153. Max Carbs = Enter the largest carbohydrate value allowed per meal entry
  154. Max Fat = Enter the largest fat value allowed per meal entry
  155. Max Protein = Enter the largest protein value allowed per meal entry
  156. """))
  157. shouldDisplayHint.toggle()
  158. },
  159. label: {
  160. HStack {
  161. Image(systemName: "questionmark.circle")
  162. }
  163. }
  164. ).buttonStyle(BorderlessButtonStyle())
  165. }.padding(.top)
  166. }.padding(.bottom)
  167. }
  168. ).listRowBackground(Color.chart)
  169. SettingInputSection(
  170. decimalValue: $decimalPlaceholder,
  171. booleanValue: $state.useFPUconversion,
  172. shouldDisplayHint: $shouldDisplayHint,
  173. selectedVerboseHint: Binding(
  174. get: { selectedVerboseHint },
  175. set: {
  176. selectedVerboseHint = $0.map { AnyView($0) }
  177. hintLabel = "Enable Fat and Protein Entries"
  178. }
  179. ),
  180. units: state.units,
  181. type: .boolean,
  182. label: "Enable Fat and Protein Entries",
  183. miniHint: """
  184. Allows you to add fat and protein macros to meals
  185. Default: OFF
  186. """,
  187. verboseHint: VStack(spacing: 10) {
  188. Text("Default: OFF").bold()
  189. Text("""
  190. Enabling this setting allows you to log fat and protein, which are then converted into future carb equivalents using the Warsaw Method.
  191. The Warsaw Method helps account for the delayed glucose spikes caused by fat and protein in meals. It uses Fat-Protein Units (FPU) to calculate the carb effect from fat and protein. The system spreads insulin delivery over several hours to mimic natural insulin release, helping to manage post-meal glucose spikes.
  192. """)
  193. Text("Fat Conversion").bold()
  194. Text("𝑭 = fat(g) × 90%").italic()
  195. Text("""
  196. Protein Conversion
  197. """).bold()
  198. Text("𝑷 = protein(g) × 40%").italic()
  199. Text("""
  200. FPU Conversion
  201. """).bold()
  202. Text("𝑭 + 𝑷 = g CHO").italic()
  203. Text(
  204. """
  205. You can personalize the conversion calculation by adjusting the following settings that will appear when this option is enabled:
  206. """
  207. )
  208. VStack(alignment: .leading) {
  209. Text("• Fat and Protein Delay")
  210. Text("• Maximum Duration")
  211. Text("• Spread Interval")
  212. Text("• Fat and Protein Percentage")
  213. }
  214. },
  215. headerText: "Fat and Protein"
  216. )
  217. if state.useFPUconversion {
  218. SettingInputSection(
  219. decimalValue: $state.delay,
  220. booleanValue: $booleanPlaceholder,
  221. shouldDisplayHint: $shouldDisplayHint,
  222. selectedVerboseHint: Binding(
  223. get: { selectedVerboseHint },
  224. set: {
  225. selectedVerboseHint = $0.map { AnyView($0) }
  226. hintLabel = "Fat and Protein Delay"
  227. }
  228. ),
  229. units: state.units,
  230. type: .decimal("delay"),
  231. label: "Fat and Protein Delay",
  232. miniHint: """
  233. Set the delay between fat & protein entry in the bolus calculator and the first FPU entry
  234. Default: 60 min
  235. """,
  236. verboseHint: VStack {
  237. Text("Default: 60 min").bold()
  238. Text("""
  239. The Fat Protein Delay setting defines the time between when you log fat and protein and when the system starts delivering insulin for the Fat-Protein Unit Carb Equivalents (FPUs).
  240. This delay accounts for the slower absorption of fat and protein, as calculated by the Warsaw Method, ensuring insulin delivery is properly timed to manage glucose spikes caused by high-fat, high-protein meals.
  241. """)
  242. }
  243. )
  244. SettingInputSection(
  245. decimalValue: $state.timeCap,
  246. booleanValue: $booleanPlaceholder,
  247. shouldDisplayHint: $shouldDisplayHint,
  248. selectedVerboseHint: Binding(
  249. get: { selectedVerboseHint },
  250. set: {
  251. selectedVerboseHint = $0.map { AnyView($0) }
  252. hintLabel = "Maximum Duration"
  253. }
  254. ),
  255. units: state.units,
  256. type: .decimal("timeCap"),
  257. label: "Maximum Duration",
  258. miniHint: """
  259. Set the maximum timeframe to extend FPUs
  260. Default: 8 hours
  261. """,
  262. verboseHint: VStack {
  263. Text("Default: 8 hours").bold()
  264. Text("""
  265. This sets the maximum length of time that Fat and Protein Carb Equivalents (FPUs) will be extended over from a single Fat and/or Protein bolus calcultor entry.
  266. It is one factor used in combination with the Fat and Protein Delay, Spread Interval, and Fat and Protein Factor to create the FPU entries
  267. Increasing this setting may result in more FPU entries with smaller carb values.
  268. Decreasing this setting may result in fewer FPU entries with larger carb values.
  269. """)
  270. Text("Accepted range for this setting is 5 - 12 hours.").italic()
  271. }
  272. )
  273. SettingInputSection(
  274. decimalValue: $state.minuteInterval,
  275. booleanValue: $booleanPlaceholder,
  276. shouldDisplayHint: $shouldDisplayHint,
  277. selectedVerboseHint: Binding(
  278. get: { selectedVerboseHint },
  279. set: {
  280. selectedVerboseHint = $0.map { AnyView($0) }
  281. hintLabel = "Spread Interval"
  282. }
  283. ),
  284. units: state.units,
  285. type: .decimal("minuteInterval"),
  286. label: "Spread Interval",
  287. miniHint: """
  288. Set the time interval between FPUs
  289. Default: 30 minutes
  290. """,
  291. verboseHint: VStack {
  292. Text("Default: 30 minutes").bold()
  293. Text("""
  294. This determines how many minutes will be between individual Fat-Protein Unit Carb Equivalent (FPU) entries from a single Fat and/or Protein bolus calculator entry.
  295. The shorter the interval, the smoother the correlating dosing result.
  296. Increasing this setting may result in fewer FPU entries with larger carb values.
  297. Decreasing this setting may result in more FPU entries with smaller carb values.
  298. """)
  299. Text("Accepted range for this setting is 5 - 60 minutes.").italic()
  300. }
  301. )
  302. SettingInputSection(
  303. decimalValue: $state.individualAdjustmentFactor,
  304. booleanValue: $booleanPlaceholder,
  305. shouldDisplayHint: $shouldDisplayHint,
  306. selectedVerboseHint: Binding(
  307. get: { selectedVerboseHint },
  308. set: {
  309. selectedVerboseHint = $0.map { AnyView($0) }
  310. hintLabel = "Fat and Protein Percentage"
  311. }
  312. ),
  313. units: state.units,
  314. type: .decimal("individualAdjustmentFactor"),
  315. label: "Fat and Protein Percentage",
  316. miniHint: """
  317. Influences the conversion rate used in the Warsaw Method
  318. Default: 50%
  319. """,
  320. verboseHint: VStack {
  321. Text("Default: 50%").bold()
  322. Text("This setting changes how much effect the fat and protein entry has on FPUs.")
  323. VStack(alignment: .center) {
  324. Text("50% is half effect:").bold()
  325. Text("(Fat × 45%) + (Protein × 20%)")
  326. Text("100% is full effect:").bold()
  327. Text("(Fat × 90%) + (Protein × 40%)")
  328. Text("200% is double effect:").bold()
  329. Text("(Fat × 180%) + (Protein x 80%)")
  330. Text("""
  331. You may find that your normal carb ratio needs to increase to a larger number when you begin adding fat and protein entries. For this reason, it is best to start with a factor of about 50% to ease into it.
  332. """).italic()
  333. }
  334. }
  335. )
  336. .sheet(isPresented: $shouldDisplayHint) {
  337. SettingInputHintView(
  338. hintDetent: $hintDetent,
  339. shouldDisplayHint: $shouldDisplayHint,
  340. hintLabel: hintLabel ?? "",
  341. hintText: selectedVerboseHint ?? AnyView(EmptyView()),
  342. sheetTitle: "Help"
  343. )
  344. }
  345. .scrollContentBackground(.hidden).background(color)
  346. .onAppear(perform: configureView)
  347. .navigationBarTitle("Meal Settings")
  348. .navigationBarTitleDisplayMode(.automatic)
  349. }
  350. }
  351. }
  352. }
  353. }