MainViewController.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  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. // Synchronize info types to ensure arrays are the correct size
  130. UserDefaultsRepository.synchronizeInfoTypes()
  131. // Reset deprecated settings
  132. UserDefaultsRepository.debugLog.value = false;
  133. infoTable.rowHeight = 21
  134. infoTable.dataSource = self
  135. infoTable.tableFooterView = UIView(frame: .zero)
  136. infoTable.bounces = false
  137. infoTable.addBorder(toSide: .Left, withColor: UIColor.darkGray.cgColor, andThickness: 2)
  138. self.infoManager = InfoManager(tableView: infoTable)
  139. smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value)
  140. self.view.layoutIfNeeded()
  141. let shareUserName = UserDefaultsRepository.shareUserName.value
  142. let sharePassword = UserDefaultsRepository.sharePassword.value
  143. let shareServer = UserDefaultsRepository.shareServer.value == "US" ?KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue
  144. dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer )
  145. // setup show/hide small graph and stats
  146. BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value
  147. statsView.isHidden = !UserDefaultsRepository.showStats.value
  148. BGChart.delegate = self
  149. BGChartFull.delegate = self
  150. if UserDefaultsRepository.forceDarkMode.value {
  151. overrideUserInterfaceStyle = .dark
  152. self.tabBarController?.overrideUserInterfaceStyle = .dark
  153. }
  154. // Load the snoozer tab
  155. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  156. snoozer.loadViewIfNeeded()
  157. // Trigger foreground and background functions
  158. let notificationCenter = NotificationCenter.default
  159. notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
  160. notificationCenter.addObserver(self, selector: #selector(appCameToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
  161. // Setup the Graph
  162. if firstGraphLoad {
  163. createGraph()
  164. createSmallBGGraph()
  165. }
  166. // setup display for NS vs Dex
  167. showHideNSDetails()
  168. scheduleAllTasks()
  169. // Set up refreshScrollView for BGText
  170. refreshScrollView = UIScrollView()
  171. refreshScrollView.translatesAutoresizingMaskIntoConstraints = false
  172. refreshScrollView.alwaysBounceVertical = true
  173. view.addSubview(refreshScrollView)
  174. NSLayoutConstraint.activate([
  175. refreshScrollView.leadingAnchor.constraint(equalTo: BGText.leadingAnchor),
  176. refreshScrollView.trailingAnchor.constraint(equalTo: BGText.trailingAnchor),
  177. refreshScrollView.topAnchor.constraint(equalTo: BGText.topAnchor),
  178. refreshScrollView.bottomAnchor.constraint(equalTo: BGText.bottomAnchor)
  179. ])
  180. refreshControl = UIRefreshControl()
  181. refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
  182. refreshScrollView.addSubview(refreshControl)
  183. // Add this line to prevent scrolling in other directions
  184. refreshScrollView.alwaysBounceVertical = true
  185. refreshScrollView.delegate = self
  186. NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: NSNotification.Name("refresh"), object: nil)
  187. }
  188. deinit {
  189. NotificationCenter.default.removeObserver(self, name: NSNotification.Name("refresh"), object: nil)
  190. }
  191. // Clean all timers and start new ones when refreshing
  192. @objc func refresh() {
  193. LogManager.shared.log(category: .general, message: "Refreshing")
  194. // Clear prediction for both Loop or OpenAPS
  195. // Check if Loop prediction data exists and clear it if necessary
  196. if !predictionData.isEmpty {
  197. predictionData.removeAll()
  198. updatePredictionGraph()
  199. }
  200. // Check if OpenAPS prediction data exists and clear it if necessary
  201. let openAPSDataIndices = [12, 13, 14, 15]
  202. for dataIndex in openAPSDataIndices {
  203. let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  204. let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  205. if !mainChart.entries.isEmpty || !smallChart.entries.isEmpty {
  206. updatePredictionGraphGeneric(
  207. dataIndex: dataIndex,
  208. predictionData: [],
  209. chartLabel: "",
  210. color: UIColor.systemGray
  211. )
  212. }
  213. }
  214. MinAgoText.text = "Refreshing"
  215. scheduleAllTasks()
  216. currentCage = nil
  217. currentSage = nil
  218. currentIage = nil
  219. lastSpeechTime = nil
  220. refreshControl.endRefreshing()
  221. }
  222. // Scroll down BGText when refreshing
  223. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  224. if scrollView == refreshScrollView {
  225. let yOffset = scrollView.contentOffset.y
  226. if yOffset < 0 {
  227. BGText.transform = CGAffineTransform(translationX: 0, y: -yOffset)
  228. } else {
  229. BGText.transform = CGAffineTransform.identity
  230. }
  231. }
  232. }
  233. override func viewWillAppear(_ animated: Bool) {
  234. // set screen lock
  235. UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value;
  236. // check the app state
  237. // TODO: move to a function ?
  238. if let appState = self.appStateController {
  239. if appState.chartSettingsChanged {
  240. // can look at settings flags to be more fine tuned
  241. self.updateBGGraphSettings()
  242. if ChartSettingsChangeEnum.smallGraphHeight.rawValue != 0 {
  243. smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value)
  244. self.view.layoutIfNeeded()
  245. }
  246. // reset the app state
  247. appState.chartSettingsChanged = false
  248. appState.chartSettingsChanges = 0
  249. }
  250. if appState.generalSettingsChanged {
  251. // settings for appBadge changed
  252. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.appBadgeChange.rawValue != 0 {
  253. }
  254. // settings for textcolor changed
  255. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.colorBGTextChange.rawValue != 0 {
  256. self.setBGTextColor()
  257. }
  258. // settings for showStats changed
  259. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showStatsChange.rawValue != 0 {
  260. statsView.isHidden = !UserDefaultsRepository.showStats.value
  261. }
  262. // settings for useIFCC changed
  263. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.useIFCCChange.rawValue != 0 {
  264. updateStats()
  265. }
  266. // settings for showSmallGraph changed
  267. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showSmallGraphChange.rawValue != 0 {
  268. BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value
  269. }
  270. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showDisplayNameChange.rawValue != 0 {
  271. self.updateServerText()
  272. }
  273. // reset the app state
  274. appState.generalSettingsChanged = false
  275. appState.generalSettingsChanges = 0
  276. }
  277. if appState.infoDataSettingsChanged {
  278. self.infoTable.reloadData()
  279. // reset
  280. appState.infoDataSettingsChanged = false
  281. }
  282. // add more processing of the app state
  283. }
  284. }
  285. // Info Table Functions
  286. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  287. guard let infoManager = infoManager else {
  288. return 0
  289. }
  290. return infoManager.numberOfRows()
  291. }
  292. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  293. let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
  294. if let values = infoManager.dataForIndexPath(indexPath) {
  295. cell.textLabel?.text = values.name
  296. cell.detailTextLabel?.text = values.value
  297. } else {
  298. cell.textLabel?.text = ""
  299. cell.detailTextLabel?.text = ""
  300. }
  301. return cell
  302. }
  303. @objc func appMovedToBackground() {
  304. // Allow screen to turn off
  305. UIApplication.shared.isIdleTimerDisabled = false;
  306. // We want to always come back to the home screen
  307. tabBarController?.selectedIndex = 0
  308. if Storage.shared.backgroundRefreshType.value == .silentTune {
  309. backgroundTask.startBackgroundTask()
  310. }
  311. if Storage.shared.backgroundRefreshType.value != .none {
  312. BackgroundAlertManager.shared.startBackgroundAlert()
  313. }
  314. }
  315. @objc func appCameToForeground() {
  316. // reset screenlock state if needed
  317. UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value;
  318. if Storage.shared.backgroundRefreshType.value == .silentTune {
  319. backgroundTask.stopBackgroundTask()
  320. }
  321. if Storage.shared.backgroundRefreshType.value != .none {
  322. BackgroundAlertManager.shared.stopBackgroundAlert()
  323. }
  324. TaskScheduler.shared.checkTasksNow()
  325. checkAndNotifyVersionStatus()
  326. checkAppExpirationStatus()
  327. }
  328. func checkAndNotifyVersionStatus() {
  329. let versionManager = AppVersionManager()
  330. versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in
  331. let now = Date()
  332. // Check if the current version is blacklisted, or if there is a newer version available
  333. if isBlacklisted {
  334. let lastBlacklistShown = UserDefaultsRepository.lastBlacklistNotificationShown.value ?? Date.distantPast
  335. if now.timeIntervalSince(lastBlacklistShown) > 86400 { // 24 hours
  336. self.versionAlert(message: "The current version has a critical issue and should be updated as soon as possible.")
  337. UserDefaultsRepository.lastBlacklistNotificationShown.value = now
  338. UserDefaultsRepository.lastVersionUpdateNotificationShown.value = now
  339. }
  340. } else if isNewer {
  341. let lastVersionUpdateShown = UserDefaultsRepository.lastVersionUpdateNotificationShown.value ?? Date.distantPast
  342. if now.timeIntervalSince(lastVersionUpdateShown) > 1209600 { // 2 weeks
  343. self.versionAlert(message: "A new version is available: \(latestVersion ?? "Unknown"). It is recommended to update.")
  344. UserDefaultsRepository.lastVersionUpdateNotificationShown.value = now
  345. }
  346. }
  347. }
  348. }
  349. func versionAlert(title: String = "Update Available", message: String) {
  350. DispatchQueue.main.async {
  351. let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
  352. alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
  353. self.present(alert, animated: true)
  354. }
  355. }
  356. func checkAppExpirationStatus() {
  357. let now = Date()
  358. let expirationDate = BuildDetails.default.calculateExpirationDate()
  359. let weekBeforeExpiration = Calendar.current.date(byAdding: .day, value: -7, to: expirationDate)!
  360. if now >= weekBeforeExpiration {
  361. let lastExpirationShown = UserDefaultsRepository.lastExpirationNotificationShown.value ?? Date.distantPast
  362. if now.timeIntervalSince(lastExpirationShown) > 86400 { // 24 hours
  363. expirationAlert()
  364. UserDefaultsRepository.lastExpirationNotificationShown.value = now
  365. }
  366. }
  367. }
  368. func expirationAlert() {
  369. DispatchQueue.main.async {
  370. 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)
  371. alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
  372. self.present(alert, animated: true)
  373. }
  374. }
  375. @objc override func viewDidAppear(_ animated: Bool) {
  376. showHideNSDetails()
  377. }
  378. func stringFromTimeInterval(interval: TimeInterval) -> String {
  379. let interval = Int(interval)
  380. let minutes = (interval / 60) % 60
  381. let hours = (interval / 3600)
  382. return String(format: "%02d:%02d", hours, minutes)
  383. }
  384. func showHideNSDetails() {
  385. var isHidden = false
  386. var isEnabled = true
  387. if !IsNightscoutEnabled() {
  388. isHidden = true
  389. isEnabled = false
  390. }
  391. LoopStatusLabel.isHidden = isHidden
  392. if IsNotLooping {
  393. PredictionLabel.isHidden = true
  394. }
  395. else {
  396. PredictionLabel.isHidden = isHidden
  397. }
  398. infoTable.isHidden = isHidden
  399. if UserDefaultsRepository.hideInfoTable.value {
  400. infoTable.isHidden = true
  401. }
  402. if IsNightscoutEnabled() {
  403. isEnabled = true
  404. }
  405. guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return }
  406. nightscoutTab.isEnabled = isEnabled
  407. }
  408. func updateBadge(val: Int) {
  409. if UserDefaultsRepository.appBadge.value {
  410. let latestBG = String(val)
  411. UIApplication.shared.applicationIconBadgeNumber = Int(Localizer.removePeriodAndCommaForBadge(Localizer.toDisplayUnits(latestBG))) ?? val
  412. } else {
  413. UIApplication.shared.applicationIconBadgeNumber = 0
  414. }
  415. }
  416. func setBGTextColor() {
  417. if bgData.count > 0 {
  418. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  419. let latestBG = bgData[bgData.count - 1].sgv
  420. var color: NSUIColor = NSUIColor.label
  421. if UserDefaultsRepository.colorBGText.value {
  422. if Float(latestBG) >= UserDefaultsRepository.highLine.value {
  423. color = NSUIColor.systemYellow
  424. } else if Float(latestBG) <= UserDefaultsRepository.lowLine.value {
  425. color = NSUIColor.systemRed
  426. } else {
  427. color = NSUIColor.systemGreen
  428. }
  429. }
  430. BGText.textColor = color
  431. snoozer.BGLabel.textColor = color
  432. }
  433. }
  434. func bgDirectionGraphic(_ value:String)->String
  435. {
  436. if value == nil { return "-" }
  437. 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":"-"]
  438. graphics:[String:String]=["Flat":"→","DoubleUp":"↑↑","SingleUp":"↑","FortyFiveUp":"↗","FortyFiveDown":"↘︎","SingleDown":"↓","DoubleDown":"↓↓","None":"-","NONE":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-", "": "-"]
  439. return graphics[value]!
  440. }
  441. func writeCalendar() {
  442. if UserDefaultsRepository.debugLog.value {
  443. self.writeDebugLog(value: "Write calendar start")
  444. }
  445. self.store.requestCalendarAccess { (granted, error) in
  446. if !granted {
  447. print("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. // if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Calendar start date") }
  481. var eventEndDate = eventStartDate.addingTimeInterval(60 * 10)
  482. var eventTitle = UserDefaultsRepository.watchLine1.value
  483. if (UserDefaultsRepository.watchLine2.value.count > 1) {
  484. eventTitle += "\n" + UserDefaultsRepository.watchLine2.value
  485. }
  486. eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: Localizer.toDisplayUnits(String(self.bgData[self.bgData.count - 1].sgv)))
  487. eventTitle = eventTitle.replacingOccurrences(of: "%DIRECTION%", with: direction)
  488. eventTitle = eventTitle.replacingOccurrences(of: "%DELTA%", with: deltaString)
  489. if self.currentOverride != 1.0 {
  490. let val = Int( self.currentOverride*100)
  491. // let overrideText = String(format:"%f1", self.currentOverride*100)
  492. let text = String(val) + "%"
  493. eventTitle = eventTitle.replacingOccurrences(of: "%OVERRIDE%", with: text)
  494. } else {
  495. eventTitle = eventTitle.replacingOccurrences(of: "%OVERRIDE%", with: "")
  496. }
  497. eventTitle = eventTitle.replacingOccurrences(of: "%LOOP%", with: self.latestLoopStatusString)
  498. var minAgo = ""
  499. if deltaTime > 9 {
  500. // write old BG reading and continue pushing out end date to show last entry
  501. minAgo = String(Int(deltaTime)) + " min"
  502. eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60))
  503. }
  504. var basal = "~"
  505. if self.latestBasal != "" {
  506. basal = self.latestBasal
  507. }
  508. eventTitle = eventTitle.replacingOccurrences(of: "%MINAGO%", with: minAgo)
  509. eventTitle = eventTitle.replacingOccurrences(of: "%IOB%", with: latestIOB?.formattedValue() ?? "0")
  510. eventTitle = eventTitle.replacingOccurrences(of: "%COB%", with: latestCOB?.formattedValue() ?? "0")
  511. eventTitle = eventTitle.replacingOccurrences(of: "%BASAL%", with: basal)
  512. // Delete Events from last 2 hours and 2 hours in future
  513. var deleteStartDate = Date().addingTimeInterval(-60*60*2)
  514. var deleteEndDate = Date().addingTimeInterval(60*60*2)
  515. // guard solves for some ios upgrades removing the calendar
  516. guard let deleteCalendar = self.store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value) as? EKCalendar else { return }
  517. var predicate2 = self.store.predicateForEvents(withStart: deleteStartDate, end: deleteEndDate, calendars: [deleteCalendar])
  518. var eVDelete = self.store.events(matching: predicate2) as [EKEvent]?
  519. if eVDelete != nil {
  520. for i in eVDelete! {
  521. do {
  522. try self.store.remove(i, span: EKSpan.thisEvent, commit: true)
  523. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Calendar Delete") }
  524. } catch let error {
  525. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Error - Calendar Delete") }
  526. print(error)
  527. }
  528. }
  529. }
  530. // Write New Event
  531. var event = EKEvent(eventStore: self.store)
  532. event.title = eventTitle
  533. event.startDate = eventStartDate
  534. event.endDate = eventEndDate
  535. event.calendar = self.store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value)
  536. do {
  537. try self.store.save(event, span: .thisEvent, commit: true)
  538. self.lastCalendarWriteAttemptTime = now
  539. self.lastCalDate = self.bgData[self.bgData.count - 1].date
  540. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Calendar Write: " + eventTitle) }
  541. //UserDefaultsRepository.savedEventID.value = event.eventIdentifier //save event id to access this particular event later
  542. } catch {
  543. print("*** Error storing to the calendar")
  544. // Display error to user
  545. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Error: Calendar Write") }
  546. }
  547. }
  548. func persistentNotification(bgTime: TimeInterval)
  549. {
  550. if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 {
  551. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  552. snoozer.sendNotification(self, bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString, deltaVal: Localizer.toDisplayUnits(String(latestDeltaString)), minAgoVal: latestMinAgoString, alertLabelVal: "Latest BG")
  553. }
  554. }
  555. @available(*, deprecated, message: "Use LogManager instead.")
  556. func writeDebugLog(value: String) {
  557. DispatchQueue.main.async {
  558. var logText = "\n" + dateTimeUtils.printNow() + " - " + value
  559. print(logText)
  560. guard let debug = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  561. if debug.debugTextView.text.lengthOfBytes(using: .utf8) > 20000 {
  562. debug.debugTextView.text = ""
  563. }
  564. debug.debugTextView.text += logText
  565. }
  566. }
  567. // General Notifications
  568. func sendGeneralNotification(_ sender: Any, title: String, subtitle: String, body: String, timer: TimeInterval) {
  569. UNUserNotificationCenter.current().delegate = self
  570. let content = UNMutableNotificationContent()
  571. content.title = title
  572. content.subtitle = subtitle
  573. content.body = body
  574. content.categoryIdentifier = "noAction"
  575. content.sound = .default
  576. let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timer, repeats: false)
  577. let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
  578. UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
  579. }
  580. func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
  581. }
  582. // User has scrolled the chart
  583. func chartTranslated(_ chartView: ChartViewBase, dX: CGFloat, dY: CGFloat) {
  584. let isViewingLatestData = abs(BGChart.highestVisibleX - BGChart.chartXMax) < 0.001
  585. if isViewingLatestData {
  586. autoScrollPauseUntil = nil // User is back at the latest data, allow auto-scrolling
  587. } else {
  588. autoScrollPauseUntil = Date().addingTimeInterval(5 * 60) // User is viewing historical data, pause auto-scrolling
  589. }
  590. }
  591. func calculateMaxBgGraphValue() -> Float {
  592. return max(topBG, topPredictionBG)
  593. }
  594. }