QuickBolusView.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import SwiftUI
  2. struct QuickBolusView: View {
  3. let suggestions: [Decimal]
  4. let onEnact: (Decimal) async -> Bool
  5. @Binding var isPresented: Bool
  6. @State private var selectedAmount: Decimal?
  7. @State private var showInfo = false
  8. @State private var isEnacting = false
  9. @State private var showAuthFailedAlert = false
  10. var body: some View {
  11. NavigationStack {
  12. VStack(spacing: 20) {
  13. pillRow
  14. .padding(.top, 8)
  15. Text(
  16. "Your most-used bolus amounts at similar times on similar days. Tap one to pick it.",
  17. comment: "Subtitle of the quick bolus pill row"
  18. )
  19. .font(.subheadline)
  20. .foregroundStyle(.secondary)
  21. .multilineTextAlignment(.center)
  22. .padding(.horizontal)
  23. }
  24. .frame(maxWidth: .infinity)
  25. .safeAreaInset(edge: .bottom, spacing: 0) {
  26. SlideToConfirmView(
  27. label: String(localized: "Slide to Enact Bolus", comment: "Slide to confirm label for quick bolus"),
  28. isEnabled: selectedAmount != nil && !isEnacting
  29. ) {
  30. guard let amount = selectedAmount, !isEnacting else { return }
  31. isEnacting = true
  32. Task {
  33. let success = await onEnact(amount)
  34. await MainActor.run {
  35. if success {
  36. isPresented = false
  37. } else {
  38. isEnacting = false
  39. showAuthFailedAlert = true
  40. }
  41. }
  42. }
  43. }
  44. .padding(.horizontal)
  45. .padding(.vertical, 12)
  46. }
  47. .navigationTitle(String(localized: "Quick Bolus", comment: "Title of the quick bolus sheet"))
  48. .navigationBarTitleDisplayMode(.inline)
  49. .toolbar {
  50. ToolbarItem(placement: .topBarTrailing) {
  51. Button {
  52. showInfo = true
  53. } label: {
  54. Image(systemName: "info.circle")
  55. }
  56. }
  57. }
  58. .sheet(isPresented: $showInfo) {
  59. QuickBolusInfoView(isPresented: $showInfo)
  60. }
  61. .alert(
  62. String(localized: "Could not authenticate", comment: "Alert title when biometric auth fails for quick bolus"),
  63. isPresented: $showAuthFailedAlert
  64. ) {
  65. Button(String(localized: "OK"), role: .cancel) {}
  66. } message: {
  67. Text(String(
  68. localized: "Face ID or Touch ID did not succeed. The bolus was not enacted.",
  69. comment: "Alert body when biometric auth fails for quick bolus"
  70. ))
  71. }
  72. }
  73. .presentationDetents([.height(320)])
  74. }
  75. private var displayedSuggestions: [Decimal] {
  76. Array(suggestions.prefix(3).sorted())
  77. }
  78. private var pillRow: some View {
  79. HStack(spacing: 12) {
  80. ForEach(displayedSuggestions, id: \.self) { amount in
  81. bolusAmountPill(amount)
  82. }
  83. }
  84. .padding(.horizontal)
  85. }
  86. private func bolusAmountPill(_ amount: Decimal) -> some View {
  87. let isSelected = selectedAmount == amount
  88. let formatted = Formatter.bolusFormatter.string(from: amount as NSDecimalNumber) ?? amount.description
  89. return Button {
  90. selectedAmount = amount
  91. } label: {
  92. HStack(alignment: .firstTextBaseline, spacing: 2) {
  93. Text(formatted)
  94. .font(.title2.bold())
  95. Text("U")
  96. .font(.callout)
  97. .foregroundStyle(isSelected ? .white.opacity(0.85) : .secondary)
  98. }
  99. .frame(maxWidth: .infinity, minHeight: 64)
  100. .background(isSelected ? Color.accentColor : Color(.secondarySystemFill))
  101. .foregroundStyle(isSelected ? .white : .primary)
  102. .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
  103. .overlay {
  104. if isSelected {
  105. RoundedRectangle(cornerRadius: 18, style: .continuous)
  106. .strokeBorder(Color.accentColor, lineWidth: 2.5)
  107. }
  108. }
  109. }
  110. .buttonStyle(.plain)
  111. .animation(.easeInOut(duration: 0.15), value: isSelected)
  112. }
  113. }