CalendarManager.swift 10 KB

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