TherapySettingsView.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  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. 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. if let correctionRangeOverrides = self.viewModel.correctionRangeOverrides,
  209. let schedule = self.viewModel.glucoseTargetRangeSchedule
  210. {
  211. SectionDivider()
  212. CorrectionRangeOverridesRangeItem(
  213. value: correctionRangeOverrides,
  214. displayGlucoseUnit: glucoseUnit,
  215. preset: CorrectionRangeOverrides.Preset.preMeal,
  216. suspendThreshold: viewModel.suspendThreshold,
  217. correctionRangeScheduleRange: schedule.scheduleRange()
  218. )
  219. }
  220. }
  221. }
  222. private var workoutCorrectionRangeSection: Card {
  223. card(for: .workoutCorrectionRangeOverride) {
  224. if let correctionRangeOverrides = self.viewModel.correctionRangeOverrides,
  225. let schedule = self.viewModel.glucoseTargetRangeSchedule
  226. {
  227. SectionDivider()
  228. CorrectionRangeOverridesRangeItem(
  229. value: correctionRangeOverrides,
  230. displayGlucoseUnit: glucoseUnit,
  231. preset: CorrectionRangeOverrides.Preset.workout,
  232. suspendThreshold: self.viewModel.suspendThreshold,
  233. correctionRangeScheduleRange: schedule.scheduleRange()
  234. )
  235. }
  236. }
  237. }
  238. private var basalRatesSection: Card {
  239. card(for: .basalRate) {
  240. if let schedule = viewModel.therapySettings.basalRateSchedule,
  241. let supportedBasalRates = viewModel.pumpSupportedIncrements()?.basalRates
  242. {
  243. let items = schedule.items
  244. let total = schedule.total()
  245. SectionDivider()
  246. ForEach(items.indices, id: \.self) { index in
  247. if index > 0 {
  248. SettingsDivider()
  249. }
  250. ScheduleValueItem(time: items[index].startTime,
  251. value: items[index].value,
  252. unit: .internationalUnitsPerHour,
  253. guardrail: .basalRate(supportedBasalRates: supportedBasalRates))
  254. }
  255. SectionDivider()
  256. HStack {
  257. Text(NSLocalizedString("Total", comment: "The text indicating Total for Daily Schedule Basal"))
  258. .bold()
  259. .foregroundColor(.primary)
  260. Spacer()
  261. Text(String(format: "%.2f ",total))
  262. .foregroundColor(.primary) +
  263. Text(NSLocalizedString("U/day", comment: "The text indicating U/day for Daily Schedule Basal"))
  264. .foregroundColor(.secondary)
  265. }
  266. }
  267. }
  268. }
  269. private var deliveryLimitsSection: Card {
  270. card(for: .deliveryLimits) {
  271. SectionDivider()
  272. self.maxBasalRateItem
  273. SettingsDivider()
  274. self.maxBolusItem
  275. }
  276. }
  277. private var maxBasalRateItem: some View {
  278. HStack {
  279. Text(DeliveryLimits.Setting.maximumBasalRate.title)
  280. Spacer()
  281. if let basalRates = self.viewModel.pumpSupportedIncrements()?.basalRates {
  282. GuardrailConstrainedQuantityView(
  283. value: self.viewModel.therapySettings.maximumBasalRatePerHour.map { HKQuantity(unit: .internationalUnitsPerHour, doubleValue: $0) },
  284. unit: .internationalUnitsPerHour,
  285. guardrail: .maximumBasalRate(
  286. supportedBasalRates: basalRates,
  287. scheduledBasalRange: self.viewModel.therapySettings.basalRateSchedule?.valueRange(),
  288. lowestCarbRatio: self.viewModel.therapySettings.carbRatioSchedule?.lowestValue()),
  289. isEditing: false,
  290. // Workaround for strange animation behavior on appearance
  291. forceDisableAnimations: true
  292. )
  293. }
  294. }
  295. .accessibilityElement(children: .combine)
  296. }
  297. private var maxBolusItem: some View {
  298. HStack {
  299. Text(DeliveryLimits.Setting.maximumBolus.title)
  300. Spacer()
  301. if let maximumBolusVolumes = self.viewModel.pumpSupportedIncrements()?.maximumBolusVolumes {
  302. GuardrailConstrainedQuantityView(
  303. value: self.viewModel.therapySettings.maximumBolus.map { HKQuantity(unit: .internationalUnit(), doubleValue: $0) },
  304. unit: .internationalUnit(),
  305. guardrail: .maximumBolus(supportedBolusVolumes: maximumBolusVolumes),
  306. isEditing: false,
  307. // Workaround for strange animation behavior on appearance
  308. forceDisableAnimations: true
  309. )
  310. }
  311. }
  312. .accessibilityElement(children: .combine)
  313. }
  314. private var insulinModelSection: Card {
  315. card(for: .insulinModel) {
  316. if let insulinModelPreset = self.viewModel.therapySettings.defaultRapidActingModel {
  317. SectionDivider()
  318. HStack {
  319. // Spacing and paddings here is my best guess based on the design...
  320. VStack(alignment: .leading, spacing: 4) {
  321. Text(insulinModelPreset.title)
  322. .font(.body)
  323. .padding(.top, 5)
  324. .fixedSize(horizontal: false, vertical: true)
  325. Text(insulinModelPreset.subtitle)
  326. .font(.footnote)
  327. .foregroundColor(.secondary)
  328. .padding(.bottom, 8)
  329. .fixedSize(horizontal: false, vertical: true)
  330. }
  331. Spacer()
  332. Image(systemName: "checkmark")
  333. .font(Font.system(.title2).weight(.semibold))
  334. .foregroundColor(.accentColor)
  335. }
  336. .accessibilityElement(children: .combine)
  337. }
  338. }
  339. }
  340. private var carbRatioSection: Card {
  341. card(for: .carbRatio) {
  342. if let items = viewModel.therapySettings.carbRatioSchedule?.items {
  343. SectionDivider()
  344. ForEach(items.indices, id: \.self) { index in
  345. if index > 0 {
  346. SettingsDivider()
  347. }
  348. ScheduleValueItem(time: items[index].startTime,
  349. value: items[index].value,
  350. unit: .gramsPerUnit,
  351. guardrail: .carbRatio)
  352. }
  353. }
  354. }
  355. }
  356. private var insulinSensitivitiesSection: Card {
  357. card(for: .insulinSensitivity) {
  358. if let items = viewModel.insulinSensitivitySchedule(for: glucoseUnit)?.items {
  359. SectionDivider()
  360. ForEach(items.indices, id: \.self) { index in
  361. if index > 0 {
  362. SettingsDivider()
  363. }
  364. ScheduleValueItem(time: items[index].startTime,
  365. value: items[index].value,
  366. unit: sensitivityUnit,
  367. guardrail: .insulinSensitivity)
  368. }
  369. }
  370. }
  371. }
  372. private var supportSection: some View {
  373. Section {
  374. NavigationLink(destination: Text("Therapy Settings Support Placeholder")) {
  375. HStack {
  376. Text("Get help with Therapy Settings", comment: "Support button for Therapy Settings")
  377. .foregroundColor(.primary)
  378. Spacer()
  379. Disclosure()
  380. }
  381. }
  382. }
  383. .contentShape(Rectangle())
  384. }
  385. }
  386. // MARK: Navigation
  387. extension TherapySettingsView {
  388. func screen(for setting: TherapySetting) -> (_ dismiss: @escaping () -> Void) -> AnyView {
  389. switch setting {
  390. case .suspendThreshold:
  391. return { dismiss in
  392. AnyView(SuspendThresholdEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss).environment(\.dismissAction, dismiss))
  393. }
  394. case .glucoseTargetRange:
  395. return { dismiss in
  396. AnyView(CorrectionRangeScheduleEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss).environment(\.dismissAction, dismiss))
  397. }
  398. case .preMealCorrectionRangeOverride:
  399. return { dismiss in
  400. AnyView(CorrectionRangeOverridesEditor(mode: mode, therapySettingsViewModel: viewModel, preset: .preMeal, didSave: dismiss).environment(\.dismissAction, dismiss))
  401. }
  402. case .workoutCorrectionRangeOverride:
  403. return { dismiss in
  404. AnyView(CorrectionRangeOverridesEditor(mode: mode, therapySettingsViewModel: viewModel, preset: .workout, didSave: dismiss).environment(\.dismissAction, dismiss))
  405. }
  406. case .basalRate:
  407. return { dismiss in
  408. AnyView(BasalRateScheduleEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss).environment(\.dismissAction, dismiss))
  409. }
  410. case .deliveryLimits:
  411. return { dismiss in
  412. AnyView(DeliveryLimitsEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss).environment(\.dismissAction, dismiss))
  413. }
  414. case .insulinModel:
  415. return { dismiss in
  416. AnyView(InsulinModelSelection(mode: mode, therapySettingsViewModel: viewModel, chartColors: chartColorPalette, didSave: dismiss).environment(\.dismissAction, dismiss))
  417. }
  418. case .carbRatio:
  419. return { dismiss in
  420. AnyView(CarbRatioScheduleEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss).environment(\.dismissAction, dismiss))
  421. }
  422. case .insulinSensitivity:
  423. return { dismiss in
  424. AnyView(InsulinSensitivityScheduleEditor(mode: mode, therapySettingsViewModel: viewModel, didSave: dismiss).environment(\.dismissAction, dismiss))
  425. }
  426. case .none:
  427. break
  428. }
  429. return { _ in AnyView(Text("\(setting.title)")) }
  430. }
  431. }
  432. // MARK: Utilities
  433. extension TherapySettingsView {
  434. private var glucoseUnit: HKUnit {
  435. displayGlucoseUnitObservable.displayGlucoseUnit
  436. }
  437. private var sensitivityUnit: HKUnit {
  438. glucoseUnit.unitDivided(by: .internationalUnit())
  439. }
  440. private func card<Content>(for therapySetting: TherapySetting, @ViewBuilder content: @escaping () -> Content) -> Card where Content: View {
  441. Card {
  442. SectionWithTapToEdit(isEnabled: mode != .acceptanceFlow,
  443. title: therapySetting.title,
  444. descriptiveText: therapySetting.descriptiveText(appName: appName),
  445. destination: screen(for: therapySetting),
  446. content: content)
  447. }
  448. }
  449. }
  450. typealias HKQuantityGuardrail = Guardrail<HKQuantity>
  451. struct ScheduleRangeItem: View {
  452. let time: TimeInterval
  453. let range: DoubleRange
  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. GuardrailConstrainedQuantityRangeView(range: range.quantityRange(for: unit), unit: unit, guardrail: guardrail, isEditing: false)
  461. .padding(.leading, 10)
  462. },
  463. expandedContent: { EmptyView() })
  464. }
  465. }
  466. struct ScheduleValueItem: View {
  467. let time: TimeInterval
  468. let value: Double
  469. let unit: HKUnit
  470. let guardrail: HKQuantityGuardrail
  471. public var body: some View {
  472. ScheduleItemView(time: time,
  473. isEditing: .constant(false),
  474. valueContent: {
  475. GuardrailConstrainedQuantityView(value: HKQuantity(unit: unit, doubleValue: value), unit: unit, guardrail: guardrail, isEditing: false)
  476. .padding(.leading, 10)
  477. },
  478. expandedContent: { EmptyView() })
  479. }
  480. }
  481. struct CorrectionRangeOverridesRangeItem: View {
  482. let value: CorrectionRangeOverrides
  483. let displayGlucoseUnit: HKUnit
  484. let preset: CorrectionRangeOverrides.Preset
  485. let suspendThreshold: GlucoseThreshold?
  486. let correctionRangeScheduleRange: ClosedRange<HKQuantity>
  487. public var body: some View {
  488. CorrectionRangeOverridesExpandableSetting(
  489. isEditing: .constant(false),
  490. value: .constant(value),
  491. preset: preset,
  492. unit: displayGlucoseUnit,
  493. suspendThreshold: suspendThreshold,
  494. correctionRangeScheduleRange: correctionRangeScheduleRange,
  495. expandedContent: { EmptyView() })
  496. }
  497. }
  498. struct SectionWithTapToEdit<Content, NavigationDestination>: View where Content: View, NavigationDestination: View {
  499. let isEnabled: Bool
  500. let title: String
  501. let descriptiveText: String
  502. let destination: (_ goBack: @escaping () -> Void) -> NavigationDestination
  503. let content: Content
  504. @State var isActive: Bool = false
  505. init(isEnabled: Bool,
  506. title: String,
  507. descriptiveText: String,
  508. destination: @escaping (@escaping () -> Void) -> NavigationDestination,
  509. content: () -> Content)
  510. {
  511. self.isEnabled = isEnabled
  512. self.title = title
  513. self.descriptiveText = descriptiveText
  514. self.destination = destination
  515. self.content = content()
  516. }
  517. private func onFinish() {
  518. // 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
  519. // Added a delay, since recently a similar issue was encountered in a plugin where a delay was also needed. Still uncertain why.
  520. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  521. self.isActive = false
  522. }
  523. }
  524. public var body: some View {
  525. Section {
  526. VStack(alignment: .leading) {
  527. Text(title)
  528. .bold()
  529. Spacer()
  530. HStack {
  531. DescriptiveText(label: descriptiveText)
  532. .fixedSize(horizontal: false, vertical: true)
  533. Spacer()
  534. if isEnabled {
  535. NavigationLink(destination: destination(onFinish), isActive: $isActive) {
  536. Disclosure()
  537. }
  538. .frame(width: 10, alignment: .trailing)
  539. }
  540. }
  541. Spacer()
  542. }
  543. content
  544. }
  545. .contentShape(Rectangle()) // make the whole card tappable
  546. .highPriorityGesture(
  547. TapGesture()
  548. .onEnded { _ in
  549. self.isActive = true
  550. })
  551. }
  552. }
  553. // MARK: Previews
  554. public struct TherapySettingsView_Previews: PreviewProvider {
  555. static let preview_glucoseScheduleItems = [
  556. RepeatingScheduleValue(startTime: 0, value: DoubleRange(80...90)),
  557. RepeatingScheduleValue(startTime: 1800, value: DoubleRange(90...100)),
  558. RepeatingScheduleValue(startTime: 3600, value: DoubleRange(100...110))
  559. ]
  560. static let preview_therapySettings = TherapySettings(
  561. glucoseTargetRangeSchedule: GlucoseRangeSchedule(unit: .milligramsPerDeciliter, dailyItems: preview_glucoseScheduleItems),
  562. correctionRangeOverrides: CorrectionRangeOverrides(preMeal: DoubleRange(88...99),
  563. workout: DoubleRange(99...111),
  564. unit: .milligramsPerDeciliter),
  565. maximumBolus: 4,
  566. suspendThreshold: GlucoseThreshold.init(unit: .milligramsPerDeciliter, value: 60),
  567. insulinSensitivitySchedule: InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter.unitDivided(by: HKUnit.internationalUnit()), dailyItems: []),
  568. carbRatioSchedule: nil,
  569. basalRateSchedule: BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: 0, value: 0.2), RepeatingScheduleValue(startTime: 1800, value: 0.75)]))
  570. static let preview_supportedBasalRates = [0.2, 0.5, 0.75, 1.0]
  571. static let preview_supportedBolusVolumes = [1.0, 2.0, 3.0]
  572. static let preview_supportedMaximumBolusVolumes = [5.0, 10.0, 15.0]
  573. static func preview_viewModel() -> TherapySettingsViewModel {
  574. TherapySettingsViewModel(therapySettings: preview_therapySettings,
  575. pumpSupportedIncrements: {
  576. PumpSupportedIncrements(basalRates: preview_supportedBasalRates,
  577. bolusVolumes: preview_supportedBolusVolumes,
  578. maximumBolusVolumes: preview_supportedMaximumBolusVolumes,
  579. maximumBasalScheduleEntryCount: 24) })
  580. }
  581. public static var previews: some View {
  582. Group {
  583. TherapySettingsView(mode: .acceptanceFlow, viewModel: preview_viewModel())
  584. .colorScheme(.light)
  585. .previewDevice(PreviewDevice(rawValue: "iPhone SE 2"))
  586. .previewDisplayName("SE light (onboarding)")
  587. .environmentObject(DisplayGlucoseUnitObservable(displayGlucoseUnit: .milligramsPerDeciliter))
  588. TherapySettingsView(mode: .settings, viewModel: preview_viewModel())
  589. .colorScheme(.light)
  590. .previewDevice(PreviewDevice(rawValue: "iPhone SE 2"))
  591. .previewDisplayName("SE light (settings)")
  592. .environmentObject(DisplayGlucoseUnitObservable(displayGlucoseUnit: .milligramsPerDeciliter))
  593. TherapySettingsView(mode: .settings, viewModel: preview_viewModel())
  594. .colorScheme(.dark)
  595. .previewDevice(PreviewDevice(rawValue: "iPhone XS Max"))
  596. .previewDisplayName("XS Max dark (settings)")
  597. TherapySettingsView(mode: .settings, viewModel: TherapySettingsViewModel(therapySettings: TherapySettings()))
  598. .colorScheme(.light)
  599. .previewDevice(PreviewDevice(rawValue: "iPhone SE 2"))
  600. .previewDisplayName("SE light (Empty TherapySettings)")
  601. .environmentObject(DisplayGlucoseUnitObservable(displayGlucoseUnit: .millimolesPerLiter))
  602. }
  603. }
  604. }
  605. fileprivate struct SectionDivider: View {
  606. var body: some View {
  607. Divider()
  608. .padding(.trailing, -16)
  609. }
  610. }
  611. fileprivate struct SettingsDivider: View {
  612. var body: some View {
  613. Divider()
  614. .padding(.trailing, -8)
  615. }
  616. }
  617. fileprivate struct Disclosure: View {
  618. var body: some View {
  619. Image(systemName: "chevron.right")
  620. .imageScale(.small)
  621. .font(.headline)
  622. .foregroundColor(.secondary)
  623. .opacity(0.5)
  624. }
  625. }