MealSettingsRootView.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  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. @Environment(AppState.self) var appState
  18. private var conversionFormatter: NumberFormatter {
  19. let formatter = NumberFormatter()
  20. formatter.numberStyle = .decimal
  21. formatter.maximumFractionDigits = 1
  22. return formatter
  23. }
  24. private var intFormater: NumberFormatter {
  25. let formatter = NumberFormatter()
  26. formatter.allowsFloats = false
  27. return formatter
  28. }
  29. private var formatter: NumberFormatter {
  30. let formatter = NumberFormatter()
  31. formatter.numberStyle = .decimal
  32. return formatter
  33. }
  34. var body: some View {
  35. List {
  36. Section(
  37. header: Text("Limits per Entry"),
  38. content: {
  39. VStack {
  40. VStack {
  41. HStack {
  42. Text("Max Carbs")
  43. Spacer()
  44. Group {
  45. Text(state.maxCarbs.description)
  46. .foregroundColor(!displayPickerMaxCarbs ? .primary : .accentColor)
  47. Text(" g").foregroundColor(.secondary)
  48. }
  49. }
  50. .onTapGesture {
  51. displayPickerMaxCarbs.toggle()
  52. }
  53. }.padding(.top)
  54. if displayPickerMaxCarbs {
  55. let setting = PickerSettingsProvider.shared.settings.maxCarbs
  56. Picker(selection: $state.maxCarbs, label: Text("")) {
  57. ForEach(
  58. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  59. id: \.self
  60. ) { value in
  61. Text("\(value.description)").tag(value)
  62. }
  63. }
  64. .pickerStyle(WheelPickerStyle())
  65. .frame(maxWidth: .infinity)
  66. }
  67. if state.useFPUconversion {
  68. VStack {
  69. HStack {
  70. Text("Max Fat")
  71. Spacer()
  72. Group {
  73. Text(state.maxFat.description)
  74. .foregroundColor(!displayPickerMaxFat ? .primary : .accentColor)
  75. Text(" g").foregroundColor(.secondary)
  76. }
  77. }
  78. .onTapGesture {
  79. displayPickerMaxFat.toggle()
  80. }
  81. }
  82. .padding(.top)
  83. if displayPickerMaxFat {
  84. let setting = PickerSettingsProvider.shared.settings.maxFat
  85. Picker(selection: $state.maxFat, label: Text("")) {
  86. ForEach(
  87. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  88. id: \.self
  89. ) { value in
  90. Text("\(value.description)").tag(value)
  91. }
  92. }
  93. .pickerStyle(WheelPickerStyle())
  94. .frame(maxWidth: .infinity)
  95. }
  96. VStack {
  97. HStack {
  98. Text("Max Protein")
  99. Spacer()
  100. Group {
  101. Text(state.maxProtein.description)
  102. .foregroundColor(!displayPickerMaxProtein ? .primary : .accentColor)
  103. Text(" g").foregroundColor(.secondary)
  104. }
  105. }
  106. .onTapGesture {
  107. displayPickerMaxProtein.toggle()
  108. }
  109. }
  110. .padding(.top)
  111. if displayPickerMaxProtein {
  112. let setting = PickerSettingsProvider.shared.settings.maxProtein
  113. Picker(selection: $state.maxProtein, label: Text("")) {
  114. ForEach(
  115. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  116. id: \.self
  117. ) { value in
  118. Text("\(value.description)").tag(value)
  119. }
  120. }
  121. .pickerStyle(WheelPickerStyle())
  122. .frame(maxWidth: .infinity)
  123. }
  124. }
  125. HStack(alignment: .center) {
  126. Text(
  127. "Set limits for each type of macro per meal entry."
  128. )
  129. .lineLimit(nil)
  130. .font(.footnote)
  131. .foregroundColor(.secondary)
  132. Spacer()
  133. Button(
  134. action: {
  135. hintLabel = "Limits per Entry"
  136. selectedVerboseHint =
  137. AnyView(
  138. VStack(alignment: .leading, spacing: 5) {
  139. Text("Max Carbs:").bold()
  140. Text("Enter the largest carb value allowed per meal entry")
  141. Text("Max Fat:").bold()
  142. Text("Enter the largest fat value allowed per meal entry")
  143. Text("Max Protein:").bold()
  144. Text("Enter the largest protein value allowed per meal entry")
  145. }
  146. )
  147. shouldDisplayHint.toggle()
  148. },
  149. label: {
  150. HStack {
  151. Image(systemName: "questionmark.circle")
  152. }
  153. }
  154. ).buttonStyle(BorderlessButtonStyle())
  155. }.padding(.top)
  156. }.padding(.bottom)
  157. }
  158. ).listRowBackground(Color.chart)
  159. SettingInputSection(
  160. decimalValue: $state.maxMealAbsorptionTime,
  161. booleanValue: $booleanPlaceholder,
  162. shouldDisplayHint: $shouldDisplayHint,
  163. selectedVerboseHint: Binding(
  164. get: { selectedVerboseHint },
  165. set: {
  166. selectedVerboseHint = $0.map { AnyView($0) }
  167. hintLabel = "Maximum Meal Absorption Time"
  168. }
  169. ),
  170. units: state.units,
  171. type: .decimal("maxMealAbsorptionTime"),
  172. label: "Max Meal Absorption Time",
  173. miniHint: "The max meal absorption time limits the duration the algorithm will track carb entries in estimating Carbs on Board (COB)",
  174. verboseHint:
  175. VStack(alignment: .leading, spacing: 10) {
  176. Text("Default: 6 hours").bold()
  177. Text(
  178. "Meals that are high in fat and protein can slow digestion. This can result in the Carbs on Board (COB) determination excluding carbs that are still being absorbed beyond the default time frame of 6 hours."
  179. )
  180. Text(
  181. "Increasing this setting will extend the time frame that carbs entered are available for determining COB."
  182. )
  183. }
  184. )
  185. SettingInputSection(
  186. decimalValue: $decimalPlaceholder,
  187. booleanValue: $state.useFPUconversion,
  188. shouldDisplayHint: $shouldDisplayHint,
  189. selectedVerboseHint: Binding(
  190. get: { selectedVerboseHint },
  191. set: {
  192. selectedVerboseHint = $0.map { AnyView($0) }
  193. hintLabel = "Enable Fat and Protein Entries"
  194. }
  195. ),
  196. units: state.units,
  197. type: .boolean,
  198. label: "Enable Fat and Protein Entries",
  199. miniHint: "Add fat and protein macros to meal entries.",
  200. verboseHint: VStack(alignment: .leading, spacing: 10) {
  201. Text("Default: OFF").bold()
  202. VStack(spacing: 10) {
  203. Text(
  204. "Enabling this setting allows you to log fat and protein, which are then converted into future carb equivalents using the Warsaw Method."
  205. )
  206. VStack(alignment: .leading, spacing: 5) {
  207. Text("Warsaw Method:").bold()
  208. Text(
  209. "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."
  210. )
  211. }
  212. VStack(alignment: .center, spacing: 5) {
  213. Text("Fat Conversion").bold()
  214. Text("𝑭 = fat(g) × 90%")
  215. }
  216. VStack(alignment: .center, spacing: 5) {
  217. Text("Protein Conversion").bold()
  218. Text("𝑷 = protein(g) × 40%")
  219. }
  220. VStack(alignment: .center, spacing: 5) {
  221. Text("FPU Conversion").bold()
  222. Text("𝑭 + 𝑷 = g CHO")
  223. }
  224. VStack(alignment: .leading, spacing: 5) {
  225. Text(
  226. "You can personalize the conversion calculation by adjusting the following settings that will appear when this option is enabled:"
  227. )
  228. Text("• Fat and Protein Delay")
  229. Text("• Maximum Duration")
  230. Text("• Spread Interval")
  231. Text("• Fat and Protein Percentage")
  232. }
  233. }
  234. },
  235. headerText: "Fat and Protein"
  236. )
  237. if state.useFPUconversion {
  238. SettingInputSection(
  239. decimalValue: $state.delay,
  240. booleanValue: $booleanPlaceholder,
  241. shouldDisplayHint: $shouldDisplayHint,
  242. selectedVerboseHint: Binding(
  243. get: { selectedVerboseHint },
  244. set: {
  245. selectedVerboseHint = $0.map { AnyView($0) }
  246. hintLabel = "Fat and Protein Delay"
  247. }
  248. ),
  249. units: state.units,
  250. type: .decimal("delay"),
  251. label: "Fat and Protein Delay",
  252. miniHint: "Delay between fat & protein entry and first FPU entry.",
  253. verboseHint:
  254. VStack(alignment: .leading, spacing: 10) {
  255. Text("Default: 60 min").bold()
  256. Text(
  257. "The Fat and 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)."
  258. )
  259. Text(
  260. "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."
  261. )
  262. }
  263. )
  264. SettingInputSection(
  265. decimalValue: $state.timeCap,
  266. booleanValue: $booleanPlaceholder,
  267. shouldDisplayHint: $shouldDisplayHint,
  268. selectedVerboseHint: Binding(
  269. get: { selectedVerboseHint },
  270. set: {
  271. selectedVerboseHint = $0.map { AnyView($0) }
  272. hintLabel = "Maximum Duration"
  273. }
  274. ),
  275. units: state.units,
  276. type: .decimal("timeCap"),
  277. label: "Maximum Duration",
  278. miniHint: "Set the maximum timeframe to extend FPUs.",
  279. verboseHint:
  280. VStack(alignment: .leading, spacing: 10) {
  281. Text("Default: 8 hours").bold()
  282. Text(
  283. "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."
  284. )
  285. Text(
  286. "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."
  287. )
  288. Text("Increasing this setting may result in more FPU entries with smaller carb values.")
  289. Text("Decreasing this setting may result in fewer FPU entries with larger carb values.")
  290. }
  291. )
  292. SettingInputSection(
  293. decimalValue: $state.minuteInterval,
  294. booleanValue: $booleanPlaceholder,
  295. shouldDisplayHint: $shouldDisplayHint,
  296. selectedVerboseHint: Binding(
  297. get: { selectedVerboseHint },
  298. set: {
  299. selectedVerboseHint = $0.map { AnyView($0) }
  300. hintLabel = "Spread Interval"
  301. }
  302. ),
  303. units: state.units,
  304. type: .decimal("minuteInterval"),
  305. label: "Spread Interval",
  306. miniHint: "Time interval between FPUs.",
  307. verboseHint:
  308. VStack(alignment: .leading, spacing: 10) {
  309. Text("Default: 30 minutes").bold()
  310. Text(
  311. "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."
  312. )
  313. Text("The shorter the interval, the smoother the correlating dosing result.")
  314. Text("Increasing this setting may result in fewer FPU entries with larger carb values.")
  315. Text("Decreasing this setting may result in more FPU entries with smaller carb values.")
  316. }
  317. )
  318. SettingInputSection(
  319. decimalValue: $state.individualAdjustmentFactor,
  320. booleanValue: $booleanPlaceholder,
  321. shouldDisplayHint: $shouldDisplayHint,
  322. selectedVerboseHint: Binding(
  323. get: { selectedVerboseHint },
  324. set: {
  325. selectedVerboseHint = $0.map { AnyView($0) }
  326. hintLabel = "Fat and Protein Percentage"
  327. }
  328. ),
  329. units: state.units,
  330. type: .decimal("individualAdjustmentFactor"),
  331. label: "Fat and Protein Percentage",
  332. miniHint: "Adjust the Warsaw Method FPU Conversion rate.",
  333. verboseHint: VStack(alignment: .leading, spacing: 10) {
  334. Text("Default: 50%").bold()
  335. VStack(spacing: 10) {
  336. Text("This setting changes how much effect the fat and protein entry has on FPUs.")
  337. VStack(alignment: .center, spacing: 5) {
  338. Text("50% is half effect:").bold()
  339. Text("(Fat × 45%) + (Protein × 20%)")
  340. Text("100% is full effect:").bold()
  341. Text("(Fat × 90%) + (Protein × 40%)")
  342. Text("110% makes fat-to-carbs ratio essentially equal:").bold()
  343. Text("(Fat × 99%) + (Protein x 44%)")
  344. }
  345. .multilineTextAlignment(.center)
  346. .fixedSize(horizontal: false, vertical: true)
  347. Text(
  348. "Tip: 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%."
  349. )
  350. }
  351. }
  352. )
  353. }
  354. }
  355. .listSectionSpacing(sectionSpacing)
  356. .sheet(isPresented: $shouldDisplayHint) {
  357. SettingInputHintView(
  358. hintDetent: $hintDetent,
  359. shouldDisplayHint: $shouldDisplayHint,
  360. hintLabel: hintLabel ?? "",
  361. hintText: selectedVerboseHint ?? AnyView(EmptyView()),
  362. sheetTitle: "Help"
  363. )
  364. }
  365. .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
  366. .onAppear(perform: configureView)
  367. .navigationBarTitle("Meal Settings")
  368. .navigationBarTitleDisplayMode(.automatic)
  369. }
  370. }
  371. }