CalendarSettingsView.swift 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. // LoopFollow
  2. // CalendarSettingsView.swift
  3. // Created by Jonas Björkert on 2025-05-26.
  4. import EventKit
  5. import SwiftUI
  6. struct CalendarSettingsView: View {
  7. // MARK: Storage bindings
  8. @ObservedObject private var writeCalendarEvent = Storage.shared.writeCalendarEvent
  9. @ObservedObject private var calendarIdentifier = Storage.shared.calendarIdentifier
  10. @ObservedObject private var watchLine1 = Storage.shared.watchLine1
  11. @ObservedObject private var watchLine2 = Storage.shared.watchLine2
  12. // MARK: Local state
  13. @State private var calendars: [EKCalendar] = []
  14. @State private var accessDenied = false
  15. // MARK: Body
  16. var body: some View {
  17. NavigationView {
  18. Form {
  19. // ------------- Calendar write -------------
  20. Section {
  21. Toggle("Save BG to Calendar",
  22. isOn: $writeCalendarEvent.value)
  23. .disabled(accessDenied) // prevent use when no access
  24. } footer: {
  25. Text("""
  26. Add the Apple-Calendar complication to your watch or CarPlay \
  27. to see BG readings. Create a separate calendar (e.g. “Follow”) \
  28. — this view will **delete** events on the same calendar each time \
  29. it writes new readings.
  30. """)
  31. }
  32. // ------------- Access / calendar picker -------------
  33. if accessDenied {
  34. Text("Calendar access denied")
  35. .foregroundColor(.red)
  36. } else {
  37. if !calendars.isEmpty {
  38. Picker("Calendar",
  39. selection: $calendarIdentifier.value)
  40. {
  41. ForEach(calendars, id: \.calendarIdentifier) { cal in
  42. Text(cal.title).tag(cal.calendarIdentifier)
  43. }
  44. }
  45. }
  46. }
  47. // ------------- Template lines -------------
  48. Section("Calendar Text") {
  49. TextField("Line 1", text: $watchLine1.value)
  50. .textInputAutocapitalization(.never)
  51. .disableAutocorrection(true)
  52. TextField("Line 2", text: $watchLine2.value)
  53. .textInputAutocapitalization(.never)
  54. .disableAutocorrection(true)
  55. }
  56. // ------------- Variable cheat-sheet -------------
  57. Section("Available Variables") {
  58. ForEach(variableDescriptions, id: \.self) { desc in
  59. Text(desc)
  60. }
  61. }
  62. }
  63. .task { // runs once on appear
  64. await requestCalendarAccessAndLoad()
  65. }
  66. }
  67. .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
  68. .navigationBarTitle("Calendar", displayMode: .inline)
  69. }
  70. // MARK: - Helpers
  71. /// Returns array of “%TOKEN% : Explanation” strings used in the cheat-sheet.
  72. private var variableDescriptions: [String] {
  73. [
  74. "%BG% : Blood-glucose reading",
  75. "%DIRECTION% : Dexcom trend arrow",
  76. "%DELTA% : Difference from last reading",
  77. "%IOB% : Insulin-on-Board",
  78. "%COB% : Carbs-on-Board",
  79. "%BASAL% : Current basal U/h",
  80. "%LOOP% : Loop status symbol",
  81. "%OVERRIDE% : Active override %",
  82. "%MINAGO% : Minutes since last reading",
  83. ]
  84. }
  85. /// Ask for calendar permission, then pull the user’s calendars.
  86. private func requestCalendarAccessAndLoad() async {
  87. let store = EKEventStore()
  88. do {
  89. try await store.requestAccess(to: .event)
  90. accessDenied = false
  91. calendars = store.calendars(for: .event)
  92. .sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
  93. } catch {
  94. accessDenied = true
  95. }
  96. // If the previously-saved calendar no longer exists, blank it out
  97. if !calendarIdentifier.value.isEmpty &&
  98. !calendars.contains(where: { $0.calendarIdentifier == calendarIdentifier.value })
  99. {
  100. calendarIdentifier.value = ""
  101. }
  102. }
  103. }