QuickPickBolusesView.swift 5.8 KB

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