OmnipodSettingsView.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. //
  2. // OmnipodSettingsView.swift
  3. // ViewDev
  4. //
  5. // Created by Pete Schwamb on 3/8/20.
  6. // Copyright © 2020 Pete Schwamb. All rights reserved.
  7. //
  8. import SwiftUI
  9. import LoopKit
  10. import LoopKitUI
  11. import HealthKit
  12. import OmniKit
  13. import RileyLinkBLEKit
  14. struct OmnipodSettingsView: View {
  15. @ObservedObject var viewModel: OmnipodSettingsViewModel
  16. @ObservedObject var rileyLinkListDataSource: RileyLinkListDataSource
  17. var handleRileyLinkSelection: (RileyLinkDevice) -> Void
  18. @State private var showingDeleteConfirmation = false
  19. @State private var showSuspendOptions = false
  20. @State private var showManualTempBasalOptions = false
  21. @State private var showSyncTimeOptions = false
  22. @State private var sendingTestBeepsCommand = false
  23. @State private var cancelingTempBasal = false
  24. var supportedInsulinTypes: [InsulinType]
  25. @Environment(\.guidanceColors) var guidanceColors
  26. @Environment(\.insulinTintColor) var insulinTintColor
  27. private var daysRemaining: Int? {
  28. if case .timeRemaining(let remaining, _) = viewModel.lifeState, remaining > .days(1) {
  29. return Int(remaining.days)
  30. }
  31. return nil
  32. }
  33. private var hoursRemaining: Int? {
  34. if case .timeRemaining(let remaining, _) = viewModel.lifeState, remaining > .hours(1) {
  35. return Int(remaining.hours.truncatingRemainder(dividingBy: 24))
  36. }
  37. return nil
  38. }
  39. private var minutesRemaining: Int? {
  40. if case .timeRemaining(let remaining, _) = viewModel.lifeState, remaining < .hours(2) {
  41. return Int(remaining.minutes.truncatingRemainder(dividingBy: 60))
  42. }
  43. return nil
  44. }
  45. func timeComponent(value: Int, units: String) -> some View {
  46. Group {
  47. Text(String(value)).font(.system(size: 28)).fontWeight(.heavy)
  48. .foregroundColor(viewModel.podOk ? .primary : .secondary)
  49. Text(units).foregroundColor(.secondary)
  50. }
  51. }
  52. var lifecycleProgress: some View {
  53. VStack(spacing: 2) {
  54. HStack(alignment: .lastTextBaseline, spacing: 3) {
  55. Text(self.viewModel.lifeState.localizedLabelText)
  56. .foregroundColor(self.viewModel.lifeState.labelColor(using: guidanceColors))
  57. Spacer()
  58. daysRemaining.map { (days) in
  59. timeComponent(value: days, units: days == 1 ?
  60. LocalizedString("day", comment: "Unit for singular day in pod life remaining") :
  61. LocalizedString("days", comment: "Unit for plural days in pod life remaining"))
  62. }
  63. hoursRemaining.map { (hours) in
  64. timeComponent(value: hours, units: hours == 1 ?
  65. LocalizedString("hour", comment: "Unit for singular hour in pod life remaining") :
  66. LocalizedString("hours", comment: "Unit for plural hours in pod life remaining"))
  67. }
  68. minutesRemaining.map { (minutes) in
  69. timeComponent(value: minutes, units: minutes == 1 ?
  70. LocalizedString("minute", comment: "Unit for singular minute in pod life remaining") :
  71. LocalizedString("minutes", comment: "Unit for plural minutes in pod life remaining"))
  72. }
  73. }
  74. ProgressView(progress: CGFloat(self.viewModel.lifeState.progress)).accentColor(self.viewModel.lifeState.progressColor(guidanceColors: guidanceColors))
  75. }
  76. }
  77. func cancelDelete() {
  78. showingDeleteConfirmation = false
  79. }
  80. var deliverySectionTitle: String {
  81. if self.viewModel.isScheduledBasal {
  82. return LocalizedString("Scheduled Basal", comment: "Title of insulin delivery section")
  83. } else {
  84. return LocalizedString("Insulin Delivery", comment: "Title of insulin delivery section")
  85. }
  86. }
  87. var deliveryStatus: some View {
  88. VStack(alignment: .leading, spacing: 5) {
  89. Text(deliverySectionTitle)
  90. .foregroundColor(Color(UIColor.secondaryLabel))
  91. if viewModel.podOk, viewModel.isSuspendedOrResuming {
  92. HStack(alignment: .center) {
  93. Image(systemName: "pause.circle.fill")
  94. .font(.system(size: 34))
  95. .fixedSize()
  96. .foregroundColor(viewModel.suspendResumeButtonColor(guidanceColors: guidanceColors))
  97. FrameworkLocalText("Insulin\nSuspended", comment: "Text shown in insulin delivery space when insulin suspended")
  98. .fontWeight(.bold)
  99. .fixedSize()
  100. }
  101. } else if let basalRate = self.viewModel.basalDeliveryRate {
  102. HStack(alignment: .center) {
  103. HStack(alignment: .lastTextBaseline, spacing: 3) {
  104. Text(self.viewModel.basalRateFormatter.string(from: basalRate) ?? "")
  105. .font(.system(size: 28))
  106. .fontWeight(.heavy)
  107. .fixedSize()
  108. FrameworkLocalText("U/hr", comment: "Units for showing temp basal rate").foregroundColor(.secondary)
  109. }
  110. }
  111. } else {
  112. HStack(alignment: .center) {
  113. Image(systemName: "x.circle.fill")
  114. .font(.system(size: 34))
  115. .fixedSize()
  116. .foregroundColor(guidanceColors.critical)
  117. FrameworkLocalText("No\nDelivery", comment: "Text shown in insulin remaining space when no pod is paired")
  118. .fontWeight(.bold)
  119. .fixedSize()
  120. }
  121. }
  122. }
  123. }
  124. func reservoir(filledPercent: CGFloat, fillColor: Color) -> some View {
  125. ZStack(alignment: Alignment(horizontal: .center, vertical: .center)) {
  126. GeometryReader { geometry in
  127. let offset = geometry.size.height * 0.05
  128. let fillHeight = geometry.size.height * 0.81
  129. Rectangle()
  130. .fill(fillColor)
  131. .mask(
  132. Image(frameworkImage: "pod_reservoir_mask_swiftui")
  133. .resizable()
  134. .scaledToFit()
  135. )
  136. .mask(
  137. Rectangle().path(in: CGRect(x: 0, y: offset + fillHeight - fillHeight * filledPercent, width: geometry.size.width, height: fillHeight * filledPercent))
  138. )
  139. }
  140. Image(frameworkImage: "pod_reservoir_swiftui")
  141. .renderingMode(.template)
  142. .resizable()
  143. .foregroundColor(fillColor)
  144. .scaledToFit()
  145. }.frame(width: 23, height: 32)
  146. }
  147. var reservoirStatus: some View {
  148. VStack(alignment: .leading, spacing: 5) {
  149. Text(LocalizedString("Insulin Remaining", comment: "Header for insulin remaining on pod settings screen"))
  150. .foregroundColor(Color(UIColor.secondaryLabel))
  151. HStack {
  152. if let podError = viewModel.podError {
  153. Image(systemName: "exclamationmark.circle.fill")
  154. .font(.system(size: 34))
  155. .fixedSize()
  156. .foregroundColor(guidanceColors.critical)
  157. Text(podError).fontWeight(.bold)
  158. } else if let reservoirLevel = viewModel.reservoirLevel, let reservoirLevelHighlightState = viewModel.reservoirLevelHighlightState {
  159. reservoir(filledPercent: CGFloat(reservoirLevel.percentage), fillColor: reservoirColor(for: reservoirLevelHighlightState))
  160. Text(viewModel.reservoirText(for: reservoirLevel))
  161. .font(.system(size: 28))
  162. .fontWeight(.heavy)
  163. .fixedSize()
  164. } else {
  165. Image(systemName: "exclamationmark.circle.fill")
  166. .font(.system(size: 34))
  167. .fixedSize()
  168. .foregroundColor(guidanceColors.warning)
  169. FrameworkLocalText("No Pod", comment: "Text shown in insulin remaining space when no pod is paired").fontWeight(.bold)
  170. }
  171. }
  172. }
  173. }
  174. var manualTempBasalRow: some View {
  175. Button(action: {
  176. self.manualBasalTapped()
  177. }) {
  178. FrameworkLocalText("Set Temporary Basal Rate", comment: "Button title to set temporary basal rate")
  179. }
  180. .sheet(isPresented: $showManualTempBasalOptions) {
  181. ManualTempBasalEntryView(
  182. enactBasal: { rate, duration, completion in
  183. viewModel.runTemporaryBasalProgram(unitsPerHour: rate, for: duration) { error in
  184. completion(error)
  185. if error == nil {
  186. showManualTempBasalOptions = false
  187. }
  188. }
  189. },
  190. didCancel: {
  191. showManualTempBasalOptions = false
  192. },
  193. allowedRates: viewModel.allowedTempBasalRates
  194. )
  195. }
  196. }
  197. func suspendResumeRow() -> some View {
  198. HStack {
  199. Button(action: {
  200. self.suspendResumeTapped()
  201. }) {
  202. HStack {
  203. Image(systemName: "pause.circle.fill")
  204. .font(.system(size: 22))
  205. .foregroundColor(viewModel.suspendResumeButtonColor(guidanceColors: guidanceColors))
  206. Text(viewModel.suspendResumeActionText)
  207. .foregroundColor(viewModel.suspendResumeActionColor())
  208. }
  209. }
  210. .actionSheet(isPresented: $showSuspendOptions) {
  211. suspendOptionsActionSheet
  212. }
  213. Spacer()
  214. if viewModel.basalTransitioning {
  215. ActivityIndicator(isAnimating: .constant(true), style: .medium)
  216. }
  217. }
  218. }
  219. private var doneButton: some View {
  220. Button(LocalizedString("Done", comment: "Title of done button on OmnipodSettingsView"), action: {
  221. self.viewModel.doneTapped()
  222. })
  223. }
  224. var headerImage: some View {
  225. VStack(alignment: .center) {
  226. Image(frameworkImage: "Pod")
  227. .resizable()
  228. .aspectRatio(contentMode: ContentMode.fit)
  229. .frame(height: 100)
  230. .padding(.horizontal)
  231. }.frame(maxWidth: .infinity)
  232. }
  233. var body: some View {
  234. List {
  235. Section() {
  236. VStack(alignment: .trailing) {
  237. Button(action: {
  238. sendingTestBeepsCommand = true
  239. viewModel.playTestBeeps { _ in
  240. sendingTestBeepsCommand = false
  241. }
  242. }) {
  243. Image(systemName: "speaker.wave.2.circle")
  244. .imageScale(.large)
  245. .foregroundColor(viewModel.rileylinkConnected ? .accentColor : .secondary)
  246. .padding(.top,5)
  247. }
  248. .buttonStyle(PlainButtonStyle())
  249. .disabled(!viewModel.rileylinkConnected || sendingTestBeepsCommand)
  250. headerImage
  251. lifecycleProgress
  252. HStack(alignment: .top) {
  253. deliveryStatus
  254. Spacer()
  255. reservoirStatus
  256. }
  257. if let faultAction = viewModel.recoveryText {
  258. Divider()
  259. Text(faultAction)
  260. .font(Font.footnote.weight(.semibold))
  261. .fixedSize(horizontal: false, vertical: true)
  262. .frame(maxWidth: .infinity, alignment: .leading)
  263. }
  264. }
  265. if let notice = viewModel.notice {
  266. VStack(alignment: .leading, spacing: 4) {
  267. Text(notice.title)
  268. .font(Font.subheadline.weight(.bold))
  269. Text(notice.description)
  270. .font(Font.footnote.weight(.semibold))
  271. }.padding(.vertical, 8)
  272. }
  273. }
  274. Section(header: SectionHeader(label: LocalizedString("Activity", comment: "Section header for activity section"))) {
  275. suspendResumeRow()
  276. .disabled(!self.viewModel.podOk)
  277. if self.viewModel.podOk, case .suspended(let suspendDate) = self.viewModel.basalDeliveryState {
  278. HStack {
  279. FrameworkLocalText("Suspended At", comment: "Label for suspended at time")
  280. Spacer()
  281. Text(self.viewModel.timeFormatter.string(from: suspendDate))
  282. .foregroundColor(Color.secondary)
  283. }
  284. }
  285. }
  286. Section() {
  287. if let manualTempRemaining = self.viewModel.manualBasalTimeRemaining, let remainingText = self.viewModel.timeRemainingFormatter.string(from: manualTempRemaining) {
  288. HStack {
  289. if cancelingTempBasal {
  290. ProgressView()
  291. .padding(.trailing)
  292. } else {
  293. Image(systemName: "exclamationmark.circle.fill")
  294. .font(.system(size: 22))
  295. .foregroundColor(guidanceColors.warning)
  296. }
  297. Button(action: {
  298. self.cancelManualBasal()
  299. }) {
  300. FrameworkLocalText("Cancel Manual Basal", comment: "Button title to cancel manual basal")
  301. }
  302. }
  303. HStack {
  304. FrameworkLocalText("Remaining", comment: "Label for remaining time of manual basal")
  305. Spacer()
  306. Text(remainingText)
  307. .foregroundColor(.secondary)
  308. }
  309. } else {
  310. manualTempBasalRow
  311. }
  312. }
  313. .disabled(cancelingTempBasal || !self.viewModel.podOk)
  314. Section(header: HStack {
  315. FrameworkLocalText("Devices", comment: "Header for devices section of RileyLinkSetupView")
  316. Spacer()
  317. ProgressView()
  318. }) {
  319. ForEach(rileyLinkListDataSource.devices, id: \.peripheralIdentifier) { device in
  320. Toggle(isOn: rileyLinkListDataSource.autoconnectBinding(for: device)) {
  321. HStack {
  322. Text(device.name ?? "Unknown")
  323. Spacer()
  324. if rileyLinkListDataSource.autoconnectBinding(for: device).wrappedValue {
  325. if device.isConnected {
  326. Text(formatRSSI(rssi:device.rssi)).foregroundColor(.secondary)
  327. } else {
  328. Image(systemName: "wifi.exclamationmark")
  329. .imageScale(.large)
  330. .foregroundColor(guidanceColors.warning)
  331. }
  332. }
  333. }
  334. .contentShape(Rectangle())
  335. .onTapGesture {
  336. handleRileyLinkSelection(device)
  337. }
  338. }
  339. }
  340. }
  341. .onAppear { rileyLinkListDataSource.isScanningEnabled = true }
  342. .onDisappear { rileyLinkListDataSource.isScanningEnabled = false }
  343. Section() {
  344. HStack {
  345. FrameworkLocalText("Pod Activated", comment: "Label for pod insertion row")
  346. Spacer()
  347. Text(self.viewModel.activatedAtString)
  348. .foregroundColor(Color.secondary)
  349. }
  350. HStack {
  351. if let expiresAt = viewModel.expiresAt, expiresAt < Date() {
  352. FrameworkLocalText("Pod Expired", comment: "Label for pod expiration row, past tense")
  353. } else {
  354. FrameworkLocalText("Pod Expires", comment: "Label for pod expiration row")
  355. }
  356. Spacer()
  357. Text(self.viewModel.expiresAtString)
  358. .foregroundColor(Color.secondary)
  359. }
  360. if let podDetails = self.viewModel.podDetails {
  361. NavigationLink(destination: PodDetailsView(podDetails: podDetails, title: LocalizedString("Pod Details", comment: "title for pod details page"))) {
  362. FrameworkLocalText("Pod Details", comment: "Text for pod details disclosure row")
  363. .foregroundColor(Color.primary)
  364. }
  365. } else {
  366. HStack {
  367. FrameworkLocalText("Pod Details", comment: "Text for pod details disclosure row")
  368. Spacer()
  369. Text("—")
  370. .foregroundColor(Color.secondary)
  371. }
  372. }
  373. if let previousPodDetails = viewModel.previousPodDetails {
  374. NavigationLink(destination: PodDetailsView(podDetails: previousPodDetails, title: LocalizedString("Previous Pod", comment: "title for previous pod page"))) {
  375. FrameworkLocalText("Previous Pod Details", comment: "Text for previous pod details row")
  376. .foregroundColor(Color.primary)
  377. }
  378. } else {
  379. HStack {
  380. FrameworkLocalText("Previous Pod Details", comment: "Text for previous pod details row")
  381. Spacer()
  382. Text("—")
  383. .foregroundColor(Color.secondary)
  384. }
  385. }
  386. }
  387. Section() {
  388. Button(action: {
  389. self.viewModel.navigateTo?(self.viewModel.lifeState.nextPodLifecycleAction)
  390. }) {
  391. Text(self.viewModel.lifeState.nextPodLifecycleActionDescription)
  392. .foregroundColor(self.viewModel.lifeState.nextPodLifecycleActionColor)
  393. }
  394. }
  395. Section(header: SectionHeader(label: LocalizedString("Configuration", comment: "Section header for configuration section")))
  396. {
  397. NavigationLink(destination:
  398. NotificationSettingsView(
  399. dateFormatter: self.viewModel.dateFormatter,
  400. expirationReminderDefault: self.$viewModel.expirationReminderDefault,
  401. scheduledReminderDate: self.viewModel.expirationReminderDate,
  402. allowedScheduledReminderDates: self.viewModel.allowedScheduledReminderDates,
  403. lowReservoirReminderValue: self.viewModel.lowReservoirAlertValue,
  404. onSaveScheduledExpirationReminder: self.viewModel.saveScheduledExpirationReminder,
  405. onSaveLowReservoirReminder: self.viewModel.saveLowReservoirReminder))
  406. {
  407. FrameworkLocalText("Notification Settings", comment: "Text for pod details disclosure row").foregroundColor(Color.primary)
  408. }
  409. NavigationLink(destination: BeepPreferenceSelectionView(initialValue: viewModel.beepPreference, onSave: viewModel.setConfirmationBeeps)) {
  410. HStack {
  411. FrameworkLocalText("Confidence Reminders", comment: "Text for confidence reminders navigation link")
  412. .foregroundColor(Color.primary)
  413. Spacer()
  414. Text(viewModel.beepPreference.title)
  415. .foregroundColor(.secondary)
  416. }
  417. }
  418. NavigationLink(destination: SilencePodSelectionView(initialValue: viewModel.silencePodPreference, onSave: viewModel.setSilencePod)) {
  419. HStack {
  420. FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link")
  421. .foregroundColor(Color.primary)
  422. Spacer()
  423. Text(viewModel.silencePodPreference.title)
  424. .foregroundColor(.secondary)
  425. }
  426. }
  427. NavigationLink(destination: InsulinTypeSetting(initialValue: viewModel.insulinType, supportedInsulinTypes: supportedInsulinTypes, allowUnsetInsulinType: false, didChange: viewModel.didChangeInsulinType)) {
  428. HStack {
  429. FrameworkLocalText("Insulin Type", comment: "Text for insulin type navigation link").foregroundColor(Color.primary)
  430. if let currentTitle = viewModel.insulinType?.brandName {
  431. Spacer()
  432. Text(currentTitle)
  433. .foregroundColor(.secondary)
  434. }
  435. }
  436. }
  437. }
  438. Section() {
  439. HStack {
  440. FrameworkLocalText("Pump Time", comment: "The title of the command to change pump time zone")
  441. Spacer()
  442. if viewModel.isClockOffset {
  443. Image(systemName: "clock.fill")
  444. .foregroundColor(guidanceColors.warning)
  445. }
  446. TimeView(timeZone: viewModel.timeZone)
  447. .foregroundColor( viewModel.isClockOffset ? guidanceColors.warning : nil)
  448. }
  449. if viewModel.synchronizingTime {
  450. HStack {
  451. FrameworkLocalText("Adjusting Pump Time...", comment: "Text indicating ongoing pump time synchronization")
  452. .foregroundColor(.secondary)
  453. Spacer()
  454. ActivityIndicator(isAnimating: .constant(true), style: .medium)
  455. }
  456. } else if self.viewModel.timeZone != TimeZone.currentFixed {
  457. Button(action: {
  458. showSyncTimeOptions = true
  459. }) {
  460. FrameworkLocalText("Sync to Current Time", comment: "The title of the command to change pump time zone")
  461. }
  462. .actionSheet(isPresented: $showSyncTimeOptions) {
  463. syncPumpTimeActionSheet
  464. }
  465. }
  466. }
  467. Section() {
  468. NavigationLink(destination: PodDiagnosticsView(
  469. title: LocalizedString("Pod Diagnostics", comment: "Title for the pod diagnostic view"),
  470. viewModel: viewModel))
  471. {
  472. FrameworkLocalText("Pod Diagnostics", comment: "Text for pod diagnostics row")
  473. .foregroundColor(Color.primary)
  474. }
  475. }
  476. if self.viewModel.lifeState.allowsPumpManagerRemoval {
  477. Section() {
  478. Button(action: {
  479. self.showingDeleteConfirmation = true
  480. }) {
  481. FrameworkLocalText("Switch to other insulin delivery device", comment: "Label for PumpManager deletion button")
  482. .foregroundColor(guidanceColors.critical)
  483. }
  484. .actionSheet(isPresented: $showingDeleteConfirmation) {
  485. removePumpManagerActionSheet
  486. }
  487. }
  488. }
  489. }
  490. .alert(isPresented: $viewModel.alertIsPresented, content: { alert(for: viewModel.activeAlert!) })
  491. .insetGroupedListStyle()
  492. .navigationBarItems(trailing: doneButton)
  493. .navigationBarTitle(self.viewModel.viewTitle)
  494. }
  495. var syncPumpTimeActionSheet: ActionSheet {
  496. ActionSheet(title: FrameworkLocalText("Time Change Detected", comment: "Title for pod sync time action sheet."), message: FrameworkLocalText("The time on your pump is different from the current time. Do you want to update the time on your pump to the current time?", comment: "Message for pod sync time action sheet"), buttons: [
  497. .default(FrameworkLocalText("Yes, Sync to Current Time", comment: "Button text to confirm pump time sync")) {
  498. self.viewModel.changeTimeZoneTapped()
  499. },
  500. .cancel(FrameworkLocalText("No, Keep Pump As Is", comment: "Button text to cancel pump time sync"))
  501. ])
  502. }
  503. var removePumpManagerActionSheet: ActionSheet {
  504. ActionSheet(title: FrameworkLocalText("Remove Pump", comment: "Title for Omnipod PumpManager deletion action sheet."), message: FrameworkLocalText("Are you sure you want to stop using Omnipod?", comment: "Message for Omnipod PumpManager deletion action sheet"), buttons: [
  505. .destructive(FrameworkLocalText("Delete Omnipod", comment: "Button text to confirm Omnipod PumpManager deletion")) {
  506. self.viewModel.stopUsingOmnipodTapped()
  507. },
  508. .cancel()
  509. ])
  510. }
  511. var suspendOptionsActionSheet: ActionSheet {
  512. ActionSheet(
  513. title: FrameworkLocalText("Suspend Delivery", comment: "Title for suspend duration selection action sheet"),
  514. message: FrameworkLocalText("Insulin delivery will be stopped until you resume manually. When would you like Loop to remind you to resume delivery?", comment: "Message for suspend duration selection action sheet"),
  515. buttons: [
  516. .default(FrameworkLocalText("30 minutes", comment: "Button text for 30 minute suspend duration"), action: { self.viewModel.suspendDelivery(duration: .minutes(30)) }),
  517. .default(FrameworkLocalText("1 hour", comment: "Button text for 1 hour suspend duration"), action: { self.viewModel.suspendDelivery(duration: .hours(1)) }),
  518. .default(FrameworkLocalText("1 hour 30 minutes", comment: "Button text for 1 hour 30 minute suspend duration"), action: { self.viewModel.suspendDelivery(duration: .hours(1.5)) }),
  519. .default(FrameworkLocalText("2 hours", comment: "Button text for 2 hour suspend duration"), action: { self.viewModel.suspendDelivery(duration: .hours(2)) }),
  520. .cancel()
  521. ])
  522. }
  523. func suspendResumeTapped() {
  524. switch self.viewModel.basalDeliveryState {
  525. case .active, .tempBasal:
  526. showSuspendOptions = true
  527. case .suspended:
  528. self.viewModel.resumeDelivery()
  529. default:
  530. break
  531. }
  532. }
  533. func manualBasalTapped() {
  534. showManualTempBasalOptions = true
  535. }
  536. func cancelManualBasal() {
  537. cancelingTempBasal = true
  538. viewModel.runTemporaryBasalProgram(unitsPerHour: 0, for: 0) { error in
  539. cancelingTempBasal = false
  540. if let error = error {
  541. self.viewModel.activeAlert = .cancelManualBasalError(error)
  542. }
  543. }
  544. }
  545. private func errorText(_ error: Error) -> String {
  546. if let error = error as? LocalizedError {
  547. return [error.localizedDescription, error.recoverySuggestion].compactMap{$0}.joined(separator: ". ")
  548. } else {
  549. return error.localizedDescription
  550. }
  551. }
  552. private func alert(for alert: OmnipodSettingsViewAlert) -> SwiftUI.Alert {
  553. switch alert {
  554. case .suspendError(let error):
  555. return SwiftUI.Alert(
  556. title: Text("Failed to Suspend Insulin Delivery", comment: "Alert title for suspend error"),
  557. message: Text(errorText(error))
  558. )
  559. case .resumeError(let error):
  560. return SwiftUI.Alert(
  561. title: Text("Failed to Resume Insulin Delivery", comment: "Alert title for resume error"),
  562. message: Text(errorText(error))
  563. )
  564. case .syncTimeError(let error):
  565. return SwiftUI.Alert(
  566. title: Text("Failed to Set Pump Time", comment: "Alert title for time sync error"),
  567. message: Text(errorText(error))
  568. )
  569. case .cancelManualBasalError(let error):
  570. return SwiftUI.Alert(
  571. title: Text("Failed to Cancel Manual Basal", comment: "Alert title for failing to cancel manual basal error"),
  572. message: Text(errorText(error))
  573. )
  574. }
  575. }
  576. func reservoirColor(for reservoirLevelHighlightState: ReservoirLevelHighlightState) -> Color {
  577. switch reservoirLevelHighlightState {
  578. case .normal:
  579. return insulinTintColor
  580. case .warning:
  581. return guidanceColors.warning
  582. case .critical:
  583. return guidanceColors.critical
  584. }
  585. }
  586. var decimalFormatter: NumberFormatter = {
  587. let formatter = NumberFormatter()
  588. formatter.numberStyle = .decimal
  589. formatter.minimumFractionDigits = 0
  590. formatter.maximumFractionDigits = 2
  591. return formatter
  592. }()
  593. private func formatRSSI(rssi: Int?) -> String {
  594. if let rssi = rssi, let rssiStr = decimalFormatter.decibleString(from: rssi) {
  595. return rssiStr
  596. } else {
  597. return ""
  598. }
  599. }
  600. }