UserInterfaceSettingsRootView.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. import SwiftUI
  2. import Swinject
  3. extension UserInterfaceSettings {
  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 displayPickerLowThreshold: Bool = false
  14. @State private var displayPickerHighThreshold: Bool = false
  15. @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
  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 glucoseFormatter: NumberFormatter {
  34. let formatter = NumberFormatter()
  35. formatter.numberStyle = .decimal
  36. formatter.maximumFractionDigits = 0
  37. if state.units == .mmolL {
  38. formatter.maximumFractionDigits = 1
  39. }
  40. formatter.roundingMode = .halfUp
  41. return formatter
  42. }
  43. private var carbsFormatter: NumberFormatter {
  44. let formatter = NumberFormatter()
  45. formatter.numberStyle = .decimal
  46. formatter.maximumFractionDigits = 0
  47. return formatter
  48. }
  49. var body: some View {
  50. Form {
  51. Section(
  52. header: Text("General Appearance"),
  53. content: {
  54. VStack {
  55. Picker(
  56. selection: $colorSchemePreference,
  57. label: Text("Trio Color Scheme")
  58. ) {
  59. ForEach(ColorSchemeOption.allCases) { selection in
  60. Text(selection.displayName).tag(selection)
  61. }
  62. }.padding(.top)
  63. HStack(alignment: .top) {
  64. Text(
  65. "Choose between Light, Dark, or System Default for the app color scheme"
  66. )
  67. .font(.footnote)
  68. .foregroundColor(.secondary)
  69. .lineLimit(nil)
  70. Spacer()
  71. Button(
  72. action: {
  73. hintLabel = "Color Scheme Preference"
  74. selectedVerboseHint =
  75. AnyView(Text("""
  76. Set the app color scheme using the following options
  77. System Default: Follows the phone's current color scheme setting at that time
  78. Light: Always in Light mode
  79. Dark: Always in Dark mode
  80. """))
  81. shouldDisplayHint.toggle()
  82. },
  83. label: {
  84. HStack {
  85. Image(systemName: "questionmark.circle")
  86. }
  87. }
  88. ).buttonStyle(BorderlessButtonStyle())
  89. }.padding(.top)
  90. }.padding(.bottom)
  91. }
  92. ).listRowBackground(Color.chart)
  93. Section {
  94. VStack {
  95. Picker(
  96. selection: $state.glucoseColorScheme,
  97. label: Text("Glucose Color Scheme")
  98. ) {
  99. ForEach(GlucoseColorScheme.allCases) { selection in
  100. Text(selection.displayName).tag(selection)
  101. }
  102. }.padding(.top)
  103. HStack(alignment: .top) {
  104. Text(
  105. "Choose between Static or Dynamic coloring for glucose readings"
  106. )
  107. .font(.footnote)
  108. .foregroundColor(.secondary)
  109. .lineLimit(nil)
  110. Spacer()
  111. Button(
  112. action: {
  113. hintLabel = "Glucose Color Scheme"
  114. selectedVerboseHint =
  115. AnyView(
  116. Text(
  117. """
  118. Set the color scheme for glucose readings on the main glucose graph, live activities, and bolus calculatorusing the following options:
  119. Static: Below-Range Target readings will be in RED, In-Range will be GREEN, Above-Range will be YELLOW
  120. Dynamic: Readings on Target will be GREEN. As readings approach and exceed below target, they become more RED. As readings approach and exceed above targer, they become more PURPLE.
  121. """
  122. )
  123. )
  124. shouldDisplayHint.toggle()
  125. },
  126. label: {
  127. HStack {
  128. Image(systemName: "questionmark.circle")
  129. }
  130. }
  131. ).buttonStyle(BorderlessButtonStyle())
  132. }.padding(.top)
  133. }.padding(.bottom)
  134. }.listRowBackground(Color.chart)
  135. Section(
  136. header: Text("Home View Settings"),
  137. content: {
  138. VStack {
  139. Toggle("Show X-Axis Grid Lines", isOn: $state.xGridLines)
  140. Toggle("Show Y-Axis Grid Lines", isOn: $state.yGridLines)
  141. HStack(alignment: .top) {
  142. Text(
  143. "Display the grid lines behind the glucose graph"
  144. )
  145. .font(.footnote)
  146. .foregroundColor(.secondary)
  147. .lineLimit(nil)
  148. Spacer()
  149. Button(
  150. action: {
  151. hintLabel = "Show Main Chart X- and Y-Axis Grid Lines"
  152. selectedVerboseHint =
  153. AnyView(Text("""
  154. Choose whether or not to display one or both X- and Y-Axis grid lines.
  155. """))
  156. shouldDisplayHint.toggle()
  157. },
  158. label: {
  159. HStack {
  160. Image(systemName: "questionmark.circle")
  161. }
  162. }
  163. ).buttonStyle(BorderlessButtonStyle())
  164. }.padding(.top)
  165. }.padding(.vertical)
  166. }
  167. ).listRowBackground(Color.chart)
  168. SettingInputSection(
  169. decimalValue: $decimalPlaceholder,
  170. booleanValue: $state.rulerMarks,
  171. shouldDisplayHint: $shouldDisplayHint,
  172. selectedVerboseHint: Binding(
  173. get: { selectedVerboseHint },
  174. set: {
  175. selectedVerboseHint = $0.map { AnyView($0) }
  176. hintLabel = "Show Low and High Thresholds"
  177. }
  178. ),
  179. units: state.units,
  180. type: .boolean,
  181. label: "Show Low and High Thresholds",
  182. miniHint: "Display the Low and High glucose Thresholds set below",
  183. verboseHint: Text("""
  184. This setting displays the upper and lower values for your glucose target range.
  185. This range is for display and statistical purposes only and does not influence insulin dosing.
  186. """)
  187. )
  188. if state.rulerMarks {
  189. Section {
  190. VStack {
  191. VStack {
  192. HStack {
  193. Text("Low Threshold")
  194. Spacer()
  195. Group {
  196. Text(state.units == .mgdL ? state.low.description : state.low.asMmolL.description)
  197. .foregroundColor(!displayPickerLowThreshold ? .primary : .accentColor)
  198. Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
  199. }
  200. }
  201. .onTapGesture {
  202. displayPickerLowThreshold.toggle()
  203. }
  204. }
  205. .padding(.top)
  206. if displayPickerLowThreshold {
  207. let setting = PickerSettingsProvider.shared.settings.low
  208. Picker(selection: $state.low, label: Text("")) {
  209. ForEach(
  210. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  211. id: \.self
  212. ) { value in
  213. let displayValue = state.units == .mgdL ? value : value.asMmolL
  214. Text("\(displayValue.description)").tag(value)
  215. }
  216. }
  217. .pickerStyle(WheelPickerStyle())
  218. .frame(maxWidth: .infinity)
  219. }
  220. VStack {
  221. HStack {
  222. Text("High Threshold")
  223. Spacer()
  224. Group {
  225. Text(state.units == .mgdL ? state.high.description : state.high.asMmolL.description)
  226. .foregroundColor(!displayPickerHighThreshold ? .primary : .accentColor)
  227. Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
  228. }
  229. }
  230. .onTapGesture {
  231. displayPickerHighThreshold.toggle()
  232. }
  233. }
  234. .padding(.top)
  235. if displayPickerHighThreshold {
  236. let setting = PickerSettingsProvider.shared.settings.high
  237. Picker(selection: $state.high, label: Text("")) {
  238. ForEach(
  239. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  240. id: \.self
  241. ) { value in
  242. let displayValue = state.units == .mgdL ? value : value.asMmolL
  243. Text("\(displayValue.description)").tag(value)
  244. }
  245. }
  246. .pickerStyle(WheelPickerStyle())
  247. .frame(maxWidth: .infinity)
  248. }
  249. HStack(alignment: .top) {
  250. Text(
  251. """
  252. Set low and high glucose values for the main screen glucose graph and statistics
  253. Low Default: 70
  254. High Default: 180
  255. """
  256. )
  257. .lineLimit(nil)
  258. .font(.footnote)
  259. .foregroundColor(.secondary)
  260. Spacer()
  261. Button(
  262. action: {
  263. hintLabel = "Low and High Thresholds"
  264. selectedVerboseHint =
  265. AnyView(Text("""
  266. Default values are based on internationally accepted Time in Range values of 70-180 mg/dL (5.5-10 mmol/L)
  267. Set the values used in the main screen glucose graph and to determine Time in Range for Statistics.
  268. Note: These values are not used to calculate insulin dosing.
  269. """))
  270. shouldDisplayHint.toggle()
  271. },
  272. label: {
  273. HStack {
  274. Image(systemName: "questionmark.circle")
  275. }
  276. }
  277. ).buttonStyle(BorderlessButtonStyle())
  278. }.padding(.top)
  279. }.padding(.bottom)
  280. }.listRowBackground(Color.chart)
  281. }
  282. Section {
  283. VStack {
  284. Picker(
  285. selection: $state.forecastDisplayType,
  286. label: Text("Forecast Display Type")
  287. ) {
  288. ForEach(ForecastDisplayType.allCases) { selection in
  289. Text(selection.displayName).tag(selection)
  290. }
  291. }.padding(.top)
  292. HStack(alignment: .top) {
  293. Text(
  294. """
  295. Choose between the OpenAPS colored "Lines" or the "Cone" of Uncertainty for the Forecast Lines
  296. Default: Cone
  297. """
  298. )
  299. .font(.footnote)
  300. .foregroundColor(.secondary)
  301. .lineLimit(nil)
  302. Spacer()
  303. Button(
  304. action: {
  305. hintLabel = "Forecast Display Type"
  306. selectedVerboseHint =
  307. AnyView(Text("""
  308. Default: Cone
  309. This setting allows you to choose between the following two options for the Forecast lines (previously: Prediction Lines).
  310. Lines: Uses the IOB, COB, UAM, and ZT forecast lines from OpenAPS
  311. Cone: Uses a combined range of all possible forecasts from the OpenAPS lines and provides you with a range of possible forecasts. This option has shown to reduce confusion and stress around algorithm forecasts by providing a less concerning visual representation.
  312. """))
  313. shouldDisplayHint.toggle()
  314. },
  315. label: {
  316. HStack {
  317. Image(systemName: "questionmark.circle")
  318. }
  319. }
  320. ).buttonStyle(BorderlessButtonStyle())
  321. }.padding(.top)
  322. }.padding(.bottom)
  323. }.listRowBackground(Color.chart)
  324. SettingInputSection(
  325. decimalValue: $state.hours,
  326. booleanValue: $booleanPlaceholder,
  327. shouldDisplayHint: $shouldDisplayHint,
  328. selectedVerboseHint: Binding(
  329. get: { selectedVerboseHint },
  330. set: {
  331. selectedVerboseHint = $0.map { AnyView($0) }
  332. hintLabel = "X-Axis Interval Step"
  333. }
  334. ),
  335. units: state.units,
  336. type: .decimal("hours"),
  337. label: "X-Axis Interval Step",
  338. miniHint: "Determines how many hours are shown in the main graph",
  339. verboseHint: Text("""
  340. Default: 6 hours
  341. This setting determines how many hours are shown in each view of the main graph.
  342. The default setting of 6 hours uses 2, 4, 6, 12, and 24 hour views.
  343. A setting of 4 would use 1.3, 2, 4, 8, and 16 hour views.
  344. A setting of 9 would use 3, 4.5, 9, 18, and 36 hour views.
  345. """)
  346. )
  347. Section {
  348. VStack {
  349. Picker(
  350. selection: $state.totalInsulinDisplayType,
  351. label: Text("Total Insulin Display Type")
  352. ) {
  353. ForEach(TotalInsulinDisplayType.allCases) { selection in
  354. Text(selection.displayName).tag(selection)
  355. }
  356. }.padding(.top)
  357. HStack(alignment: .top) {
  358. Text(
  359. "Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph"
  360. )
  361. .font(.footnote)
  362. .foregroundColor(.secondary)
  363. .lineLimit(nil)
  364. Spacer()
  365. Button(
  366. action: {
  367. hintLabel = "Total Insulin Display Type"
  368. selectedVerboseHint =
  369. AnyView(Text("""
  370. Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph.
  371. Total Daily Dose: Displays the last 24 hours of total insulin administered, both basal and bolus.
  372. Total Insulin in Scope: Displays the total insulin administered since midnight, both basal and bolus.
  373. """))
  374. shouldDisplayHint.toggle()
  375. },
  376. label: {
  377. HStack {
  378. Image(systemName: "questionmark.circle")
  379. }
  380. }
  381. ).buttonStyle(BorderlessButtonStyle())
  382. }.padding(.top)
  383. }.padding(.bottom)
  384. }.listRowBackground(Color.chart)
  385. // TODO: this needs to be a picker: mmol/L or %
  386. SettingInputSection(
  387. decimalValue: $decimalPlaceholder,
  388. booleanValue: $state.overrideHbA1cUnit,
  389. shouldDisplayHint: $shouldDisplayHint,
  390. selectedVerboseHint: Binding(
  391. get: { selectedVerboseHint },
  392. set: {
  393. selectedVerboseHint = $0.map { AnyView($0) }
  394. hintLabel = "Override HbA1c Unit"
  395. }
  396. ),
  397. units: state.units,
  398. type: .boolean,
  399. label: "Override HbA1c Unit",
  400. miniHint: "Display HbA1c in mmol/mol or %",
  401. verboseHint: Text(
  402. "Choose between displaying the HbA1c value in the statistics view as a percentage (Target of <6.5%) or mmol/mol (Target of <48 mmol/mol)"
  403. ),
  404. headerText: "Trio Statistics"
  405. )
  406. // TODO: this needs to be a picker: choose bar chart or progress bar
  407. SettingInputSection(
  408. decimalValue: $decimalPlaceholder,
  409. booleanValue: $state.oneDimensionalGraph,
  410. shouldDisplayHint: $shouldDisplayHint,
  411. selectedVerboseHint: Binding(
  412. get: { selectedVerboseHint },
  413. set: {
  414. selectedVerboseHint = $0.map { AnyView($0) }
  415. hintLabel = "Standing / Laying TIR Chart"
  416. }
  417. ),
  418. units: state.units,
  419. type: .boolean,
  420. label: "Standing / Laying TIR Chart",
  421. miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
  422. verboseHint: Text("Standing / Laying TIR Chart… bla bla bla")
  423. )
  424. SettingInputSection(
  425. decimalValue: $state.carbsRequiredThreshold,
  426. booleanValue: $state.showCarbsRequiredBadge,
  427. shouldDisplayHint: $shouldDisplayHint,
  428. selectedVerboseHint: Binding(
  429. get: { selectedVerboseHint },
  430. set: {
  431. selectedVerboseHint = $0.map { AnyView($0) }
  432. hintLabel = "Show Carbs Required Badge"
  433. }
  434. ),
  435. units: state.units,
  436. type: .conditionalDecimal("carbsRequiredThreshold"),
  437. label: "Show Carbs Required Badge",
  438. conditionalLabel: "Carbs Required Threshold",
  439. miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
  440. verboseHint: Text("Show Carbs Required Badge… bla bla bla"),
  441. headerText: "Carbs Required Badge"
  442. )
  443. }
  444. .sheet(isPresented: $shouldDisplayHint) {
  445. SettingInputHintView(
  446. hintDetent: $hintDetent,
  447. shouldDisplayHint: $shouldDisplayHint,
  448. hintLabel: hintLabel ?? "",
  449. hintText: selectedVerboseHint ?? AnyView(EmptyView()),
  450. sheetTitle: "Help"
  451. )
  452. }
  453. .scrollContentBackground(.hidden).background(color)
  454. .onAppear(perform: configureView)
  455. .navigationBarTitle("User Interface")
  456. .navigationBarTitleDisplayMode(.automatic)
  457. }
  458. }
  459. }