CalendarManager.swift 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. @Injected() private var broadcaster: Broadcaster!
  15. @Injected() private var glucoseStorage: GlucoseStorage!
  16. init(resolver: Resolver) {
  17. injectServices(resolver)
  18. broadcaster.register(GlucoseObserver.self, observer: self)
  19. setupGlucose()
  20. }
  21. func requestAccessIfNeeded() -> AnyPublisher<Bool, Never> {
  22. Future { promise in
  23. let status = EKEventStore.authorizationStatus(for: .event)
  24. switch status {
  25. case .notDetermined:
  26. EKEventStore().requestAccess(to: .event) { granted, error in
  27. if let error = error {
  28. warning(.service, "Calendar access not granded", error: error)
  29. }
  30. promise(.success(granted))
  31. }
  32. case .denied,
  33. .restricted:
  34. promise(.success(false))
  35. case .authorized:
  36. promise(.success(true))
  37. #if swift(>=5.9)
  38. case .fullAccess:
  39. promise(.success(true))
  40. case .writeOnly:
  41. if #available(iOS 17.0, *) {
  42. EKEventStore().requestFullAccessToEvents(completion: { (granted: Bool, error: Error?) -> Void in
  43. if let error = error {
  44. print("Calendar access not upgraded")
  45. warning(.service, "Calendar access not upgraded", error: error)
  46. }
  47. promise(.success(granted))
  48. })
  49. }
  50. #endif
  51. @unknown default:
  52. warning(.service, "Unknown calendar access status")
  53. promise(.success(false))
  54. }
  55. }.eraseToAnyPublisher()
  56. }
  57. func calendarIDs() -> [String] {
  58. EKEventStore().calendars(for: .event).map(\.title)
  59. }
  60. func createEvent(for glucose: BloodGlucose?, delta: Int?) {
  61. guard settingsManager.settings.useCalendar else { return }
  62. guard let calendar = currentCalendar else { return }
  63. deleteAllEvents(in: calendar)
  64. guard let glucose = glucose, let glucoseValue = glucose.glucose else { return }
  65. // create an event now
  66. let event = EKEvent(eventStore: eventStore)
  67. let glucoseText = glucoseFormatter
  68. .string(from: Double(
  69. settingsManager.settings.units == .mmolL ?glucoseValue
  70. .asMmolL : Decimal(glucoseValue)
  71. ) as NSNumber)!
  72. let directionText = glucose.direction?.symbol ?? "↔︎"
  73. let deltaText = delta
  74. .map {
  75. deltaFormatter
  76. .string(from: Double(settingsManager.settings.units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)!
  77. } ?? "--"
  78. let title = glucoseText + " " + directionText + " " + deltaText
  79. event.title = title
  80. event.notes = "iAPS"
  81. event.startDate = Date()
  82. event.endDate = Date(timeIntervalSinceNow: 60 * 10)
  83. event.calendar = calendar
  84. do {
  85. try eventStore.save(event, span: .thisEvent)
  86. } catch {
  87. warning(.service, "Cannot create calendar event", error: error)
  88. }
  89. }
  90. var currentCalendar: EKCalendar? {
  91. let calendars = eventStore.calendars(for: .event)
  92. guard calendars.isNotEmpty else { return nil }
  93. return calendars.first { $0.title == self.currentCalendarID }
  94. }
  95. private func deleteAllEvents(in calendar: EKCalendar) {
  96. let predicate = eventStore.predicateForEvents(
  97. withStart: Date(timeIntervalSinceNow: -24 * 3600),
  98. end: Date(),
  99. calendars: [calendar]
  100. )
  101. let events = eventStore.events(matching: predicate)
  102. for event in events {
  103. do {
  104. try eventStore.remove(event, span: .thisEvent)
  105. } catch {
  106. warning(.service, "Cannot remove calendar events", error: error)
  107. }
  108. }
  109. }
  110. private var glucoseFormatter: NumberFormatter {
  111. let formatter = NumberFormatter()
  112. formatter.numberStyle = .decimal
  113. formatter.maximumFractionDigits = 0
  114. if settingsManager.settings.units == .mmolL {
  115. formatter.minimumFractionDigits = 1
  116. formatter.maximumFractionDigits = 1
  117. }
  118. formatter.roundingMode = .halfUp
  119. return formatter
  120. }
  121. private var deltaFormatter: NumberFormatter {
  122. let formatter = NumberFormatter()
  123. formatter.numberStyle = .decimal
  124. formatter.maximumFractionDigits = 1
  125. formatter.positivePrefix = "+"
  126. return formatter
  127. }
  128. func setupGlucose() {
  129. let glucose = glucoseStorage.recent()
  130. let recentGlucose = glucose.last
  131. let glucoseDelta: Int?
  132. if glucose.count >= 2 {
  133. glucoseDelta = (recentGlucose?.glucose ?? 0) - (glucose[glucose.count - 2].glucose ?? 0)
  134. } else {
  135. glucoseDelta = nil
  136. }
  137. createEvent(for: recentGlucose, delta: glucoseDelta)
  138. }
  139. }
  140. extension BaseCalendarManager: GlucoseObserver {
  141. func glucoseDidUpdate(_: [BloodGlucose]) {
  142. setupGlucose()
  143. }
  144. }
  145. extension BloodGlucose.Direction {
  146. var symbol: String {
  147. switch self {
  148. case .tripleUp:
  149. return "↑↑↑"
  150. case .doubleUp:
  151. return "↑↑"
  152. case .singleUp:
  153. return "↑"
  154. case .fortyFiveUp:
  155. return "↗︎"
  156. case .flat:
  157. return "→"
  158. case .fortyFiveDown:
  159. return "↘︎"
  160. case .singleDown:
  161. return "↓"
  162. case .doubleDown:
  163. return "↓↓"
  164. case .tripleDown:
  165. return "↓↓↓"
  166. case .none:
  167. return "↔︎"
  168. case .notComputable:
  169. return "↔︎"
  170. case .rateOutOfRange:
  171. return "↔︎"
  172. }
  173. }
  174. }