BackgroundRefreshSettingsView.swift 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. // LoopFollow
  2. // BackgroundRefreshSettingsView.swift
  3. // Created by Jonas Björkert on 2025-01-13.
  4. import SwiftUI
  5. struct BackgroundRefreshSettingsView: View {
  6. @ObservedObject var viewModel: BackgroundRefreshSettingsViewModel
  7. @Environment(\.presentationMode) var presentationMode
  8. @State private var forceRefresh = false
  9. @State private var timer: Timer?
  10. @ObservedObject var bleManager = BLEManager.shared
  11. var body: some View {
  12. NavigationView {
  13. Form {
  14. refreshTypeSection
  15. if viewModel.backgroundRefreshType.isBluetooth {
  16. selectedDeviceSection
  17. availableDevicesSection
  18. }
  19. }
  20. .navigationBarTitle("Background Refresh Settings", displayMode: .inline)
  21. .toolbar {
  22. ToolbarItem(placement: .navigationBarTrailing) {
  23. Button("Done") {
  24. presentationMode.wrappedValue.dismiss()
  25. }
  26. }
  27. }
  28. .onAppear {
  29. startTimer()
  30. }
  31. .onDisappear {
  32. stopTimer()
  33. }
  34. }
  35. }
  36. // MARK: - Subviews / Computed Properties
  37. private var refreshTypeSection: some View {
  38. Section {
  39. Picker("Background Refresh Type", selection: $viewModel.backgroundRefreshType) {
  40. ForEach(BackgroundRefreshType.allCases, id: \.self) { type in
  41. Text(type.rawValue).tag(type)
  42. }
  43. }
  44. .pickerStyle(MenuPickerStyle())
  45. VStack(alignment: .leading, spacing: 4) {
  46. Text("Adjust the background refresh type.")
  47. .font(.footnote)
  48. .foregroundColor(.secondary)
  49. switch viewModel.backgroundRefreshType {
  50. case .none:
  51. Text("No background refresh. Alarms and updates will not work unless the app is open in the foreground.")
  52. .font(.footnote)
  53. .foregroundColor(.secondary)
  54. case .silentTune:
  55. Text("A silent tune will play in the background, keeping the app active. May be interrupted by other apps. Allows continuous updates but consumes more battery.")
  56. .font(.footnote)
  57. .foregroundColor(.secondary)
  58. case .rileyLink:
  59. Text("Requires a RileyLink-compatible device within Bluetooth range. Provides updates once per minute and uses less battery than the silent tune method.")
  60. .font(.footnote)
  61. .foregroundColor(.secondary)
  62. case .dexcom:
  63. Text("Requires a Dexcom G6/ONE/G7/ONE+ transmitter within Bluetooth range. Provides updates every 5 minutes and uses less battery than the silent tune method. If you have more than one device to choose from, select the one with the smallest expected bg delay.")
  64. .font(.footnote)
  65. .foregroundColor(.secondary)
  66. case .omnipodDash:
  67. Text("Requires an OmniPod DASH pod paired with this device within Bluetooth range. Provides updates once every 3 minutes and uses less battery than the silent tune method.")
  68. .font(.footnote)
  69. .foregroundColor(.secondary)
  70. }
  71. }
  72. }
  73. }
  74. @ViewBuilder
  75. private var selectedDeviceSection: some View {
  76. if let storedDevice = bleManager.getSelectedDevice() {
  77. Section(header: Text("Selected Device")) {
  78. VStack(alignment: .leading, spacing: 4) {
  79. Text(storedDevice.name ?? "Unknown Device")
  80. .font(.headline)
  81. deviceConnectionStatus(for: storedDevice)
  82. if storedDevice.rssi != 0 {
  83. Text("RSSI: \(storedDevice.rssi) dBm")
  84. .foregroundColor(.secondary)
  85. .font(.footnote)
  86. }
  87. if let offset = BLEManager.shared.expectedSensorFetchOffsetString(for: storedDevice) {
  88. Text("Expected bg delay: \(offset)")
  89. .foregroundColor(.secondary)
  90. .font(.footnote)
  91. }
  92. HStack {
  93. Spacer()
  94. Button(action: {
  95. bleManager.disconnect()
  96. }) {
  97. Text("Disconnect")
  98. .foregroundColor(.blue)
  99. }
  100. .buttonStyle(BorderlessButtonStyle())
  101. Spacer()
  102. }
  103. }
  104. .padding(.vertical, 8)
  105. }
  106. .id(forceRefresh)
  107. }
  108. }
  109. private func formattedTimeString(from seconds: TimeInterval) -> String {
  110. if seconds < 60 {
  111. return "\(Int(seconds)) seconds"
  112. } else {
  113. let minutes = Int(seconds / 60)
  114. let seconds = Int(seconds.truncatingRemainder(dividingBy: 60))
  115. return "\(minutes):\(String(format: "%02d", seconds)) minutes"
  116. }
  117. }
  118. private var availableDevicesSection: some View {
  119. Section(header: scanningStatusHeader) {
  120. BLEDeviceSelectionView(
  121. bleManager: bleManager,
  122. selectedFilter: viewModel.backgroundRefreshType,
  123. onSelectDevice: { device in
  124. bleManager.connect(device: device)
  125. }
  126. )
  127. }
  128. }
  129. private var scanningStatusHeader: some View {
  130. Text("Scanning for \(viewModel.backgroundRefreshType.rawValue)...")
  131. .font(.subheadline)
  132. .foregroundColor(.secondary)
  133. }
  134. private func deviceConnectionStatus(for device: BLEDevice) -> some View {
  135. let expectedConnectionTime: TimeInterval = bleManager.expectedHeartbeatInterval() ?? 300
  136. let now = Date()
  137. let timeSinceLastConnection = device.isConnected ? 0 : now.timeIntervalSince(device.lastConnected ?? now)
  138. if device.isConnected {
  139. return Text("Connected")
  140. .foregroundColor(.green)
  141. } else if let lastConnected = device.lastConnected {
  142. let timeRatio = timeSinceLastConnection / expectedConnectionTime
  143. let timeString = formattedTimeString(from: timeSinceLastConnection)
  144. if timeRatio < 1.0 {
  145. return Text("Disconnected for \(timeString)")
  146. .foregroundColor(.green)
  147. } else if timeRatio <= 1.15 {
  148. return Text("Disconnected for \(timeString)")
  149. .foregroundColor(.orange)
  150. } else if timeRatio <= 3.0 {
  151. return Text("Disconnected for \(timeString)")
  152. .foregroundColor(.red)
  153. } else {
  154. let date = dateTimeUtils.formattedDate(from: lastConnected)
  155. return Text("Last connection: \(date)")
  156. .foregroundColor(.red)
  157. }
  158. } else {
  159. return Text("Reconnecting...")
  160. .foregroundColor(.orange)
  161. }
  162. }
  163. private func startTimer() {
  164. timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
  165. self.forceRefresh.toggle()
  166. }
  167. }
  168. private func stopTimer() {
  169. timer?.invalidate()
  170. timer = nil
  171. }
  172. }