BluetoothPermissionStepView.swift 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. //
  2. // BluetoothPermissionStepView.swift
  3. // Trio
  4. //
  5. // Created by Cengiz Deniz on 18.04.25.
  6. //
  7. import CoreBluetooth
  8. import SwiftUI
  9. import UIKit
  10. struct BluetoothPermissionStepView: View {
  11. @Bindable var state: Onboarding.StateModel
  12. var bluetoothManager: BluetoothStateManager
  13. var currentStep: Binding<OnboardingStep>
  14. var body: some View {
  15. VStack(alignment: .leading, spacing: 20) {
  16. Text("Enable device connectivity")
  17. .font(.title3)
  18. .bold()
  19. .multilineTextAlignment(.leading)
  20. Text("Trio requires Bluetooth to function as a (hybrid) closed‑loop system.")
  21. .font(.body)
  22. .multilineTextAlignment(.leading)
  23. .foregroundColor(Color.secondary)
  24. .padding(.bottom)
  25. VStack(alignment: .leading, spacing: 20) {
  26. HStack(spacing: 12) {
  27. Image(systemName: "keyboard.onehanded.left.fill")
  28. .font(.system(size: 24))
  29. .foregroundColor(Color.bgDarkBlue)
  30. .frame(width: 44, height: 44)
  31. .background(Circle().fill(Color.primary.opacity(0.8)))
  32. Text(
  33. "Connect to your insulin pump so Trio can send dosing commands and stay active in the background."
  34. )
  35. .font(.body)
  36. .foregroundColor(.primary)
  37. }
  38. HStack(spacing: 12) {
  39. Image(systemName: "sensor.tag.radiowaves.forward.fill")
  40. .font(.system(size: 24))
  41. .foregroundColor(Color.bgDarkBlue)
  42. .frame(width: 44, height: 44)
  43. .background(Circle().fill(Color.primary.opacity(0.8)))
  44. Text("Receive glucose readings every 5 minutes from your CGM to keep the loop running.")
  45. .font(.body)
  46. .foregroundColor(.primary)
  47. }
  48. }
  49. Text("You can change these permissions any time in the iOS Settings app.")
  50. .font(.footnote)
  51. .multilineTextAlignment(.leading)
  52. .foregroundColor(Color.secondary)
  53. .padding(.top)
  54. }
  55. .padding(.horizontal)
  56. .background(
  57. SystemAlert(
  58. isPresented: $state.shouldDisplayBluetoothRequestAlert,
  59. title: String(localized: "“Trio” Would Like to Use Bluetooth"),
  60. message: String(
  61. localized: "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices."
  62. ),
  63. allowTitle: String(localized: "Allow"),
  64. denyTitle: String(localized: "Don’t Allow"),
  65. onAllow: {
  66. bluetoothManager.authorizeBluetooth { auth in
  67. DispatchQueue.main.async {
  68. state.hasBluetoothGranted = (auth == .authorized)
  69. state.shouldDisplayBluetoothRequestAlert = false
  70. if let next = currentStep.wrappedValue.next {
  71. currentStep.wrappedValue = next
  72. }
  73. }
  74. }
  75. },
  76. onDeny: {
  77. state.hasBluetoothGranted = false
  78. state.shouldDisplayBluetoothRequestAlert = false
  79. if let next = currentStep.wrappedValue.next {
  80. currentStep.wrappedValue = next
  81. }
  82. }
  83. )
  84. )
  85. }
  86. }
  87. /// Presents a real UIAlertController, pinned to the system's own style
  88. ///
  89. /// Why use this?
  90. /// SwiftUI’s built‑in .alert will always inherit the color scheme of its host view (in our case, we have forced .dark for the entire onboarding screen).
  91. /// There’s no way to tell SwiftUI “use the system setting here only for this one alert.”
  92. /// The workaround is to present a plain UIKit UIAlertController ourself, in its own representable, and explicitly tell it to use the system’s interface style instead of inheriting our forced dark mode.
  93. /// We enforce usage of the system's interface style by setting its overrideUserInterfaceStyle to whatever the device is actually using (.light or .dark).
  94. struct SystemAlert: UIViewControllerRepresentable {
  95. @Binding var isPresented: Bool
  96. let title: String
  97. let message: String
  98. let allowTitle: String
  99. let denyTitle: String
  100. /// called after Allow or Deny
  101. let onAllow: () -> Void
  102. let onDeny: () -> Void
  103. func makeUIViewController(context _: Context) -> UIViewController {
  104. // empty container
  105. UIViewController()
  106. }
  107. func updateUIViewController(_ uiVC: UIViewController, context _: Context) {
  108. guard isPresented, uiVC.presentedViewController == nil else { return }
  109. let alert = UIAlertController(
  110. title: title,
  111. message: message,
  112. preferredStyle: .alert
  113. )
  114. // force it back to the "real" system style
  115. let systemStyle = UIScreen.main.traitCollection.userInterfaceStyle
  116. alert.overrideUserInterfaceStyle = systemStyle
  117. alert.addAction(.init(title: denyTitle, style: .cancel) { _ in
  118. isPresented = false
  119. onDeny()
  120. })
  121. alert.addAction(.init(title: allowTitle, style: .default) { _ in
  122. isPresented = false
  123. onAllow()
  124. })
  125. uiVC.present(alert, animated: true)
  126. }
  127. }