TherapySettingsView.swift 27 KB

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