CalendarManager.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import Combine
  2. import CoreData
  3. import EventKit
  4. import Swinject
  5. protocol CalendarManager {
  6. func requestAccessIfNeeded() async -> Bool
  7. func calendarIDs() -> [String]
  8. var currentCalendarID: String? { get set }
  9. func createEvent(for glucose: GlucoseStored, delta: Int)
  10. }
  11. final class BaseCalendarManager: CalendarManager, Injectable {
  12. private lazy var eventStore: EKEventStore = { EKEventStore() }()
  13. @Persisted(key: "CalendarManager.currentCalendarID") var currentCalendarID: String? = nil
  14. @Injected() private var settingsManager: SettingsManager!
  15. @Injected() private var broadcaster: Broadcaster!
  16. @Injected() private var glucoseStorage: GlucoseStorage!
  17. @Injected() private var storage: FileStorage!
  18. init(resolver: Resolver) {
  19. injectServices(resolver)
  20. broadcaster.register(GlucoseObserver.self, observer: self)
  21. setupGlucose()
  22. }
  23. let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
  24. func requestAccessIfNeeded() async -> Bool {
  25. let status = EKEventStore.authorizationStatus(for: .event)
  26. switch status {
  27. case .notDetermined:
  28. return await withCheckedContinuation { continuation in
  29. #if swift(>=5.9)
  30. if #available(iOS 17.0, *) {
  31. EKEventStore().requestFullAccessToEvents { granted, error in
  32. if let error = error {
  33. print("Calendar access not granted: \(error)")
  34. warning(.service, "Calendar access not granted", error: error)
  35. }
  36. continuation.resume(returning: granted)
  37. }
  38. } else {
  39. EKEventStore().requestAccess(to: .event) { granted, error in
  40. if let error = error {
  41. print("Calendar access not granted: \(error)")
  42. warning(.service, "Calendar access not granted", error: error)
  43. }
  44. continuation.resume(returning: granted)
  45. }
  46. }
  47. #else
  48. EKEventStore().requestAccess(to: .event) { granted, error in
  49. if let error = error {
  50. print("Calendar access not granted: \(error)")
  51. warning(.service, "Calendar access not granted", error: error)
  52. }
  53. continuation.resume(returning: granted)
  54. }
  55. #endif
  56. }
  57. case .denied,
  58. .restricted:
  59. return false
  60. case .authorized:
  61. return true
  62. #if swift(>=5.9)
  63. case .fullAccess:
  64. return true
  65. case .writeOnly:
  66. if #available(iOS 17.0, *) {
  67. return await withCheckedContinuation { continuation in
  68. EKEventStore().requestFullAccessToEvents { granted, error in
  69. if let error = error {
  70. print("Calendar access not upgraded: \(error)")
  71. warning(.service, "Calendar access not upgraded", error: error)
  72. }
  73. continuation.resume(returning: granted)
  74. }
  75. }
  76. } else {
  77. return false
  78. }
  79. #endif
  80. @unknown default:
  81. warning(.service, "Unknown calendar access status")
  82. return false
  83. }
  84. }
  85. func calendarIDs() -> [String] {
  86. EKEventStore().calendars(for: .event).map(\.title)
  87. }
  88. private func getLastDetermination() -> [OrefDetermination] {
  89. CoreDataStack.shared.fetchEntities(
  90. ofType: OrefDetermination.self,
  91. onContext: coredataContext,
  92. predicate: NSPredicate.predicateFor30MinAgoForDetermination,
  93. key: "timestamp",
  94. ascending: false,
  95. fetchLimit: 1,
  96. propertiesToFetch: ["timestamp", "cob", "iob"]
  97. )
  98. }
  99. func createEvent(for glucose: GlucoseStored, delta: Int) {
  100. guard settingsManager.settings.useCalendar else { return }
  101. guard let calendar = currentCalendar else { return }
  102. deleteAllEvents(in: calendar)
  103. let glucoseValue = glucose.glucose
  104. // create an event now
  105. let event = EKEvent(eventStore: eventStore)
  106. // Calendar settings
  107. let displeyCOBandIOB = settingsManager.settings.displayCalendarIOBandCOB
  108. let displayEmojis = settingsManager.settings.displayCalendarEmojis
  109. // Latest Loop data
  110. var freshLoop: Double = 20
  111. var lastLoop: Date?
  112. if displeyCOBandIOB || displayEmojis {
  113. lastLoop = getLastDetermination().first?.timestamp
  114. freshLoop = -1 * (lastLoop?.timeIntervalSinceNow.minutes ?? 0)
  115. }
  116. var glucoseIcon = "🟢"
  117. if displayEmojis {
  118. glucoseIcon = Double(glucoseValue) <= Double(settingsManager.settings.low) ? "🔴" : glucoseIcon
  119. glucoseIcon = Double(glucoseValue) >= Double(settingsManager.settings.high) ? "🟠" : glucoseIcon
  120. glucoseIcon = freshLoop > 15 ? "🚫" : glucoseIcon
  121. }
  122. let glucoseText = glucoseFormatter
  123. .string(from: Double(
  124. settingsManager.settings.units == .mmolL ? Int(glucoseValue)
  125. .asMmolL : Decimal(glucoseValue)
  126. ) as NSNumber)!
  127. let directionText = glucose.direction ?? "↔︎"
  128. let deltaValue = settingsManager.settings.units == .mmolL ? Int(delta.asMmolL) : delta
  129. let deltaText = deltaFormatter.string(from: NSNumber(value: deltaValue)) ?? "--"
  130. let iobText = iobFormatter.string(from: (getLastDetermination().first?.iob ?? 0) as NSNumber) ?? ""
  131. let cobText = cobFormatter.string(from: (getLastDetermination().first?.cob ?? 0) as NSNumber) ?? ""
  132. var glucoseDisplayText = displayEmojis ? glucoseIcon + " " : ""
  133. glucoseDisplayText += glucoseText + " " + directionText + " " + deltaText
  134. var iobDisplayText = ""
  135. var cobDisplayText = ""
  136. if displeyCOBandIOB {
  137. if displayEmojis {
  138. iobDisplayText += "💉"
  139. cobDisplayText += "🥨"
  140. } else {
  141. iobDisplayText += "IOB:"
  142. cobDisplayText += "COB:"
  143. }
  144. iobDisplayText += " " + iobText
  145. cobDisplayText += " " + cobText
  146. event.location = iobDisplayText + " " + cobDisplayText
  147. }
  148. event.title = glucoseDisplayText
  149. event.notes = "Trio"
  150. event.startDate = Date()
  151. event.endDate = Date(timeIntervalSinceNow: 60 * 10)
  152. event.calendar = calendar
  153. do {
  154. try eventStore.save(event, span: .thisEvent)
  155. } catch {
  156. warning(.service, "Cannot create calendar event", error: error)
  157. }
  158. }
  159. var currentCalendar: EKCalendar? {
  160. let calendars = eventStore.calendars(for: .event)
  161. guard calendars.isNotEmpty else { return nil }
  162. return calendars.first { $0.title == self.currentCalendarID }
  163. }
  164. private func deleteAllEvents(in calendar: EKCalendar) {
  165. let predicate = eventStore.predicateForEvents(
  166. withStart: Date(timeIntervalSinceNow: -24 * 3600),
  167. end: Date(),
  168. calendars: [calendar]
  169. )
  170. let events = eventStore.events(matching: predicate)
  171. for event in events {
  172. do {
  173. try eventStore.remove(event, span: .thisEvent)
  174. } catch {
  175. warning(.service, "Cannot remove calendar events", error: error)
  176. }
  177. }
  178. }
  179. private var glucoseFormatter: NumberFormatter {
  180. let formatter = NumberFormatter()
  181. formatter.numberStyle = .decimal
  182. formatter.maximumFractionDigits = 0
  183. if settingsManager.settings.units == .mmolL {
  184. formatter.minimumFractionDigits = 1
  185. formatter.maximumFractionDigits = 1
  186. }
  187. formatter.roundingMode = .halfUp
  188. return formatter
  189. }
  190. private var deltaFormatter: NumberFormatter {
  191. let formatter = NumberFormatter()
  192. formatter.numberStyle = .decimal
  193. formatter.maximumFractionDigits = 1
  194. formatter.positivePrefix = "+"
  195. return formatter
  196. }
  197. private var iobFormatter: NumberFormatter {
  198. let formatter = NumberFormatter()
  199. formatter.numberStyle = .decimal
  200. formatter.maximumFractionDigits = 1
  201. return formatter
  202. }
  203. private var cobFormatter: NumberFormatter {
  204. let formatter = NumberFormatter()
  205. formatter.numberStyle = .decimal
  206. formatter.maximumFractionDigits = 0
  207. return formatter
  208. }
  209. private func setupGlucose() {
  210. coredataContext.performAndWait {
  211. let results = CoreDataStack.shared.fetchEntities(
  212. ofType: GlucoseStored.self,
  213. onContext: coredataContext,
  214. predicate: NSPredicate.predicateFor30MinAgo,
  215. key: "date",
  216. ascending: false
  217. )
  218. guard results.count >= 2 else { return }
  219. if let lastGlucose = results.first,
  220. let secondLastReading = results.dropFirst().first?.glucose
  221. {
  222. let glucoseDelta = lastGlucose.glucose - secondLastReading
  223. self.createEvent(for: lastGlucose, delta: Int(glucoseDelta))
  224. } else {
  225. debugPrint("Failed to unwrap necessary glucose readings")
  226. }
  227. }
  228. }
  229. }
  230. extension BaseCalendarManager: GlucoseObserver {
  231. func glucoseDidUpdate(_: [BloodGlucose]) {
  232. setupGlucose()
  233. }
  234. }
  235. extension BloodGlucose.Direction {
  236. var symbol: String {
  237. switch self {
  238. case .tripleUp:
  239. return "↑↑↑"
  240. case .doubleUp:
  241. return "↑↑"
  242. case .singleUp:
  243. return "↑"
  244. case .fortyFiveUp:
  245. return "↗︎"
  246. case .flat:
  247. return "→"
  248. case .fortyFiveDown:
  249. return "↘︎"
  250. case .singleDown:
  251. return "↓"
  252. case .doubleDown:
  253. return "↓↓"
  254. case .tripleDown:
  255. return "↓↓↓"
  256. case .none:
  257. return "↔︎"
  258. case .notComputable:
  259. return "↔︎"
  260. case .rateOutOfRange:
  261. return "↔︎"
  262. }
  263. }
  264. }