CalendarSettingsView.swift 4.1 KB

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