MainViewController.swift 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. //
  2. // FirstViewController.swift
  3. // LoopFollow
  4. //
  5. // Created by Jon Fawcett on 6/1/20.
  6. // Copyright © 2020 Jon Fawcett. All rights reserved.
  7. //
  8. import UIKit
  9. import Charts
  10. import EventKit
  11. import ShareClient
  12. import UserNotifications
  13. import AVFAudio
  14. import CoreBluetooth
  15. func IsNightscoutEnabled() -> Bool {
  16. return !ObservableUserDefaults.shared.url.value.isEmpty
  17. }
  18. class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate, UIScrollViewDelegate {
  19. @IBOutlet weak var BGText: UILabel!
  20. @IBOutlet weak var DeltaText: UILabel!
  21. @IBOutlet weak var DirectionText: UILabel!
  22. @IBOutlet weak var BGChart: LineChartView!
  23. @IBOutlet weak var BGChartFull: LineChartView!
  24. @IBOutlet weak var MinAgoText: UILabel!
  25. @IBOutlet weak var infoTable: UITableView!
  26. @IBOutlet weak var Console: UITableViewCell!
  27. @IBOutlet weak var DragBar: UIImageView!
  28. @IBOutlet weak var PredictionLabel: UILabel!
  29. @IBOutlet weak var LoopStatusLabel: UILabel!
  30. @IBOutlet weak var statsPieChart: PieChartView!
  31. @IBOutlet weak var statsLowPercent: UILabel!
  32. @IBOutlet weak var statsInRangePercent: UILabel!
  33. @IBOutlet weak var statsHighPercent: UILabel!
  34. @IBOutlet weak var statsAvgBG: UILabel!
  35. @IBOutlet weak var statsEstA1C: UILabel!
  36. @IBOutlet weak var statsStdDev: UILabel!
  37. @IBOutlet weak var serverText: UILabel!
  38. @IBOutlet weak var statsView: UIView!
  39. @IBOutlet weak var smallGraphHeightConstraint: NSLayoutConstraint!
  40. var refreshScrollView: UIScrollView!
  41. var refreshControl: UIRefreshControl!
  42. let speechSynthesizer = AVSpeechSynthesizer()
  43. var appStateController: AppStateController?
  44. // Variables for BG Charts
  45. public var numPoints: Int = 13
  46. public var linePlotData: [Double] = []
  47. public var linePlotDataTime: [Double] = []
  48. var firstGraphLoad: Bool = true
  49. var firstBasalGraphLoad: Bool = true
  50. var minAgoBG: Double = 0.0
  51. var currentOverride = 1.0
  52. // Vars for NS Pull
  53. var mmol = false as Bool
  54. var apnsKey = UserDefaultsRepository.token.value as String
  55. var defaults : UserDefaults?
  56. let consoleLogging = true
  57. var timeofLastBGUpdate = 0 as TimeInterval
  58. var currentSage : sageData?
  59. var currentCage : cageData?
  60. var currentIage : iageData?
  61. var backgroundTask = BackgroundTask()
  62. // Refresh NS Data
  63. var timer = Timer()
  64. // check every 30 Seconds whether new bgvalues should be retrieved
  65. let timeInterval: TimeInterval = 30.0
  66. // Check Alarms Timer
  67. // Don't check within 1 minute of alarm triggering to give the snoozer time to save data
  68. var checkAlarmTimer = Timer()
  69. var checkAlarmInterval: TimeInterval = 60.0
  70. var graphNowTimer = Timer()
  71. var lastCalendarWriteAttemptTime: TimeInterval = 0
  72. // Info Table Setup
  73. var infoManager: InfoManager!
  74. var profileManager = ProfileManager.shared
  75. var bgData: [ShareGlucoseData] = []
  76. var basalProfile: [basalProfileStruct] = []
  77. var basalData: [basalGraphStruct] = []
  78. var basalScheduleData: [basalGraphStruct] = []
  79. var bolusData: [bolusGraphStruct] = []
  80. var smbData: [bolusGraphStruct] = []
  81. var carbData: [carbGraphStruct] = []
  82. var overrideGraphData: [DataStructs.overrideStruct] = []
  83. var tempTargetGraphData: [DataStructs.tempTargetStruct] = []
  84. var predictionData: [ShareGlucoseData] = []
  85. var bgCheckData: [ShareGlucoseData] = []
  86. var suspendGraphData: [DataStructs.timestampOnlyStruct] = []
  87. var resumeGraphData: [DataStructs.timestampOnlyStruct] = []
  88. var sensorStartGraphData: [DataStructs.timestampOnlyStruct] = []
  89. var noteGraphData: [DataStructs.noteStruct] = []
  90. var chartData = LineChartData()
  91. var newBGPulled = false
  92. var lastCalDate: Double = 0
  93. var latestDirectionString = ""
  94. var latestMinAgoString = ""
  95. var latestDeltaString = ""
  96. var latestLoopStatusString = ""
  97. var latestLoopTime: Double = 0
  98. var latestCOB: CarbMetric?
  99. var latestBasal = ""
  100. var latestPumpVolume: Double = 50.0
  101. var latestIOB: InsulinMetric?
  102. var lastOverrideStartTime: TimeInterval = 0
  103. var lastOverrideEndTime: TimeInterval = 0
  104. var topBG: Float = UserDefaultsRepository.minBGScale.value
  105. var topPredictionBG: Float = UserDefaultsRepository.minBGScale.value
  106. var lastOverrideAlarm: TimeInterval = 0
  107. var lastTempTargetAlarm: TimeInterval = 0
  108. var lastTempTargetStartTime: TimeInterval = 0
  109. var lastTempTargetEndTime: TimeInterval = 0
  110. // share
  111. var bgDataShare: [ShareGlucoseData] = []
  112. var dexShare: ShareClient?;
  113. // calendar setup
  114. let store = EKEventStore()
  115. var snoozeTabItem: UITabBarItem = UITabBarItem()
  116. // Stores the time of the last speech announcement to prevent repeated announcements.
  117. // This is a temporary safeguard until the issue with multiple calls to speakBG is fixed.
  118. var lastSpeechTime: Date?
  119. var autoScrollPauseUntil: Date? = nil
  120. var IsNotLooping = false
  121. let contactImageUpdater = ContactImageUpdater()
  122. override func viewDidLoad() {
  123. super.viewDidLoad()
  124. //Migration of UserDefaultsRepository -> Storage handling
  125. if !UserDefaultsRepository.backgroundRefresh.value {
  126. Storage.shared.backgroundRefreshType.value = .none
  127. UserDefaultsRepository.backgroundRefresh.value = true
  128. }
  129. // Ensure alertNotLooping has a minimum value of 15.
  130. if UserDefaultsRepository.alertNotLooping.value < 15 {
  131. UserDefaultsRepository.alertNotLooping.value = 15
  132. }
  133. // Synchronize info types to ensure arrays are the correct size
  134. UserDefaultsRepository.synchronizeInfoTypes()
  135. infoTable.rowHeight = 21
  136. infoTable.dataSource = self
  137. infoTable.tableFooterView = UIView(frame: .zero)
  138. infoTable.bounces = false
  139. infoTable.addBorder(toSide: .Left, withColor: UIColor.darkGray.cgColor, andThickness: 2)
  140. self.infoManager = InfoManager(tableView: infoTable)
  141. smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value)
  142. self.view.layoutIfNeeded()
  143. let shareUserName = UserDefaultsRepository.shareUserName.value
  144. let sharePassword = UserDefaultsRepository.sharePassword.value
  145. let shareServer = UserDefaultsRepository.shareServer.value == "US" ?KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue
  146. dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer )
  147. // setup show/hide small graph and stats
  148. BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value
  149. statsView.isHidden = !UserDefaultsRepository.showStats.value
  150. BGChart.delegate = self
  151. BGChartFull.delegate = self
  152. if UserDefaultsRepository.forceDarkMode.value {
  153. overrideUserInterfaceStyle = .dark
  154. self.tabBarController?.overrideUserInterfaceStyle = .dark
  155. }
  156. // Load the snoozer tab
  157. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  158. snoozer.loadViewIfNeeded()
  159. // Trigger foreground and background functions
  160. let notificationCenter = NotificationCenter.default
  161. notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
  162. notificationCenter.addObserver(self, selector: #selector(appCameToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
  163. // Setup the Graph
  164. if firstGraphLoad {
  165. createGraph()
  166. createSmallBGGraph()
  167. }
  168. // setup display for NS vs Dex
  169. showHideNSDetails()
  170. scheduleAllTasks()
  171. // Set up refreshScrollView for BGText
  172. refreshScrollView = UIScrollView()
  173. refreshScrollView.translatesAutoresizingMaskIntoConstraints = false
  174. refreshScrollView.alwaysBounceVertical = true
  175. view.addSubview(refreshScrollView)
  176. NSLayoutConstraint.activate([
  177. refreshScrollView.leadingAnchor.constraint(equalTo: BGText.leadingAnchor),
  178. refreshScrollView.trailingAnchor.constraint(equalTo: BGText.trailingAnchor),
  179. refreshScrollView.topAnchor.constraint(equalTo: BGText.topAnchor),
  180. refreshScrollView.bottomAnchor.constraint(equalTo: BGText.bottomAnchor)
  181. ])
  182. refreshControl = UIRefreshControl()
  183. refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
  184. refreshScrollView.addSubview(refreshControl)
  185. // Add this line to prevent scrolling in other directions
  186. refreshScrollView.alwaysBounceVertical = true
  187. refreshScrollView.delegate = self
  188. NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: NSNotification.Name("refresh"), object: nil)
  189. }
  190. deinit {
  191. NotificationCenter.default.removeObserver(self, name: NSNotification.Name("refresh"), object: nil)
  192. }
  193. // Clean all timers and start new ones when refreshing
  194. @objc func refresh() {
  195. LogManager.shared.log(category: .general, message: "Refreshing")
  196. // Clear prediction for both Loop or OpenAPS
  197. // Check if Loop prediction data exists and clear it if necessary
  198. if !predictionData.isEmpty {
  199. predictionData.removeAll()
  200. updatePredictionGraph()
  201. }
  202. // Check if OpenAPS prediction data exists and clear it if necessary
  203. let openAPSDataIndices = [12, 13, 14, 15]
  204. for dataIndex in openAPSDataIndices {
  205. let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  206. let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  207. if !mainChart.entries.isEmpty || !smallChart.entries.isEmpty {
  208. updatePredictionGraphGeneric(
  209. dataIndex: dataIndex,
  210. predictionData: [],
  211. chartLabel: "",
  212. color: UIColor.systemGray
  213. )
  214. }
  215. }
  216. MinAgoText.text = "Refreshing"
  217. latestMinAgoString = "Refreshing"
  218. scheduleAllTasks()
  219. currentCage = nil
  220. currentSage = nil
  221. currentIage = nil
  222. lastSpeechTime = nil
  223. refreshControl.endRefreshing()
  224. }
  225. // Scroll down BGText when refreshing
  226. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  227. if scrollView == refreshScrollView {
  228. let yOffset = scrollView.contentOffset.y
  229. if yOffset < 0 {
  230. BGText.transform = CGAffineTransform(translationX: 0, y: -yOffset)
  231. } else {
  232. BGText.transform = CGAffineTransform.identity
  233. }
  234. }
  235. }
  236. override func viewWillAppear(_ animated: Bool) {
  237. // set screen lock
  238. UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value;
  239. // check the app state
  240. // TODO: move to a function ?
  241. if let appState = self.appStateController {
  242. if appState.chartSettingsChanged {
  243. // can look at settings flags to be more fine tuned
  244. self.updateBGGraphSettings()
  245. if ChartSettingsChangeEnum.smallGraphHeight.rawValue != 0 {
  246. smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value)
  247. self.view.layoutIfNeeded()
  248. }
  249. // reset the app state
  250. appState.chartSettingsChanged = false
  251. appState.chartSettingsChanges = 0
  252. }
  253. if appState.generalSettingsChanged {
  254. // settings for appBadge changed
  255. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.appBadgeChange.rawValue != 0 {
  256. }
  257. // settings for textcolor changed
  258. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.colorBGTextChange.rawValue != 0 {
  259. self.setBGTextColor()
  260. }
  261. // settings for showStats changed
  262. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showStatsChange.rawValue != 0 {
  263. statsView.isHidden = !UserDefaultsRepository.showStats.value
  264. }
  265. // settings for useIFCC changed
  266. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.useIFCCChange.rawValue != 0 {
  267. updateStats()
  268. }
  269. // settings for showSmallGraph changed
  270. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showSmallGraphChange.rawValue != 0 {
  271. BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value
  272. }
  273. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showDisplayNameChange.rawValue != 0 {
  274. self.updateServerText()
  275. }
  276. // reset the app state
  277. appState.generalSettingsChanged = false
  278. appState.generalSettingsChanges = 0
  279. }
  280. if appState.infoDataSettingsChanged {
  281. self.infoTable.reloadData()
  282. // reset
  283. appState.infoDataSettingsChanged = false
  284. }
  285. // add more processing of the app state
  286. }
  287. }
  288. // Info Table Functions
  289. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  290. guard let infoManager = infoManager else {
  291. return 0
  292. }
  293. return infoManager.numberOfRows()
  294. }
  295. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  296. let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
  297. if let values = infoManager.dataForIndexPath(indexPath) {
  298. cell.textLabel?.text = values.name
  299. cell.detailTextLabel?.text = values.value
  300. } else {
  301. cell.textLabel?.text = ""
  302. cell.detailTextLabel?.text = ""
  303. }
  304. return cell
  305. }
  306. @objc func appMovedToBackground() {
  307. // Allow screen to turn off
  308. UIApplication.shared.isIdleTimerDisabled = false;
  309. // We want to always come back to the home screen
  310. tabBarController?.selectedIndex = 0
  311. if Storage.shared.backgroundRefreshType.value == .silentTune {
  312. backgroundTask.startBackgroundTask()
  313. }
  314. if Storage.shared.backgroundRefreshType.value != .none {
  315. BackgroundAlertManager.shared.startBackgroundAlert()
  316. }
  317. }
  318. @objc func appCameToForeground() {
  319. // reset screenlock state if needed
  320. UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value;
  321. if Storage.shared.backgroundRefreshType.value == .silentTune {
  322. backgroundTask.stopBackgroundTask()
  323. }
  324. if Storage.shared.backgroundRefreshType.value != .none {
  325. BackgroundAlertManager.shared.stopBackgroundAlert()
  326. }
  327. TaskScheduler.shared.checkTasksNow()
  328. checkAndNotifyVersionStatus()
  329. checkAppExpirationStatus()
  330. }
  331. func checkAndNotifyVersionStatus() {
  332. let versionManager = AppVersionManager()
  333. versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in
  334. let now = Date()
  335. // Check if the current version is blacklisted, or if there is a newer version available
  336. if isBlacklisted {
  337. let lastBlacklistShown = UserDefaultsRepository.lastBlacklistNotificationShown.value ?? Date.distantPast
  338. if now.timeIntervalSince(lastBlacklistShown) > 86400 { // 24 hours
  339. self.versionAlert(message: "The current version has a critical issue and should be updated as soon as possible.")
  340. UserDefaultsRepository.lastBlacklistNotificationShown.value = now
  341. UserDefaultsRepository.lastVersionUpdateNotificationShown.value = now
  342. }
  343. } else if isNewer {
  344. let lastVersionUpdateShown = UserDefaultsRepository.lastVersionUpdateNotificationShown.value ?? Date.distantPast
  345. if now.timeIntervalSince(lastVersionUpdateShown) > 1209600 { // 2 weeks
  346. self.versionAlert(message: "A new version is available: \(latestVersion ?? "Unknown"). It is recommended to update.")
  347. UserDefaultsRepository.lastVersionUpdateNotificationShown.value = now
  348. }
  349. }
  350. }
  351. }
  352. func versionAlert(title: String = "Update Available", message: String) {
  353. DispatchQueue.main.async {
  354. let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
  355. alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
  356. self.present(alert, animated: true)
  357. }
  358. }
  359. func checkAppExpirationStatus() {
  360. let now = Date()
  361. let expirationDate = BuildDetails.default.calculateExpirationDate()
  362. let weekBeforeExpiration = Calendar.current.date(byAdding: .day, value: -7, to: expirationDate)!
  363. if now >= weekBeforeExpiration {
  364. let lastExpirationShown = UserDefaultsRepository.lastExpirationNotificationShown.value ?? Date.distantPast
  365. if now.timeIntervalSince(lastExpirationShown) > 86400 { // 24 hours
  366. expirationAlert()
  367. UserDefaultsRepository.lastExpirationNotificationShown.value = now
  368. }
  369. }
  370. }
  371. func expirationAlert() {
  372. DispatchQueue.main.async {
  373. let alert = UIAlertController(title: "App Expiration Warning", message: "This app will expire in less than a week. Please rebuild to continue using it.", preferredStyle: .alert)
  374. alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
  375. self.present(alert, animated: true)
  376. }
  377. }
  378. @objc override func viewDidAppear(_ animated: Bool) {
  379. showHideNSDetails()
  380. }
  381. func stringFromTimeInterval(interval: TimeInterval) -> String {
  382. let interval = Int(interval)
  383. let minutes = (interval / 60) % 60
  384. let hours = (interval / 3600)
  385. return String(format: "%02d:%02d", hours, minutes)
  386. }
  387. func showHideNSDetails() {
  388. var isHidden = false
  389. var isEnabled = true
  390. if !IsNightscoutEnabled() {
  391. isHidden = true
  392. isEnabled = false
  393. }
  394. LoopStatusLabel.isHidden = isHidden
  395. if IsNotLooping {
  396. PredictionLabel.isHidden = true
  397. }
  398. else {
  399. PredictionLabel.isHidden = isHidden
  400. }
  401. infoTable.isHidden = isHidden
  402. if UserDefaultsRepository.hideInfoTable.value {
  403. infoTable.isHidden = true
  404. }
  405. if IsNightscoutEnabled() {
  406. isEnabled = true
  407. }
  408. guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return }
  409. nightscoutTab.isEnabled = isEnabled
  410. }
  411. func updateBadge(val: Int) {
  412. if UserDefaultsRepository.appBadge.value {
  413. let latestBG = String(val)
  414. UIApplication.shared.applicationIconBadgeNumber = Int(Localizer.removePeriodAndCommaForBadge(Localizer.toDisplayUnits(latestBG))) ?? val
  415. } else {
  416. UIApplication.shared.applicationIconBadgeNumber = 0
  417. }
  418. }
  419. func setBGTextColor() {
  420. if bgData.count > 0 {
  421. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  422. let latestBG = bgData[bgData.count - 1].sgv
  423. var color: NSUIColor = NSUIColor.label
  424. if UserDefaultsRepository.colorBGText.value {
  425. if Float(latestBG) >= UserDefaultsRepository.highLine.value {
  426. color = NSUIColor.systemYellow
  427. } else if Float(latestBG) <= UserDefaultsRepository.lowLine.value {
  428. color = NSUIColor.systemRed
  429. } else {
  430. color = NSUIColor.systemGreen
  431. }
  432. }
  433. BGText.textColor = color
  434. snoozer.BGLabel.textColor = color
  435. }
  436. }
  437. func bgDirectionGraphic(_ value:String)->String
  438. {
  439. if value == nil { return "-" }
  440. let //graphics:[String:String]=["Flat":"\u{2192}","DoubleUp":"\u{21C8}","SingleUp":"\u{2191}","FortyFiveUp":"\u{2197}\u{FE0E}","FortyFiveDown":"\u{2198}\u{FE0E}","SingleDown":"\u{2193}","DoubleDown":"\u{21CA}","None":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-"]
  441. graphics:[String:String]=["Flat":"→","DoubleUp":"↑↑","SingleUp":"↑","FortyFiveUp":"↗","FortyFiveDown":"↘︎","SingleDown":"↓","DoubleDown":"↓↓","None":"-","NONE":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-", "": "-"]
  442. return graphics[value]!
  443. }
  444. func writeCalendar() {
  445. self.store.requestCalendarAccess { (granted, error) in
  446. if !granted {
  447. LogManager.shared.log(category: .calendar, message: "Failed to get calendar access: \(String(describing: error))")
  448. return
  449. }
  450. self.processCalendarUpdates()
  451. }
  452. }
  453. func processCalendarUpdates() {
  454. if UserDefaultsRepository.calendarIdentifier.value == "" { return }
  455. if self.bgData.count < 1 { return }
  456. // This lets us fire the method to write Min Ago entries only once a minute starting after 6 minutes but allows new readings through
  457. let now = dateTimeUtils.getNowTimeIntervalUTC()
  458. let newestBGDate = bgData[bgData.count - 1].date
  459. if lastCalDate == newestBGDate {
  460. if (now - lastCalendarWriteAttemptTime) < 60 || (now - newestBGDate) < 360 {
  461. return
  462. }
  463. }
  464. // Create Event info
  465. var deltaBG = 0 // protect index out of bounds
  466. if self.bgData.count > 1 {
  467. deltaBG = self.bgData[self.bgData.count - 1].sgv - self.bgData[self.bgData.count - 2].sgv as Int
  468. }
  469. let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - self.bgData[self.bgData.count - 1].date) / 60
  470. var deltaString = ""
  471. if deltaBG < 0 {
  472. deltaString = Localizer.toDisplayUnits(String(deltaBG))
  473. }
  474. else
  475. {
  476. deltaString = "+" + Localizer.toDisplayUnits(String(deltaBG))
  477. }
  478. let direction = self.bgDirectionGraphic(self.bgData[self.bgData.count - 1].direction ?? "")
  479. var eventStartDate = Date(timeIntervalSince1970: self.bgData[self.bgData.count - 1].date)
  480. var eventEndDate = eventStartDate.addingTimeInterval(60 * 10)
  481. var eventTitle = UserDefaultsRepository.watchLine1.value
  482. if (UserDefaultsRepository.watchLine2.value.count > 1) {
  483. eventTitle += "\n" + UserDefaultsRepository.watchLine2.value
  484. }
  485. eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: Localizer.toDisplayUnits(String(self.bgData[self.bgData.count - 1].sgv)))
  486. eventTitle = eventTitle.replacingOccurrences(of: "%DIRECTION%", with: direction)
  487. eventTitle = eventTitle.replacingOccurrences(of: "%DELTA%", with: deltaString)
  488. if self.currentOverride != 1.0 {
  489. let val = Int( self.currentOverride*100)
  490. // let overrideText = String(format:"%f1", self.currentOverride*100)
  491. let text = String(val) + "%"
  492. eventTitle = eventTitle.replacingOccurrences(of: "%OVERRIDE%", with: text)
  493. } else {
  494. eventTitle = eventTitle.replacingOccurrences(of: "%OVERRIDE%", with: "")
  495. }
  496. eventTitle = eventTitle.replacingOccurrences(of: "%LOOP%", with: self.latestLoopStatusString)
  497. var minAgo = ""
  498. if deltaTime > 9 {
  499. // write old BG reading and continue pushing out end date to show last entry
  500. minAgo = String(Int(deltaTime)) + " min"
  501. eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60))
  502. }
  503. var basal = "~"
  504. if self.latestBasal != "" {
  505. basal = self.latestBasal
  506. }
  507. eventTitle = eventTitle.replacingOccurrences(of: "%MINAGO%", with: minAgo)
  508. eventTitle = eventTitle.replacingOccurrences(of: "%IOB%", with: latestIOB?.formattedValue() ?? "0")
  509. eventTitle = eventTitle.replacingOccurrences(of: "%COB%", with: latestCOB?.formattedValue() ?? "0")
  510. eventTitle = eventTitle.replacingOccurrences(of: "%BASAL%", with: basal)
  511. // Delete Events from last 2 hours and 2 hours in future
  512. var deleteStartDate = Date().addingTimeInterval(-60*60*2)
  513. var deleteEndDate = Date().addingTimeInterval(60*60*2)
  514. // guard solves for some ios upgrades removing the calendar
  515. guard let deleteCalendar = self.store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value) as? EKCalendar else { return }
  516. var predicate2 = self.store.predicateForEvents(withStart: deleteStartDate, end: deleteEndDate, calendars: [deleteCalendar])
  517. var eVDelete = self.store.events(matching: predicate2) as [EKEvent]?
  518. if eVDelete != nil {
  519. for i in eVDelete! {
  520. do {
  521. try self.store.remove(i, span: EKSpan.thisEvent, commit: true)
  522. } catch let error {
  523. print(error)
  524. }
  525. }
  526. }
  527. // Write New Event
  528. var event = EKEvent(eventStore: self.store)
  529. event.title = eventTitle
  530. event.startDate = eventStartDate
  531. event.endDate = eventEndDate
  532. event.calendar = self.store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value)
  533. do {
  534. try self.store.save(event, span: .thisEvent, commit: true)
  535. self.lastCalendarWriteAttemptTime = now
  536. self.lastCalDate = self.bgData[self.bgData.count - 1].date
  537. //UserDefaultsRepository.savedEventID.value = event.eventIdentifier //save event id to access this particular event later
  538. } catch {
  539. LogManager.shared.log(category: .calendar, message: "Error storing to the calendar")
  540. }
  541. }
  542. func persistentNotification(bgTime: TimeInterval)
  543. {
  544. if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 {
  545. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  546. snoozer.sendNotification(self, bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString, deltaVal: latestDeltaString, minAgoVal: latestMinAgoString, alertLabelVal: "Latest BG")
  547. }
  548. }
  549. // General Notifications
  550. func sendGeneralNotification(_ sender: Any, title: String, subtitle: String, body: String, timer: TimeInterval) {
  551. UNUserNotificationCenter.current().delegate = self
  552. let content = UNMutableNotificationContent()
  553. content.title = title
  554. content.subtitle = subtitle
  555. content.body = body
  556. content.categoryIdentifier = "noAction"
  557. content.sound = .default
  558. let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timer, repeats: false)
  559. let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
  560. UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
  561. }
  562. func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
  563. }
  564. // User has scrolled the chart
  565. func chartTranslated(_ chartView: ChartViewBase, dX: CGFloat, dY: CGFloat) {
  566. let isViewingLatestData = abs(BGChart.highestVisibleX - BGChart.chartXMax) < 0.001
  567. if isViewingLatestData {
  568. autoScrollPauseUntil = nil // User is back at the latest data, allow auto-scrolling
  569. } else {
  570. autoScrollPauseUntil = Date().addingTimeInterval(5 * 60) // User is viewing historical data, pause auto-scrolling
  571. }
  572. }
  573. func calculateMaxBgGraphValue() -> Float {
  574. return max(topBG, topPredictionBG)
  575. }
  576. }