TherapySettingsView.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. //
  2. // TherapySettingsView.swift
  3. // LoopKitUI
  4. //
  5. // Created by Rick Pasetto on 7/7/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import AVFoundation
  9. import HealthKit
  10. import LoopKit
  11. import SwiftUI
  12. public struct TherapySettingsView: View {
  13. @Environment(\.dismiss) var dismiss
  14. @Environment(\.appName) private var appName
  15. public struct ActionButton {
  16. public init(localizedString: String, action: @escaping () -> Void) {
  17. self.localizedString = localizedString
  18. self.action = action
  19. }
  20. let localizedString: String
  21. let action: () -> Void
  22. }
  23. @ObservedObject var viewModel: TherapySettingsViewModel
  24. private let actionButton: ActionButton?
  25. public init(viewModel: TherapySettingsViewModel,
  26. actionButton: ActionButton? = nil) {
  27. self.viewModel = viewModel
  28. self.actionButton = actionButton
  29. }
  30. public var body: some View {
  31. switch viewModel.mode {
  32. case .acceptanceFlow: return AnyView(content)
  33. case .settings: return AnyView(navigationViewWrappedContent)
  34. }
  35. }
  36. private var content: some View {
  37. List {
  38. Group {
  39. if viewModel.mode == .acceptanceFlow && viewModel.prescription != nil {
  40. // At start of acceptance flow
  41. prescriptionSection
  42. } else if viewModel.mode == .acceptanceFlow && viewModel.prescription == nil {
  43. // At end of acceptance flow
  44. summaryHeaderSection
  45. }
  46. suspendThresholdSection
  47. correctionRangeSection
  48. preMealCorrectionRangeSection
  49. if !viewModel.sensitivityOverridesEnabled {
  50. workoutCorrectionRangeSection
  51. }
  52. carbRatioSection
  53. basalRatesSection
  54. deliveryLimitsSection
  55. insulinModelSection
  56. insulinSensitivitiesSection
  57. }
  58. lastItem
  59. }
  60. .insetGroupedListStyle()
  61. .onAppear() {
  62. UITableView.appearance().separatorStyle = .singleLine // Add lines between rows
  63. }
  64. .navigationBarTitle(Text(LocalizedString("Therapy Settings", comment: "Therapy Settings screen title")), displayMode: .large)
  65. }
  66. private var navigationViewWrappedContent: some View {
  67. NavigationView {
  68. content
  69. .navigationBarItems(trailing: dismissButton)
  70. }
  71. }
  72. private var dismissButton: some View {
  73. Button(action: {
  74. self.dismiss()
  75. }) {
  76. Text(LocalizedString("Done", comment: "Text for dismiss button")).bold()
  77. }
  78. }
  79. @ViewBuilder private var lastItem: some View {
  80. if viewModel.mode == .acceptanceFlow {
  81. if actionButton != nil {
  82. Button(action: actionButton!.action) {
  83. Text(actionButton!.localizedString)
  84. }
  85. .buttonStyle(ActionButtonStyle(.primary))
  86. .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
  87. }
  88. } else {
  89. supportSection
  90. }
  91. }
  92. }
  93. // MARK: Sections
  94. extension TherapySettingsView {
  95. private var prescriptionSection: some View {
  96. Section(header: Spacer()) {
  97. VStack(alignment: .leading) {
  98. Spacer()
  99. Text(LocalizedString("Prescription", comment: "title for prescription section"))
  100. .bold()
  101. Spacer()
  102. DescriptiveText(label: prescriptionDescriptiveText)
  103. Spacer()
  104. }
  105. }
  106. }
  107. private var summaryHeaderSection: some View {
  108. Section(header: Spacer()) {
  109. VStack(alignment: .leading) {
  110. Spacer()
  111. Text(LocalizedString("Review and Save Settings", comment: "title for summary description section"))
  112. .bold()
  113. .foregroundColor(.white)
  114. Spacer()
  115. VStack (alignment: .leading, spacing: 10) {
  116. DescriptiveText(label: summaryHeaderReviewText, color: .white)
  117. DescriptiveText(label: summaryHeaderEditText, color: .white)
  118. }
  119. Spacer()
  120. }
  121. }
  122. .listRowBackground(Color.accentColor)
  123. }
  124. private var summaryHeaderReviewText: String {
  125. String(format: LocalizedString("Review your therapy settings below. If you’d like to edit any of these settings, tap Back to go back to that screen.", comment: "Description of how to interact with summary screen"))
  126. }
  127. private var summaryHeaderEditText: String {
  128. String(format: LocalizedString("If these settings look good to you, tap Save Settings to continue.", comment: "Description of how to interact with summary screen"))
  129. }
  130. private var prescriptionDescriptiveText: String {
  131. String(format: LocalizedString("Submitted by %1$@, %2$@", comment: "Format for prescription descriptive text (1: providerName, 2: datePrescribed)"),
  132. viewModel.prescription!.providerName,
  133. DateFormatter.localizedString(from: viewModel.prescription!.datePrescribed, dateStyle: .short, timeStyle: .none))
  134. }
  135. private var suspendThresholdSection: some View {
  136. section(for: .suspendThreshold, header: viewModel.prescription == nil ? AnyView(Spacer()) : AnyView(EmptyView())) {
  137. if let glucoseUnit = self.glucoseUnit {
  138. HStack {
  139. Spacer()
  140. GuardrailConstrainedQuantityView(
  141. value: self.viewModel.therapySettings.suspendThreshold?.quantity,
  142. unit: glucoseUnit,
  143. guardrail: .suspendThreshold,
  144. isEditing: false,
  145. // Workaround for strange animation behavior on appearance
  146. forceDisableAnimations: true
  147. )
  148. }
  149. }
  150. }
  151. }
  152. private var correctionRangeSection: some View {
  153. section(for: .glucoseTargetRange) {
  154. if let glucoseUnit = self.glucoseUnit, let schedule = self.viewModel.therapySettings.glucoseTargetRangeSchedule {
  155. ForEach(schedule.items, id: \.self) { value in
  156. ScheduleRangeItem(time: value.startTime,
  157. range: value.value,
  158. unit: glucoseUnit,
  159. guardrail: .correctionRange)
  160. }
  161. }
  162. }
  163. }
  164. private var preMealCorrectionRangeSection: some View {
  165. section(for: .preMealCorrectionRangeOverride) {
  166. if let glucoseUnit = self.glucoseUnit, let schedule = self.viewModel.therapySettings.glucoseTargetRangeSchedule {
  167. CorrectionRangeOverridesRangeItem(
  168. preMealTargetRange: self.viewModel.therapySettings.preMealTargetRange,
  169. workoutTargetRange: self.viewModel.therapySettings.workoutTargetRange,
  170. unit: glucoseUnit,
  171. preset: CorrectionRangeOverrides.Preset.preMeal,
  172. suspendThreshold: self.viewModel.therapySettings.suspendThreshold,
  173. correctionRangeScheduleRange: schedule.scheduleRange()
  174. )
  175. }
  176. }
  177. }
  178. private var workoutCorrectionRangeSection: some View {
  179. section(for: .workoutCorrectionRangeOverride) {
  180. if let glucoseUnit = self.glucoseUnit, let schedule = self.viewModel.therapySettings.glucoseTargetRangeSchedule {
  181. CorrectionRangeOverridesRangeItem(
  182. preMealTargetRange: self.viewModel.therapySettings.preMealTargetRange,
  183. workoutTargetRange: self.viewModel.therapySettings.workoutTargetRange,
  184. unit: glucoseUnit,
  185. preset: CorrectionRangeOverrides.Preset.workout,
  186. suspendThreshold: self.viewModel.therapySettings.suspendThreshold,
  187. correctionRangeScheduleRange: schedule.scheduleRange()
  188. )
  189. }
  190. }
  191. }
  192. private var basalRatesSection: some View {
  193. section(for: .basalRate) {
  194. if self.viewModel.therapySettings.basalRateSchedule != nil && self.viewModel.pumpSupportedIncrements?() != nil {
  195. ForEach(self.viewModel.therapySettings.basalRateSchedule!.items, id: \.self) { value in
  196. ScheduleValueItem(time: value.startTime,
  197. value: value.value,
  198. unit: .internationalUnitsPerHour,
  199. guardrail: Guardrail.basalRate(supportedBasalRates: self.viewModel.pumpSupportedIncrements!()!.basalRates))
  200. }
  201. }
  202. }
  203. }
  204. private var deliveryLimitsSection: some View {
  205. section(for: .deliveryLimits) {
  206. self.maxBasalRateItem
  207. self.maxBolusItem
  208. }
  209. }
  210. private var maxBasalRateItem: some View {
  211. HStack {
  212. Text(DeliveryLimits.Setting.maximumBasalRate.title)
  213. Spacer()
  214. if self.viewModel.pumpSupportedIncrements?() != nil {
  215. GuardrailConstrainedQuantityView(
  216. value: self.viewModel.therapySettings.maximumBasalRatePerHour.map { HKQuantity(unit: .internationalUnitsPerHour, doubleValue: $0) },
  217. unit: .internationalUnitsPerHour,
  218. guardrail: Guardrail.maximumBasalRate(
  219. supportedBasalRates: self.viewModel.pumpSupportedIncrements!()!.basalRates,
  220. scheduledBasalRange: self.viewModel.therapySettings.basalRateSchedule?.valueRange(),
  221. lowestCarbRatio: self.viewModel.therapySettings.carbRatioSchedule?.lowestValue()),
  222. isEditing: false,
  223. // Workaround for strange animation behavior on appearance
  224. forceDisableAnimations: true
  225. )
  226. }
  227. }
  228. .accessibilityElement(children: .combine)
  229. }
  230. private var maxBolusItem: some View {
  231. HStack {
  232. Text(DeliveryLimits.Setting.maximumBolus.title)
  233. Spacer()
  234. if self.viewModel.pumpSupportedIncrements?() != nil {
  235. GuardrailConstrainedQuantityView(
  236. value: self.viewModel.therapySettings.maximumBolus.map { HKQuantity(unit: .internationalUnit(), doubleValue: $0) },
  237. unit: .internationalUnit(),
  238. guardrail: Guardrail.maximumBolus(supportedBolusVolumes: self.viewModel.pumpSupportedIncrements!()!.bolusVolumes),
  239. isEditing: false,
  240. // Workaround for strange animation behavior on appearance
  241. forceDisableAnimations: true
  242. )
  243. }
  244. }
  245. .accessibilityElement(children: .combine)
  246. }
  247. private var insulinModelSection: some View {
  248. section(for: .insulinModel) {
  249. if self.viewModel.therapySettings.insulinModelSettings != nil {
  250. // Spacing and paddings here is my best guess based on the design...
  251. VStack(alignment: .leading, spacing: 4) {
  252. Text(self.viewModel.therapySettings.insulinModelSettings!.title)
  253. .font(.body)
  254. .padding(.top, 5)
  255. Text(self.viewModel.therapySettings.insulinModelSettings!.subtitle)
  256. .font(.footnote)
  257. .foregroundColor(.secondary)
  258. .padding(.bottom, 8)
  259. }
  260. .accessibilityElement(children: .combine)
  261. }
  262. }
  263. }
  264. private var carbRatioSection: some View {
  265. section(for: .carbRatio) {
  266. if self.viewModel.therapySettings.carbRatioSchedule != nil {
  267. ForEach(self.viewModel.therapySettings.carbRatioSchedule!.items, id: \.self) { value in
  268. ScheduleValueItem(time: value.startTime,
  269. value: value.value,
  270. unit: .gramsPerUnit,
  271. guardrail: Guardrail.carbRatio)
  272. }
  273. }
  274. }
  275. }
  276. private var insulinSensitivitiesSection: some View {
  277. section(for: .insulinSensitivity) {
  278. if let sensitivityUnit = self.sensitivityUnit, let schedule = self.viewModel.therapySettings.insulinSensitivitySchedule {
  279. ForEach(schedule.items, id: \.self) { value in
  280. ScheduleValueItem(time: value.startTime,
  281. value: value.value,
  282. unit: sensitivityUnit,
  283. guardrail: Guardrail.insulinSensitivity)
  284. }
  285. }
  286. }
  287. }
  288. private var supportSection: some View {
  289. Section(header: SectionHeader(label: LocalizedString("Support", comment: "Title for support section"))) {
  290. NavigationLink(destination: Text("Therapy Settings Support Placeholder")) {
  291. Text("Get help with Therapy Settings", comment: "Support button for Therapy Settings")
  292. }
  293. }
  294. }
  295. }
  296. // MARK: Utilities
  297. extension TherapySettingsView {
  298. private var glucoseUnit: HKUnit? {
  299. viewModel.therapySettings.glucoseTargetRangeSchedule?.unit
  300. }
  301. private var sensitivityUnit: HKUnit? {
  302. glucoseUnit?.unitDivided(by: .internationalUnit())
  303. }
  304. private func section<Content>(for therapySetting: TherapySetting,
  305. @ViewBuilder content: @escaping () -> Content) -> some View where Content: View {
  306. SectionWithTapToEdit(isEnabled: viewModel.mode != .acceptanceFlow,
  307. header: EmptyView(),
  308. title: therapySetting.title,
  309. descriptiveText: therapySetting.descriptiveText(appName: appName),
  310. destination: screen(for: therapySetting),
  311. content: content)
  312. }
  313. private func section<Content, Header>(for therapySetting: TherapySetting,
  314. header: Header,
  315. @ViewBuilder content: @escaping () -> Content) -> some View where Content: View, Header: View {
  316. SectionWithTapToEdit(isEnabled: viewModel.mode != .acceptanceFlow,
  317. header: header,
  318. title: therapySetting.title,
  319. descriptiveText: therapySetting.descriptiveText(appName: appName),
  320. destination: screen(for: therapySetting),
  321. content: content)
  322. }
  323. }
  324. typealias HKQuantityGuardrail = Guardrail<HKQuantity>
  325. struct ScheduleRangeItem: View {
  326. let time: TimeInterval
  327. let range: DoubleRange
  328. let unit: HKUnit
  329. let guardrail: HKQuantityGuardrail
  330. public var body: some View {
  331. ScheduleItemView(time: time,
  332. isEditing: .constant(false),
  333. valueContent: {
  334. GuardrailConstrainedQuantityRangeView(range: range.quantityRange(for: unit), unit: unit, guardrail: guardrail, isEditing: false)
  335. },
  336. expandedContent: { EmptyView() })
  337. }
  338. }
  339. struct ScheduleValueItem: View {
  340. let time: TimeInterval
  341. let value: Double
  342. let unit: HKUnit
  343. let guardrail: HKQuantityGuardrail
  344. public var body: some View {
  345. ScheduleItemView(time: time,
  346. isEditing: .constant(false),
  347. valueContent: {
  348. GuardrailConstrainedQuantityView(value: HKQuantity(unit: unit, doubleValue: value), unit: unit, guardrail: guardrail, isEditing: false)
  349. },
  350. expandedContent: { EmptyView() })
  351. }
  352. }
  353. struct CorrectionRangeOverridesRangeItem: View {
  354. let preMealTargetRange: DoubleRange?
  355. let workoutTargetRange: DoubleRange?
  356. let unit: HKUnit
  357. let preset: CorrectionRangeOverrides.Preset
  358. let suspendThreshold: GlucoseThreshold?
  359. let correctionRangeScheduleRange: ClosedRange<HKQuantity>
  360. public var body: some View {
  361. CorrectionRangeOverridesExpandableSetting(
  362. isEditing: .constant(false),
  363. value: .constant(CorrectionRangeOverrides(
  364. preMeal: preMealTargetRange,
  365. workout: workoutTargetRange,
  366. unit: unit
  367. )),
  368. preset: preset,
  369. unit: unit,
  370. suspendThreshold: suspendThreshold,
  371. correctionRangeScheduleRange: correctionRangeScheduleRange,
  372. expandedContent: { EmptyView() })
  373. }
  374. }
  375. struct SectionWithTapToEdit<Header, Content, NavigationDestination>: View where Header: View, Content: View, NavigationDestination: View {
  376. let isEnabled: Bool
  377. let header: Header
  378. let title: String
  379. let descriptiveText: String
  380. let destination: (_ goBack: @escaping () -> Void) -> NavigationDestination
  381. let content: () -> Content
  382. @State var isActive: Bool = false
  383. private func onFinish() {
  384. // Dispatching here fixes an issue on iOS 14.2 where schedule editors do not dismiss. It does not fix iOS 14.0 and 14.1
  385. DispatchQueue.main.async {
  386. self.isActive = false
  387. }
  388. }
  389. public var body: some View {
  390. Section(header: header) {
  391. VStack(alignment: .leading) {
  392. Spacer()
  393. Text(title)
  394. .bold()
  395. Spacer()
  396. ZStack(alignment: .leading) {
  397. DescriptiveText(label: descriptiveText)
  398. if isEnabled {
  399. NavigationLink(destination: destination(onFinish), isActive: $isActive) {
  400. EmptyView()
  401. }
  402. }
  403. }
  404. Spacer()
  405. }
  406. content()
  407. }
  408. .contentShape(Rectangle()) // make the whole card tappable
  409. .highPriorityGesture(
  410. TapGesture()
  411. .onEnded { _ in
  412. self.isActive = true
  413. })
  414. }
  415. }
  416. // MARK: Navigation
  417. private extension TherapySettingsView {
  418. func screen(for setting: TherapySetting) -> (_ goBack: @escaping () -> Void) -> AnyView {
  419. switch setting {
  420. case .suspendThreshold:
  421. if viewModel.therapySettings.glucoseUnit != nil {
  422. return { goBack in
  423. AnyView(SuspendThresholdEditor(viewModel: self.viewModel, didSave: goBack).environment(\.dismiss, goBack))
  424. }
  425. }
  426. case .glucoseTargetRange:
  427. if viewModel.therapySettings.glucoseUnit != nil {
  428. return { goBack in
  429. AnyView(CorrectionRangeScheduleEditor(viewModel: self.viewModel, didSave: goBack).environment(\.dismiss, goBack))
  430. }
  431. }
  432. case .preMealCorrectionRangeOverride:
  433. if self.viewModel.therapySettings.glucoseUnit != nil {
  434. return { goBack in
  435. AnyView(CorrectionRangeOverridesEditor(viewModel: self.viewModel, preset: .preMeal, didSave: goBack).environment(\.dismiss, goBack))
  436. }
  437. }
  438. case .workoutCorrectionRangeOverride:
  439. if self.viewModel.therapySettings.glucoseUnit != nil {
  440. return { goBack in
  441. AnyView(CorrectionRangeOverridesEditor(viewModel: self.viewModel, preset: .workout, didSave: goBack).environment(\.dismiss, goBack))
  442. }
  443. }
  444. case .basalRate:
  445. if self.viewModel.pumpSupportedIncrements?() != nil {
  446. return { goBack in
  447. AnyView(BasalRateScheduleEditor(viewModel: self.viewModel, didSave: goBack).environment(\.dismiss, goBack))
  448. }
  449. }
  450. case .deliveryLimits:
  451. if self.viewModel.pumpSupportedIncrements?() != nil {
  452. return { goBack in
  453. AnyView(DeliveryLimitsEditor(viewModel: self.viewModel, didSave: goBack).environment(\.dismiss, goBack))
  454. }
  455. }
  456. case .insulinModel:
  457. if self.viewModel.therapySettings.glucoseUnit != nil && self.viewModel.therapySettings.insulinModelSettings != nil {
  458. return { goBack in
  459. AnyView(InsulinModelSelection(viewModel: self.viewModel, didSave: goBack).environment(\.dismiss, goBack))
  460. }
  461. }
  462. case .carbRatio:
  463. return { goBack in
  464. AnyView(CarbRatioScheduleEditor(viewModel: self.viewModel, didSave: goBack).environment(\.dismiss, goBack))
  465. }
  466. case .insulinSensitivity:
  467. if self.viewModel.therapySettings.glucoseUnit != nil {
  468. return { goBack in
  469. return AnyView(InsulinSensitivityScheduleEditor(viewModel: self.viewModel, didSave: goBack).environment(\.dismiss, goBack))
  470. }
  471. }
  472. case .none:
  473. break
  474. }
  475. return { _ in AnyView(Text("\(setting.title)")) }
  476. }
  477. }
  478. // MARK: Previews
  479. public struct TherapySettingsView_Previews: PreviewProvider {
  480. static let preview_glucoseScheduleItems = [
  481. RepeatingScheduleValue(startTime: 0, value: DoubleRange(80...90)),
  482. RepeatingScheduleValue(startTime: 1800, value: DoubleRange(90...100)),
  483. RepeatingScheduleValue(startTime: 3600, value: DoubleRange(100...110))
  484. ]
  485. static let preview_therapySettings = TherapySettings(
  486. glucoseTargetRangeSchedule: GlucoseRangeSchedule(unit: .milligramsPerDeciliter, dailyItems: preview_glucoseScheduleItems),
  487. preMealTargetRange: DoubleRange(88...99),
  488. workoutTargetRange: DoubleRange(99...111),
  489. maximumBasalRatePerHour: 55,
  490. maximumBolus: 4,
  491. suspendThreshold: GlucoseThreshold.init(unit: .milligramsPerDeciliter, value: 60),
  492. insulinSensitivitySchedule: InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter.unitDivided(by: HKUnit.internationalUnit()), dailyItems: []),
  493. carbRatioSchedule: nil,
  494. basalRateSchedule: BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: 0, value: 0.2), RepeatingScheduleValue(startTime: 1800, value: 0.75)]))
  495. static let preview_supportedBasalRates = [0.2, 0.5, 0.75, 1.0]
  496. static let preview_supportedBolusVolumes = [5.0, 10.0, 15.0]
  497. static func preview_viewModel(mode: SettingsPresentationMode) -> TherapySettingsViewModel {
  498. TherapySettingsViewModel(mode: mode,
  499. therapySettings: preview_therapySettings,
  500. preferredGlucoseUnit: .milligramsPerDeciliter,
  501. supportedInsulinModelSettings: SupportedInsulinModelSettings(fiaspModelEnabled: true, walshModelEnabled: true),
  502. pumpSupportedIncrements: { PumpSupportedIncrements(basalRates: preview_supportedBasalRates,
  503. bolusVolumes: preview_supportedBolusVolumes,
  504. maximumBasalScheduleEntryCount: 24) } ,
  505. chartColors: ChartColorPalette(axisLine: .clear, axisLabel: .secondaryLabel, grid: .systemGray3, glucoseTint: .systemTeal, insulinTint: .systemOrange))
  506. }
  507. public static var previews: some View {
  508. Group {
  509. TherapySettingsView(viewModel: preview_viewModel(mode: .acceptanceFlow))
  510. .colorScheme(.light)
  511. .previewDevice(PreviewDevice(rawValue: "iPhone SE 2"))
  512. .previewDisplayName("SE light (onboarding)")
  513. TherapySettingsView(viewModel: preview_viewModel(mode: .settings))
  514. .colorScheme(.light)
  515. .previewDevice(PreviewDevice(rawValue: "iPhone SE 2"))
  516. .previewDisplayName("SE light (settings)")
  517. TherapySettingsView(viewModel: preview_viewModel(mode: .settings))
  518. .colorScheme(.dark)
  519. .previewDevice(PreviewDevice(rawValue: "iPhone XS Max"))
  520. .previewDisplayName("XS Max dark (settings)")
  521. TherapySettingsView(viewModel: TherapySettingsViewModel(mode: .settings,
  522. therapySettings: TherapySettings(),
  523. preferredGlucoseUnit: .milligramsPerDeciliter,
  524. chartColors: ChartColorPalette(axisLine: .clear,
  525. axisLabel: .secondaryLabel,
  526. grid: .systemGray3,
  527. glucoseTint: .systemTeal,
  528. insulinTint: .systemOrange)))
  529. .colorScheme(.light)
  530. .previewDevice(PreviewDevice(rawValue: "iPhone SE 2"))
  531. .previewDisplayName("SE light (Empty TherapySettings)")
  532. }
  533. }
  534. }