SettingsMenuView.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // LoopFollow
  2. // SettingsMenuView.swift
  3. import SwiftUI
  4. import UIKit
  5. struct SettingsMenuView: View {
  6. // MARK: - Observed Objects
  7. @ObservedObject private var nightscoutURL = Storage.shared.url
  8. @ObservedObject private var settingsPath = Observable.shared.settingsPath
  9. // MARK: – Local state
  10. var onBack: (() -> Void)?
  11. // MARK: – Observed objects
  12. @ObservedObject private var url = Storage.shared.url
  13. // MARK: – Body
  14. var body: some View {
  15. NavigationStack(path: $settingsPath.value) {
  16. List {
  17. dataSection
  18. Section("Display Settings") {
  19. NavigationRow(title: "General",
  20. icon: "gearshape")
  21. {
  22. settingsPath.value.append(Sheet.general)
  23. }
  24. NavigationRow(title: "Graph",
  25. icon: "chart.xyaxis.line")
  26. {
  27. settingsPath.value.append(Sheet.graph)
  28. }
  29. if !nightscoutURL.value.isEmpty {
  30. NavigationRow(title: "Information Display",
  31. icon: "info.circle")
  32. {
  33. settingsPath.value.append(Sheet.infoDisplay)
  34. }
  35. }
  36. NavigationRow(title: "Tabs",
  37. icon: "rectangle.3.group")
  38. {
  39. settingsPath.value.append(Sheet.tabSettings)
  40. }
  41. }
  42. Section("App Settings") {
  43. NavigationRow(title: "Background Refresh",
  44. icon: "arrow.clockwise")
  45. {
  46. settingsPath.value.append(Sheet.backgroundRefresh)
  47. }
  48. NavigationRow(title: "Import/Export",
  49. icon: "square.and.arrow.down")
  50. {
  51. settingsPath.value.append(Sheet.importExport)
  52. }
  53. NavigationRow(title: "APN",
  54. icon: "bell.and.waves.left.and.right")
  55. {
  56. settingsPath.value.append(Sheet.apn)
  57. }
  58. #if !targetEnvironment(macCatalyst)
  59. NavigationRow(title: "Live Activity",
  60. icon: "dot.radiowaves.left.and.right")
  61. {
  62. settingsPath.value.append(Sheet.liveActivity)
  63. }
  64. #endif
  65. if !nightscoutURL.value.isEmpty {
  66. NavigationRow(title: "Remote",
  67. icon: "antenna.radiowaves.left.and.right")
  68. {
  69. settingsPath.value.append(Sheet.remote)
  70. }
  71. }
  72. }
  73. Section("Alarms") {
  74. NavigationRow(title: "Alarms",
  75. icon: "bell.badge")
  76. {
  77. settingsPath.value.append(Sheet.alarmSettings)
  78. }
  79. }
  80. Section("Integrations") {
  81. NavigationRow(title: "Calendar",
  82. icon: "calendar")
  83. {
  84. settingsPath.value.append(Sheet.calendar)
  85. }
  86. NavigationRow(title: "Contact",
  87. icon: "person.circle")
  88. {
  89. settingsPath.value.append(Sheet.contact)
  90. }
  91. }
  92. Section("Advanced Settings") {
  93. NavigationRow(title: "Advanced",
  94. icon: "exclamationmark.shield")
  95. {
  96. settingsPath.value.append(Sheet.advanced)
  97. }
  98. }
  99. }
  100. .navigationTitle("Settings")
  101. .navigationBarTitleDisplayMode(.large)
  102. .navigationDestination(for: Sheet.self) { $0.destination }
  103. .toolbar {
  104. if let onBack {
  105. ToolbarItem(placement: .navigationBarLeading) {
  106. Button(action: onBack) {
  107. Image(systemName: "chevron.left")
  108. }
  109. }
  110. }
  111. }
  112. }
  113. }
  114. // MARK: – Section builders
  115. @ViewBuilder
  116. private var dataSection: some View {
  117. Section("Data Settings") {
  118. Picker("Units",
  119. selection: Binding(
  120. get: { Storage.shared.units.value },
  121. set: { Storage.shared.units.value = $0 }
  122. )) {
  123. Text("mg/dL").tag("mg/dL")
  124. Text("mmol/L").tag("mmol/L")
  125. }
  126. .pickerStyle(.segmented)
  127. NavigationRow(title: "Nightscout",
  128. icon: "network")
  129. {
  130. settingsPath.value.append(Sheet.nightscout)
  131. }
  132. NavigationRow(title: "Dexcom",
  133. icon: "sensor.tag.radiowaves.forward")
  134. {
  135. settingsPath.value.append(Sheet.dexcom)
  136. }
  137. }
  138. }
  139. }
  140. // MARK: – Sheet routing
  141. private enum Sheet: Hashable, Identifiable {
  142. case nightscout, dexcom
  143. case backgroundRefresh
  144. case general, graph
  145. case tabSettings
  146. case infoDisplay
  147. case alarmSettings
  148. case apn
  149. #if !targetEnvironment(macCatalyst)
  150. case liveActivity
  151. #endif
  152. case remote
  153. case importExport
  154. case calendar, contact
  155. case advanced
  156. case aggregatedStats
  157. var id: Self { self }
  158. @ViewBuilder
  159. var destination: some View {
  160. switch self {
  161. case .nightscout: NightscoutSettingsView(viewModel: .init())
  162. case .dexcom: DexcomSettingsView(viewModel: .init())
  163. case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init())
  164. case .general: GeneralSettingsView()
  165. case .graph: GraphSettingsView()
  166. case .tabSettings: TabCustomizationModal()
  167. case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
  168. case .alarmSettings: AlarmSettingsView()
  169. case .apn: APNSettingsView()
  170. #if !targetEnvironment(macCatalyst)
  171. case .liveActivity: LiveActivitySettingsView()
  172. #endif
  173. case .remote: RemoteSettingsView(viewModel: .init())
  174. case .importExport: ImportExportSettingsView()
  175. case .calendar: CalendarSettingsView()
  176. case .contact: ContactSettingsView(viewModel: .init())
  177. case .advanced: AdvancedSettingsView(viewModel: .init())
  178. case .aggregatedStats:
  179. AggregatedStatsViewWrapper()
  180. }
  181. }
  182. }
  183. // Helper view to access MainViewController
  184. struct AggregatedStatsViewWrapper: View {
  185. @State private var mainViewController: MainViewController?
  186. var body: some View {
  187. Group {
  188. if let mainVC = mainViewController {
  189. AggregatedStatsContentView(mainViewController: mainVC)
  190. } else {
  191. Text("Loading stats...")
  192. .onAppear {
  193. mainViewController = getMainViewController()
  194. }
  195. }
  196. }
  197. }
  198. private func getMainViewController() -> MainViewController? {
  199. guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
  200. let window = windowScene.windows.first,
  201. let rootVC = window.rootViewController
  202. else {
  203. return nil
  204. }
  205. if let mainVC = rootVC as? MainViewController {
  206. return mainVC
  207. }
  208. if let navVC = rootVC as? UINavigationController,
  209. let mainVC = navVC.viewControllers.first as? MainViewController
  210. {
  211. return mainVC
  212. }
  213. if let tabVC = rootVC as? UITabBarController {
  214. for vc in tabVC.viewControllers ?? [] {
  215. if let mainVC = vc as? MainViewController {
  216. return mainVC
  217. }
  218. if let navVC = vc as? UINavigationController,
  219. let mainVC = navVC.viewControllers.first as? MainViewController
  220. {
  221. return mainVC
  222. }
  223. }
  224. }
  225. return nil
  226. }
  227. }
  228. // MARK: – UIKit helpers (unchanged)
  229. import UIKit
  230. extension UIApplication {
  231. var topMost: UIViewController? {
  232. guard var top = keyWindow?.rootViewController else { return nil }
  233. while let presented = top.presentedViewController {
  234. top = presented
  235. }
  236. return top
  237. }
  238. }
  239. extension UIViewController {
  240. func presentSimpleAlert(title: String, message: String) {
  241. let a = UIAlertController(title: title,
  242. message: message,
  243. preferredStyle: .alert)
  244. a.addAction(UIAlertAction(title: "OK", style: .default))
  245. present(a, animated: true)
  246. }
  247. }