CalendarSettingsView.swift 4.3 KB

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