CalendarManager.swift 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import Combine
  2. import EventKit
  3. import Swinject
  4. protocol CalendarManager {
  5. func requestAccessIfNeeded() -> AnyPublisher<Bool, Never>
  6. func calendarIDs() -> [String]
  7. var currentCalendarID: String? { get set }
  8. func createEvent(for glucose: BloodGlucose?, delta: Int?)
  9. }
  10. final class BaseCalendarManager: CalendarManager, Injectable {
  11. private lazy var eventStore: EKEventStore = { EKEventStore() }()
  12. @Persisted(key: "CalendarManager.currentCalendarID") var currentCalendarID: String? = nil
  13. @Injected() private var settingsManager: SettingsManager!
  14. init(resilver: Resolver) {
  15. injectServices(resilver)
  16. }
  17. func requestAccessIfNeeded() -> AnyPublisher<Bool, Never> {
  18. Future { promise in
  19. let status = EKEventStore.authorizationStatus(for: .event)
  20. switch status {
  21. case .notDetermined:
  22. EKEventStore().requestAccess(to: .event) { granted, error in
  23. if let error = error {
  24. warning(.service, "Calendar access not granded", error: error)
  25. }
  26. promise(.success(granted))
  27. }
  28. case .denied,
  29. .restricted:
  30. promise(.success(false))
  31. case .authorized:
  32. promise(.success(true))
  33. @unknown default:
  34. warning(.service, "Unknown calendar access status")
  35. promise(.success(false))
  36. }
  37. }.eraseToAnyPublisher()
  38. }
  39. func calendarIDs() -> [String] {
  40. EKEventStore().calendars(for: .event).map(\.title)
  41. }
  42. func createEvent(for glucose: BloodGlucose?, delta: Int?) {
  43. guard settingsManager.settings.useCalendar else { return }
  44. guard let calendar = currentCalendar else { return }
  45. deleteAllEvents(in: calendar)
  46. guard let glucose = glucose, let glucoseValue = glucose.glucose else { return }
  47. // create an event now
  48. let event = EKEvent(eventStore: eventStore)
  49. let glucoseText = glucoseFormatter
  50. .string(from: Double(
  51. settingsManager.settings.units == .mmolL ?glucoseValue
  52. .asMmolL : Decimal(glucoseValue)
  53. ) as NSNumber)!
  54. let directionText = glucose.direction?.symbol ?? "↔︎"
  55. let deltaText = delta
  56. .map {
  57. deltaFormatter
  58. .string(from: Double(settingsManager.settings.units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)!
  59. } ?? "--"
  60. let title = glucoseText + " " + directionText + " " + deltaText
  61. event.title = title
  62. event.notes = "FreeAPS X"
  63. event.startDate = Date()
  64. event.endDate = Date(timeIntervalSinceNow: 60 * 10)
  65. event.calendar = calendar
  66. do {
  67. try eventStore.save(event, span: .thisEvent)
  68. } catch {
  69. warning(.service, "Cannot create calendar event", error: error)
  70. }
  71. }
  72. var currentCalendar: EKCalendar? {
  73. let calendars = eventStore.calendars(for: .event)
  74. guard calendars.isNotEmpty else { return nil }
  75. return calendars.first { $0.title == self.currentCalendarID }
  76. }
  77. private func deleteAllEvents(in calendar: EKCalendar) {
  78. let predicate = eventStore.predicateForEvents(
  79. withStart: Date(timeIntervalSinceNow: -24 * 3600),
  80. end: Date(),
  81. calendars: [calendar]
  82. )
  83. let events = eventStore.events(matching: predicate)
  84. for event in events {
  85. do {
  86. try eventStore.remove(event, span: .thisEvent)
  87. } catch {
  88. warning(.service, "Cannot remove calendar events", error: error)
  89. }
  90. }
  91. }
  92. private var glucoseFormatter: NumberFormatter {
  93. let formatter = NumberFormatter()
  94. formatter.numberStyle = .decimal
  95. formatter.maximumFractionDigits = 0
  96. if settingsManager.settings.units == .mmolL {
  97. formatter.minimumFractionDigits = 1
  98. formatter.maximumFractionDigits = 1
  99. }
  100. return formatter
  101. }
  102. private var deltaFormatter: NumberFormatter {
  103. let formatter = NumberFormatter()
  104. formatter.numberStyle = .decimal
  105. formatter.maximumFractionDigits = 2
  106. formatter.positivePrefix = "+"
  107. return formatter
  108. }
  109. }
  110. extension BloodGlucose.Direction {
  111. var symbol: String {
  112. switch self {
  113. case .tripleUp:
  114. return "↑↑↑"
  115. case .doubleUp:
  116. return "↑↑"
  117. case .singleUp:
  118. return "↑"
  119. case .fortyFiveUp:
  120. return "↗︎"
  121. case .flat:
  122. return "→"
  123. case .fortyFiveDown:
  124. return "↘︎"
  125. case .singleDown:
  126. return "↓"
  127. case .doubleDown:
  128. return "↓↓"
  129. case .tripleDown:
  130. return "↓↓↓"
  131. case .none:
  132. return "↔︎"
  133. case .notComputable:
  134. return "↔︎"
  135. case .rateOutOfRange:
  136. return "↔︎"
  137. }
  138. }
  139. }