TherapySettingsView.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  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. @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference
  14. @Environment(\.chartColorPalette) var chartColorPalette
  15. @Environment(\.dismissAction) var dismissAction
  16. @Environment(\.appName) private var appName
  17. public struct ActionButton {
  18. public init(localizedString: String, action: @escaping () -> Void) {
  19. self.localizedString = localizedString
  20. self.action = action
  21. }
  22. let localizedString: String
  23. let action: () -> Void
  24. }
  25. private let mode: SettingsPresentationMode
  26. @ObservedObject var viewModel: TherapySettingsViewModel
  27. private let actionButton: ActionButton?
  28. public init(mode: SettingsPresentationMode,
  29. viewModel: TherapySettingsViewModel,
  30. actionButton: ActionButton? = nil) {
  31. self.mode = mode
  32. self.viewModel = viewModel
  33. self.actionButton = actionButton
  34. }
  35. public var body: some View {
  36. switch mode {
  37. case .acceptanceFlow:
  38. content
  39. case .settings:
  40. navigationViewWrappedContent
  41. }
  42. }
  43. private var content: some View {
  44. CardList(title: cardListTitle, style: .sectioned(cardListSections), trailer: cardListTrailer)
  45. }
  46. private var cardListTitle: Text? { mode == .acceptanceFlow ? Text(therapySettingsTitle) : nil }
  47. private var therapySettingsTitle: String {
  48. return LocalizedString("Therapy Settings", comment: "Therapy Settings screen title")
  49. }
  50. private var cardListSections: [CardListSection] {
  51. var cardListSections: [CardListSection] = []
  52. cardListSections.append(therapySettingsCardListSection)
  53. if mode == .settings {
  54. cardListSections.append(supportCardListSection)
  55. }
  56. return cardListSections
  57. }
  58. private var therapySettingsCardListSection: CardListSection {
  59. CardListSection {
  60. therapySettingsCardStack
  61. .spacing(20)
  62. }
  63. }
  64. private var therapySettingsCardStack: CardStack {
  65. var cards: [Card] = []
  66. if mode == .acceptanceFlow {
  67. if viewModel.prescription != nil {
  68. cards.append(prescriptionSection)
  69. } else {
  70. cards.append(summaryHeaderSection)
  71. }
  72. }
  73. cards.append(suspendThresholdSection)
  74. cards.append(correctionRangeSection)
  75. cards.append(preMealCorrectionRangeSection)
  76. if !viewModel.sensitivityOverridesEnabled {
  77. cards.append(workoutCorrectionRangeSection)
  78. }
  79. cards.append(carbRatioSection)
  80. cards.append(basalRatesSection)
  81. cards.append(deliveryLimitsSection)
  82. if viewModel.adultChildInsulinModelSelectionEnabled {
  83. cards.append(insulinModelSection)
  84. }
  85. cards.append(insulinSensitivitiesSection)
  86. return CardStack(cards: cards)
  87. }
  88. private var supportCardListSection: CardListSection {
  89. CardListSection(title: Text(LocalizedString("Support", comment: "Title for support section"))) {
  90. supportSection
  91. }
  92. }
  93. private var navigationViewWrappedContent: some View {
  94. NavigationView {
  95. ZStack {
  96. Color(.systemGroupedBackground)
  97. .edgesIgnoringSafeArea(.all)
  98. content
  99. .toolbar {
  100. ToolbarItem(placement: .navigationBarTrailing) {
  101. dismissButton
  102. }
  103. }
  104. .navigationBarTitle(therapySettingsTitle, displayMode: .large)
  105. }
  106. }
  107. }
  108. private var dismissButton: some View {
  109. Button(action: dismissAction) {
  110. Text(LocalizedString("Done", comment: "Text for dismiss button"))
  111. .bold()
  112. }
  113. }
  114. @ViewBuilder
  115. private var cardListTrailer: some View {
  116. if mode == .acceptanceFlow {
  117. if let actionButton = actionButton {
  118. Button(action: actionButton.action) {
  119. Text(actionButton.localizedString)
  120. }
  121. .buttonStyle(ActionButtonStyle(.primary))
  122. .padding()
  123. }
  124. }
  125. }
  126. }
  127. // MARK: Sections
  128. extension TherapySettingsView {
  129. private var prescriptionSection: Card {
  130. Card {
  131. HStack {
  132. VStack(alignment: .leading) {
  133. Text(LocalizedString("Prescription", comment: "title for prescription section"))
  134. .bold()
  135. Spacer()
  136. DescriptiveText(label: prescriptionDescriptiveText)
  137. }
  138. Spacer()
  139. }
  140. }
  141. }
  142. private var summaryHeaderSection: Card {
  143. Card {
  144. VStack(alignment: .leading) {
  145. Text(LocalizedString("Review and Save Settings", comment: "title for summary description section"))
  146. .bold()
  147. .foregroundColor(.white)
  148. Spacer()
  149. VStack (alignment: .leading, spacing: 10) {
  150. DescriptiveText(label: summaryHeaderReviewText, color: .white)
  151. .fixedSize(horizontal: false, vertical: true)
  152. DescriptiveText(label: summaryHeaderEditText, color: .white)
  153. .fixedSize(horizontal: false, vertical: true)
  154. }
  155. }
  156. }
  157. .backgroundColor(Color.accentColor)
  158. }
  159. private var summaryHeaderReviewText: String {
  160. 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"))
  161. }
  162. private var summaryHeaderEditText: String {
  163. String(format: LocalizedString("If these settings look good to you, tap Save Settings to continue.", comment: "Description of how to interact with summary screen"))
  164. }
  165. private var prescriptionDescriptiveText: String {
  166. guard let prescription = viewModel.prescription else {
  167. return ""
  168. }
  169. return String(format: LocalizedString("Submitted by %1$@, %2$@", comment: "Format for prescription descriptive text (1: providerName, 2: datePrescribed)"),
  170. prescription.providerName,
  171. DateFormatter.localizedString(from: prescription.datePrescribed, dateStyle: .short, timeStyle: .none))
  172. }
  173. private var suspendThresholdSection: Card {
  174. card(for: .suspendThreshold) {
  175. SectionDivider()
  176. HStack {
  177. Spacer()
  178. GuardrailConstrainedQuantityView(
  179. value: self.viewModel.suspendThreshold?.quantity,
  180. unit: glucoseUnit,
  181. guardrail: .suspendThreshold,
  182. isEditing: false,
  183. // Workaround for strange animation behavior on appearance
  184. forceDisableAnimations: true
  185. )
  186. }
  187. }
  188. }
  189. private var correctionRangeSection: Card {
  190. card(for: .glucoseTargetRange) {
  191. if let items = self.viewModel.glucoseTargetRangeSchedule(for: glucoseUnit)?.items
  192. {
  193. SectionDivider()
  194. ForEach(items.indices, id: \.self) { index in
  195. if index > 0 {
  196. SettingsDivider()
  197. }
  198. ScheduleRangeItem(time: items[index].startTime,
  199. range: items[index].value,
  200. unit: glucoseUnit,
  201. guardrail: .correctionRange)
  202. }
  203. }
  204. }
  205. }
  206. private var preMealCorrectionRangeSection: Card {
  207. card(for: .preMealCorrectionRangeOverride) {
  208. let correctionRangeOverrides = self.viewModel.correctionRangeOverrides
  209. if let schedule = self.viewModel.glucoseTargetRangeSchedule {
  210. SectionDivider()
  211. CorrectionRangeOverridesRangeItem(
  212. value: correctionRangeOverrides,
  213. displayGlucoseUnit: glucoseUnit,
  214. preset: CorrectionRangeOverrides.Preset.preMeal,
  215. suspendThreshold: viewModel.suspendThreshold,
  216. correctionRangeScheduleRange: schedule.scheduleRange()
  217. )
  218. }
  219. }
  220. }
  221. private var workoutCorrectionRangeSection: Card {
  222. card(for: .workoutCorrectionRangeOverride) {
  223. let correctionRangeOverrides = self.viewModel.correctionRangeOverrides
  224. if let schedule = self.viewModel.glucoseTargetRangeSchedule {
  225. SectionDivider()
  226. CorrectionRangeOverridesRangeItem(
  227. value: correctionRangeOverrides,
  228. displayGlucoseUnit: glucoseUnit,
  229. preset: CorrectionRangeOverrides.Preset.workout,
  230. suspendThreshold: self.viewModel.suspendThreshold,
  231. correctionRangeScheduleRange: schedule.scheduleRange()
  232. )
  233. }
  234. }
  235. }
  236. private var basalRatesSection: Card {
  237. card(for: .basalRate) {
  238. if let schedule = viewModel.therapySettings.basalRateSchedule,
  239. let supportedBasalRates = viewModel.pumpSupportedIncrements()?.basalRates
  240. {
  241. let items = schedule.items
  242. let total = schedule.total()
  243. SectionDivider()
  244. ForEach(items.indices, id: \.self) { index in
  245. if index > 0 {
  246. SettingsDivider()
  247. }
  248. ScheduleValueItem(time: items[index].startTime,
  249. value: items[index].value,
  250. unit: .internationalUnitsPerHour,
  251. guardrail: .basalRate(supportedBasalRates: supportedBasalRates))
  252. }
  253. SectionDivider()
  254. HStack {
  255. Text(NSLocalizedString("Total", comment: "The text indicating Total for Daily Schedule Basal"))
  256. .bold()
  257. .foregroundColor(.primary)
  258. Spacer()
  259. Text(String(format: "%.2f ",total))
  260. .foregroundColor(.primary) +
  261. Text(NSLocalizedString("U/day", comment: "The text indicating U/day for Daily Schedule Basal"))
  262. .foregroundColor(.secondary)
  263. }
  264. }
  265. }
  266. }
  267. private var deliveryLimitsSection: Card {
  268. card(for: .deliveryLimits) {
  269. SectionDivider()
  270. self.maxBasalRateItem
  271. SettingsDivider()
  272. self.maxBolusItem
  273. }
  274. }
  275. private var maxBasalRateItem: some View {
  276. HStack {
  277. Text(DeliveryLimits.Setting.maximumBasalRate.title)
  278. Spacer()
  279. if let basalRates = self.viewModel.pumpSupportedIncrements()?.basalRates {
  280. GuardrailConstrainedQuantityView(
  281. value: self.viewModel.therapySettings.maximumBasalRatePerHour.map { HKQuantity(unit: .internationalUnitsPerHour, doubleValue: $0) },
  282. unit: .internationalUnitsPerHour,
  283. guardrail: .maximumBasalRate(
  284. supportedBasalRates: basalRates,
  285. scheduledBasalRange: self.viewModel.therapySettings.basalRateSchedule?.valueRange(),
  286. lowestCarbRatio: self.viewModel.therapySettings.carbRatioSchedule?.lowestValue()),
  287. isEditing: false,
  288. // Workaround for strange animation behavior on appearance
  289. forceDisableAnimations: true
  290. )
  291. }
  292. }
  293. .accessibilityElement(children: .combine)
  294. }
  295. private var maxBolusItem: some View {
  296. HStack {
  297. Text(DeliveryLimits.Setting.maximumBolus.title)
  298. Spacer()
  299. if let maximumBolusVolumes = self.viewModel.pumpSupportedIncrements()?.maximumBolusVolumes {
  300. GuardrailConstrainedQuantityView(
  301. value: self.viewModel.therapySettings.maximumBolus.map { HKQuantity(unit: .internationalUnit(), doubleValue: $0) },
  302. unit: .internationalUnit(),
  303. guardrail: .maximumBolus(supportedBolusVolumes: maximumBolusVolumes),
  304. isEditing: false,
  305. // Workaround for strange animation behavior on appearance
  306. forceDisableAnimations: true
  307. )
  308. }
  309. }
  310. .accessibilityElement(children: .combine)
  311. }
  312. private var insulinModelSection: Card {
  313. card(for: .insulinModel) {
  314. if let insulinModelPreset = self.viewModel.therapySettings.defaultRapidActingModel {
  315. SectionDivider()
  316. HStack {
  317. // Spacing and paddings here is my best guess based on the design...
  318. VStack(alignment: .leading, spacing: 4) {
  319. Text(insulinModelPreset.title)
  320. .font(.body)
  321. .padding(.top, 5)
  322. .fixedSize(horizontal: false, vertical: true)
  323. Text(insulinModelPreset.subtitle)
  324. .font(.footnote)
  325. .foregroundColor(.secondary)
  326. .padding(.bottom, 8)
  327. .fixedSize(horizontal: false, vertical: true)
  328. }
  329. Spacer()
  330. Image(systemName: "checkmark")
  331. .font(Font.system(.title2).weight(.semibold))
  332. .foregroundColor(.accentColor)
  333. }
  334. .accessibilityElement(children: .combine)
  335. }
  336. }
  337. }
  338. private var carbRatioSection: Card {
  339. card(for: .carbRatio) {
  340. if let items = viewModel.therapySettings.carbRatioSchedule?.items {
  341. SectionDivider()
  342. ForEach(items.indices, id: \.self) { index in
  343. if index > 0 {
  344. SettingsDivider()
  345. }
  346. ScheduleValueItem(time: items[index].startTime,
  347. value: items[index].value,
  348. unit: .gramsPerUnit,
  349. guardrail: .carbRatio)
  350. }
  351. }
  352. }
  353. }
  354. private var insulinSensitivitiesSection: Card {
  355. card(for: .insulinSensitivity) {
  356. if let items = viewModel.insulinSensitivitySchedule(for: glucoseUnit)?.items {
  357. SectionDivider()
  358. ForEach(items.indices, id: \.self) { index in
  359. if index > 0 {
  360. SettingsDivider()
  361. }
  362. ScheduleValueItem(time: items[index].startTime,
  363. value: items[index].value,
  364. unit: sensitivityUnit,
  365. guardrail: .insulinSensitivity)
  366. }
  367. }
  368. }
  369. }
  370. private var supportSection: some View {
  371. Section {
  372. NavigationLink(destination: DemoPlaceHolderView(appName: appName)) {
  373. HStack {
  374. Text("Get help with Therapy Settings", comment: "Support button for Therapy Settings")
  375. .foregroundColor(.primary)
  376. Spacer()
  377. Disclosure()
  378. }
  379. }
  380. }
  381. .contentShape(Rectangle())
  382. }
  383. }
  384. // MARK: Navigation
  385. extension TherapySettingsView {
  386. @ViewBuilder
  387. func screen(for setting: TherapySetting, dismiss: @escaping () -> Void) -> some View {
  388. switch setting {
  389. case .suspendThreshold:
  390. SuspendThresholdEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss)
  391. case .glucoseTargetRange:
  392. CorrectionRangeScheduleEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss)
  393. case .preMealCorrectionRangeOverride:
  394. CorrectionRangeOverridesEditor(mode: mode, therapySettingsViewModel: viewModel, preset: .preMeal, didSave: dismiss)
  395. case .workoutCorrectionRangeOverride:
  396. CorrectionRangeOverridesEditor(mode: mode, therapySettingsViewModel: viewModel, preset: .workout, didSave: dismiss)
  397. case .basalRate:
  398. BasalRateScheduleEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss)
  399. case .deliveryLimits:
  400. DeliveryLimitsEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss)
  401. case .insulinModel:
  402. InsulinModelSelection(mode: mode, therapySettingsViewModel: viewModel, chartColors: chartColorPalette, didSave: dismiss)
  403. case .carbRatio:
  404. CarbRatioScheduleEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss)
  405. case .insulinSensitivity:
  406. InsulinSensitivityScheduleEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss)
  407. case .none:
  408. EmptyView()
  409. }
  410. }
  411. }
  412. // MARK: Utilities
  413. extension TherapySettingsView {
  414. private var glucoseUnit: HKUnit {
  415. displayGlucosePreference.unit
  416. }
  417. private var sensitivityUnit: HKUnit {
  418. glucoseUnit.unitDivided(by: .internationalUnit())
  419. }
  420. private func card<Content>(for therapySetting: TherapySetting, @ViewBuilder content: @escaping () -> Content) -> Card where Content: View {
  421. Card {
  422. SectionWithTapToEdit(
  423. isEnabled: mode != .acceptanceFlow,
  424. title: therapySetting.title,
  425. descriptiveText: therapySetting.descriptiveText(appName: appName),
  426. destination: { dismiss in
  427. screen(for: therapySetting, dismiss: dismiss)
  428. .environment(\.dismissAction, dismiss)
  429. },
  430. content: content
  431. )
  432. }
  433. }
  434. }
  435. typealias HKQuantityGuardrail = Guardrail<HKQuantity>
  436. struct ScheduleRangeItem: View {
  437. let time: TimeInterval
  438. let range: DoubleRange
  439. let unit: HKUnit
  440. let guardrail: HKQuantityGuardrail
  441. public var body: some View {
  442. ScheduleItemView(time: time,
  443. isEditing: .constant(false),
  444. valueContent: {
  445. GuardrailConstrainedQuantityRangeView(range: range.quantityRange(for: unit), unit: unit, guardrail: guardrail, isEditing: false)
  446. .padding(.leading, 10)
  447. },
  448. expandedContent: { EmptyView() })
  449. }
  450. }
  451. struct ScheduleValueItem: View {
  452. let time: TimeInterval
  453. let value: Double
  454. let unit: HKUnit
  455. let guardrail: HKQuantityGuardrail
  456. public var body: some View {
  457. ScheduleItemView(time: time,
  458. isEditing: .constant(false),
  459. valueContent: {
  460. GuardrailConstrainedQuantityView(value: HKQuantity(unit: unit, doubleValue: value), unit: unit, guardrail: guardrail, isEditing: false)
  461. .padding(.leading, 10)
  462. },
  463. expandedContent: { EmptyView() })
  464. }
  465. }
  466. struct CorrectionRangeOverridesRangeItem: View {
  467. let value: CorrectionRangeOverrides
  468. let displayGlucoseUnit: HKUnit
  469. let preset: CorrectionRangeOverrides.Preset
  470. let suspendThreshold: GlucoseThreshold?
  471. let correctionRangeScheduleRange: ClosedRange<HKQuantity>
  472. public var body: some View {
  473. CorrectionRangeOverridesExpandableSetting(
  474. isEditing: .constant(false),
  475. value: .constant(value),
  476. preset: preset,
  477. unit: displayGlucoseUnit,
  478. suspendThreshold: suspendThreshold,
  479. correctionRangeScheduleRange: correctionRangeScheduleRange,
  480. expandedContent: { EmptyView() })
  481. }
  482. }
  483. struct SectionWithTapToEdit<Content, NavigationDestination>: View where Content: View, NavigationDestination: View {
  484. let isEnabled: Bool
  485. let title: String
  486. let descriptiveText: String
  487. let destination: (_ goBack: @escaping () -> Void) -> NavigationDestination
  488. let content: Content
  489. @State var isActive: Bool = false
  490. init(isEnabled: Bool,
  491. title: String,
  492. descriptiveText: String,
  493. destination: @escaping (@escaping () -> Void) -> NavigationDestination,
  494. content: () -> Content)
  495. {
  496. self.isEnabled = isEnabled
  497. self.title = title
  498. self.descriptiveText = descriptiveText
  499. self.destination = destination
  500. self.content = content()
  501. }
  502. private func onFinish() {
  503. // 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
  504. // Added a delay, since recently a similar issue was encountered in a plugin where a delay was also needed. Still uncertain why.
  505. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  506. self.isActive = false
  507. }
  508. }
  509. public var body: some View {
  510. Section {
  511. VStack(alignment: .leading) {
  512. Text(title)
  513. .bold()
  514. Spacer()
  515. HStack {
  516. DescriptiveText(label: descriptiveText)
  517. .fixedSize(horizontal: false, vertical: true)
  518. Spacer()
  519. if isEnabled {
  520. NavigationLink(destination: destination(onFinish), isActive: $isActive) {
  521. Disclosure()
  522. }
  523. .frame(width: 10, alignment: .trailing)
  524. }
  525. }
  526. Spacer()
  527. }
  528. content
  529. }
  530. .contentShape(Rectangle()) // make the whole card tappable
  531. .highPriorityGesture(
  532. TapGesture()
  533. .onEnded { _ in
  534. self.isActive = true
  535. })
  536. }
  537. }
  538. // MARK: Previews
  539. public struct TherapySettingsView_Previews: PreviewProvider {
  540. static let preview_glucoseScheduleItems = [
  541. RepeatingScheduleValue(startTime: 0, value: DoubleRange(80...90)),
  542. RepeatingScheduleValue(startTime: 1800, value: DoubleRange(90...100)),
  543. RepeatingScheduleValue(startTime: 3600, value: DoubleRange(100...110))
  544. ]
  545. static let preview_therapySettings = TherapySettings(
  546. glucoseTargetRangeSchedule: GlucoseRangeSchedule(unit: .milligramsPerDeciliter, dailyItems: preview_glucoseScheduleItems),
  547. correctionRangeOverrides: CorrectionRangeOverrides(preMeal: DoubleRange(88...99),
  548. workout: DoubleRange(99...111),
  549. unit: .milligramsPerDeciliter),
  550. maximumBolus: 4,
  551. suspendThreshold: GlucoseThreshold.init(unit: .milligramsPerDeciliter, value: 60),
  552. insulinSensitivitySchedule: InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter.unitDivided(by: HKUnit.internationalUnit()), dailyItems: []),
  553. carbRatioSchedule: nil,
  554. basalRateSchedule: BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: 0, value: 0.2), RepeatingScheduleValue(startTime: 1800, value: 0.75)]))
  555. static let preview_supportedBasalRates = [0.2, 0.5, 0.75, 1.0]
  556. static let preview_supportedBolusVolumes = [1.0, 2.0, 3.0]
  557. static let preview_supportedMaximumBolusVolumes = [5.0, 10.0, 15.0]
  558. static func preview_viewModel() -> TherapySettingsViewModel {
  559. TherapySettingsViewModel(therapySettings: preview_therapySettings,
  560. pumpSupportedIncrements: {
  561. PumpSupportedIncrements(basalRates: preview_supportedBasalRates,
  562. bolusVolumes: preview_supportedBolusVolumes,
  563. maximumBolusVolumes: preview_supportedMaximumBolusVolumes,
  564. maximumBasalScheduleEntryCount: 24) })
  565. }
  566. public static var previews: some View {
  567. Group {
  568. TherapySettingsView(mode: .acceptanceFlow, viewModel: preview_viewModel())
  569. .colorScheme(.light)
  570. .previewDevice(PreviewDevice(rawValue: "iPhone SE 2"))
  571. .previewDisplayName("SE light (onboarding)")
  572. .environmentObject(DisplayGlucosePreference(displayGlucoseUnit: .milligramsPerDeciliter))
  573. TherapySettingsView(mode: .settings, viewModel: preview_viewModel())
  574. .colorScheme(.light)
  575. .previewDevice(PreviewDevice(rawValue: "iPhone SE 2"))
  576. .previewDisplayName("SE light (settings)")
  577. .environmentObject(DisplayGlucosePreference(displayGlucoseUnit: .milligramsPerDeciliter))
  578. TherapySettingsView(mode: .settings, viewModel: preview_viewModel())
  579. .colorScheme(.dark)
  580. .previewDevice(PreviewDevice(rawValue: "iPhone XS Max"))
  581. .previewDisplayName("XS Max dark (settings)")
  582. TherapySettingsView(mode: .settings, viewModel: TherapySettingsViewModel(therapySettings: TherapySettings()))
  583. .colorScheme(.light)
  584. .previewDevice(PreviewDevice(rawValue: "iPhone SE 2"))
  585. .previewDisplayName("SE light (Empty TherapySettings)")
  586. .environmentObject(DisplayGlucosePreference(displayGlucoseUnit: .millimolesPerLiter))
  587. }
  588. }
  589. }
  590. fileprivate struct SectionDivider: View {
  591. var body: some View {
  592. Divider()
  593. .padding(.trailing, -16)
  594. }
  595. }
  596. fileprivate struct SettingsDivider: View {
  597. var body: some View {
  598. Divider()
  599. .padding(.trailing, -8)
  600. }
  601. }
  602. fileprivate struct Disclosure: View {
  603. var body: some View {
  604. Image(systemName: "chevron.right")
  605. .imageScale(.small)
  606. .font(.headline)
  607. .foregroundColor(.secondary)
  608. .opacity(0.5)
  609. }
  610. }