MainViewController.swift 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
  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 Photos
  14. class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate {
  15. @IBOutlet weak var BGText: UILabel!
  16. @IBOutlet weak var DeltaText: UILabel!
  17. @IBOutlet weak var DirectionText: UILabel!
  18. @IBOutlet weak var BGChart: LineChartView!
  19. @IBOutlet weak var BGChartFull: LineChartView!
  20. @IBOutlet weak var MinAgoText: UILabel!
  21. @IBOutlet weak var infoTable: UITableView!
  22. @IBOutlet weak var Console: UITableViewCell!
  23. @IBOutlet weak var DragBar: UIImageView!
  24. @IBOutlet weak var PredictionLabel: UILabel!
  25. @IBOutlet weak var LoopStatusLabel: UILabel!
  26. @IBOutlet weak var statsPieChart: PieChartView!
  27. @IBOutlet weak var statsLowPercent: UILabel!
  28. @IBOutlet weak var statsInRangePercent: UILabel!
  29. @IBOutlet weak var statsHighPercent: UILabel!
  30. @IBOutlet weak var statsAvgBG: UILabel!
  31. @IBOutlet weak var statsEstA1C: UILabel!
  32. @IBOutlet weak var statsStdDev: UILabel!
  33. @IBOutlet weak var serverText: UILabel!
  34. @IBOutlet weak var statsView: UIView!
  35. @IBOutlet weak var smallGraphHeightConstraint: NSLayoutConstraint!
  36. let speechSynthesizer = AVSpeechSynthesizer()
  37. // Data Table class
  38. class infoData {
  39. public var name: String
  40. public var value: String
  41. init(name: String, value: String) {
  42. self.name = name
  43. self.value = value
  44. }
  45. }
  46. var appStateController: AppStateController?
  47. // Variables for BG Charts
  48. public var numPoints: Int = 13
  49. public var linePlotData: [Double] = []
  50. public var linePlotDataTime: [Double] = []
  51. var firstGraphLoad: Bool = true
  52. var firstBasalGraphLoad: Bool = true
  53. var minAgoBG: Double = 0.0
  54. var currentOverride = 1.0
  55. // Vars for NS Pull
  56. var mmol = false as Bool
  57. var urlUser = UserDefaultsRepository.url.value as String
  58. var token = UserDefaultsRepository.token.value as String
  59. var defaults : UserDefaults?
  60. let consoleLogging = true
  61. var timeofLastBGUpdate = 0 as TimeInterval
  62. var nsVerifiedAlerted = false
  63. var backgroundTask = BackgroundTask()
  64. // Refresh NS Data
  65. var timer = Timer()
  66. // check every 30 Seconds whether new bgvalues should be retrieved
  67. let timeInterval: TimeInterval = 30.0
  68. // Min Ago Timer
  69. var minAgoTimer = Timer()
  70. var minAgoTimeInterval: TimeInterval = 1.0
  71. // Check Alarms Timer
  72. // Don't check within 1 minute of alarm triggering to give the snoozer time to save data
  73. var checkAlarmTimer = Timer()
  74. var checkAlarmInterval: TimeInterval = 60.0
  75. var calTimer = Timer()
  76. var bgTimer = Timer()
  77. var cageSageTimer = Timer()
  78. var profileTimer = Timer()
  79. var deviceStatusTimer = Timer()
  80. var treatmentsTimer = Timer()
  81. var alarmTimer = Timer()
  82. var calendarTimer = Timer()
  83. var graphNowTimer = Timer()
  84. // Info Table Setup
  85. var tableData : [infoData] = []
  86. var derivedTableData: [infoData] = []
  87. var bgData: [ShareGlucoseData] = []
  88. var basalProfile: [basalProfileStruct] = []
  89. var basalData: [basalGraphStruct] = []
  90. var basalScheduleData: [basalGraphStruct] = []
  91. var bolusData: [bolusGraphStruct] = []
  92. var carbData: [carbGraphStruct] = []
  93. var overrideGraphData: [DataStructs.overrideStruct] = []
  94. var predictionData: [ShareGlucoseData] = []
  95. var bgCheckData: [ShareGlucoseData] = []
  96. var suspendGraphData: [DataStructs.timestampOnlyStruct] = []
  97. var resumeGraphData: [DataStructs.timestampOnlyStruct] = []
  98. var sensorStartGraphData: [DataStructs.timestampOnlyStruct] = []
  99. var noteGraphData: [DataStructs.noteStruct] = []
  100. var chartData = LineChartData()
  101. var newBGPulled = false
  102. var lastCalDate: Double = 0
  103. var latestDirectionString = ""
  104. var latestMinAgoString = ""
  105. var latestDeltaString = ""
  106. var latestLoopStatusString = ""
  107. var latestLoopTime: Double = 0
  108. var latestCOB = ""
  109. var latestBasal = ""
  110. var latestPumpVolume: Double = 50.0
  111. var latestIOB = ""
  112. var lastOverrideStartTime: TimeInterval = 0
  113. var lastOverrideEndTime: TimeInterval = 0
  114. var topBG: Float = UserDefaultsRepository.minBGScale.value
  115. var lastOverrideAlarm: TimeInterval = 0
  116. // share
  117. var bgDataShare: [ShareGlucoseData] = []
  118. var dexShare: ShareClient?;
  119. var dexVerifiedAlerted = false
  120. // calendar setup
  121. let store = EKEventStore()
  122. var snoozeTabItem: UITabBarItem = UITabBarItem()
  123. override func viewDidLoad() {
  124. super.viewDidLoad()
  125. // reset the infoTable names in case we add or delete items
  126. UserDefaultsRepository.infoNames.value.removeAll()
  127. UserDefaultsRepository.infoNames.value.append("IOB")
  128. UserDefaultsRepository.infoNames.value.append("COB")
  129. UserDefaultsRepository.infoNames.value.append("Basal")
  130. UserDefaultsRepository.infoNames.value.append("Override")
  131. UserDefaultsRepository.infoNames.value.append("Battery")
  132. UserDefaultsRepository.infoNames.value.append("Pump")
  133. UserDefaultsRepository.infoNames.value.append("SAGE")
  134. UserDefaultsRepository.infoNames.value.append("CAGE")
  135. UserDefaultsRepository.infoNames.value.append("Rec. Bolus")
  136. UserDefaultsRepository.infoNames.value.append("Pred.")
  137. // Reset deprecated settings
  138. UserDefaultsRepository.debugLog.value = false;
  139. UserDefaultsRepository.alwaysDownloadAllBG.value = true;
  140. // table view
  141. //infoTable.layer.borderColor = UIColor.darkGray.cgColor
  142. //infoTable.layer.borderWidth = 1.0
  143. //infoTable.layer.cornerRadius = 6
  144. infoTable.rowHeight = 24
  145. infoTable.dataSource = self
  146. infoTable.tableFooterView = UIView(frame: .zero) // get rid of extra rows
  147. infoTable.bounces = false
  148. infoTable.addBorder(toSide: .Left, withColor: UIColor.darkGray.cgColor, andThickness: 2)
  149. // initialize the tableData
  150. self.tableData = []
  151. for i in 0..<UserDefaultsRepository.infoNames.value.count {
  152. self.tableData.append(infoData(name:UserDefaultsRepository.infoNames.value[i], value:""))
  153. }
  154. createDerivedData()
  155. smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value)
  156. self.view.layoutIfNeeded()
  157. // TODO: need non-us server ?
  158. let shareUserName = UserDefaultsRepository.shareUserName.value
  159. let sharePassword = UserDefaultsRepository.sharePassword.value
  160. let shareServer = UserDefaultsRepository.shareServer.value == "US" ?KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue
  161. dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer )
  162. //print("Share: \(dexShare)")
  163. // setup show/hide small graph and stats
  164. BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value
  165. statsView.isHidden = !UserDefaultsRepository.showStats.value
  166. BGChart.delegate = self
  167. BGChartFull.delegate = self
  168. if UserDefaultsRepository.forceDarkMode.value {
  169. overrideUserInterfaceStyle = .dark
  170. self.tabBarController?.overrideUserInterfaceStyle = .dark
  171. }
  172. // Disable the snoozer tab unless an alarm is active
  173. //let tabBarControllerItems = self.tabBarController?.tabBar.items
  174. //if let arrayOfTabBarItems = tabBarControllerItems as AnyObject as? NSArray{
  175. // snoozeTabItem = arrayOfTabBarItems[2] as! UITabBarItem
  176. //}
  177. //snoozeTabItem.isEnabled = false;
  178. // Load the snoozer tab
  179. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  180. snoozer.loadViewIfNeeded()
  181. // Trigger foreground and background functions
  182. let notificationCenter = NotificationCenter.default
  183. notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
  184. notificationCenter.addObserver(self, selector: #selector(appCameToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
  185. // Setup the Graph
  186. if firstGraphLoad {
  187. createGraph()
  188. createSmallBGGraph()
  189. }
  190. // setup display for NS vs Dex
  191. showHideNSDetails()
  192. // Load Startup Data
  193. restartAllTimers()
  194. }
  195. override func viewWillAppear(_ animated: Bool) {
  196. // set screen lock
  197. UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value;
  198. // check the app state
  199. // TODO: move to a function ?
  200. if let appState = self.appStateController {
  201. if appState.chartSettingsChanged {
  202. // can look at settings flags to be more fine tuned
  203. self.updateBGGraphSettings()
  204. if ChartSettingsChangeEnum.smallGraphHeight.rawValue != 0 {
  205. smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value)
  206. self.view.layoutIfNeeded()
  207. }
  208. // reset the app state
  209. appState.chartSettingsChanged = false
  210. appState.chartSettingsChanges = 0
  211. }
  212. if appState.generalSettingsChanged {
  213. // settings for appBadge changed
  214. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.appBadgeChange.rawValue != 0 {
  215. }
  216. // settings for textcolor changed
  217. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.colorBGTextChange.rawValue != 0 {
  218. self.setBGTextColor()
  219. }
  220. // settings for showStats changed
  221. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showStatsChange.rawValue != 0 {
  222. statsView.isHidden = !UserDefaultsRepository.showStats.value
  223. }
  224. // settings for useIFCC changed
  225. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.useIFCCChange.rawValue != 0 {
  226. updateStats()
  227. }
  228. // settings for showSmallGraph changed
  229. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showSmallGraphChange.rawValue != 0 {
  230. BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value
  231. }
  232. // reset the app state
  233. appState.generalSettingsChanged = false
  234. appState.generalSettingsChanges = 0
  235. }
  236. if appState.infoDataSettingsChanged {
  237. createDerivedData()
  238. self.infoTable.reloadData()
  239. // reset
  240. appState.infoDataSettingsChanged = false
  241. }
  242. // add more processing of the app state
  243. }
  244. }
  245. private func createDerivedData() {
  246. self.derivedTableData = []
  247. for i in 0..<self.tableData.count {
  248. if(UserDefaultsRepository.infoVisible.value[UserDefaultsRepository.infoSort.value[i]]) {
  249. self.derivedTableData.append(self.tableData[UserDefaultsRepository.infoSort.value[i]])
  250. }
  251. }
  252. }
  253. // Info Table Functions
  254. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  255. //return tableData.count
  256. return derivedTableData.count
  257. }
  258. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  259. let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
  260. let values = derivedTableData[indexPath.row]
  261. cell.textLabel?.text = values.name
  262. cell.detailTextLabel?.text = values.value
  263. return cell
  264. }
  265. @objc func appMovedToBackground() {
  266. // Allow screen to turn off
  267. UIApplication.shared.isIdleTimerDisabled = false;
  268. // We want to always come back to the home screen
  269. tabBarController?.selectedIndex = 0
  270. // Cancel the current timer and start a fresh background timer using the settings value only if background task is enabled
  271. if UserDefaultsRepository.backgroundRefresh.value {
  272. backgroundTask.startBackgroundTask()
  273. }
  274. }
  275. @objc func appCameToForeground() {
  276. // reset screenlock state if needed
  277. UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value;
  278. // Cancel the background tasks, start a fresh timer
  279. if UserDefaultsRepository.backgroundRefresh.value {
  280. backgroundTask.stopBackgroundTask()
  281. }
  282. restartAllTimers()
  283. }
  284. @objc override func viewDidAppear(_ animated: Bool) {
  285. showHideNSDetails()
  286. }
  287. //update Min Ago Text. We need to call this separately because it updates between readings
  288. func updateMinAgo(){
  289. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Update min ago text") }
  290. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  291. if bgData.count > 0 {
  292. let deltaTime = (TimeInterval(Date().timeIntervalSince1970)-bgData[bgData.count - 1].date) / 60
  293. minAgoBG = Double(TimeInterval(Date().timeIntervalSince1970)-bgData[bgData.count - 1].date)
  294. MinAgoText.text = String(Int(deltaTime)) + " min ago"
  295. snoozer.MinAgoLabel.text = String(Int(deltaTime)) + " min ago"
  296. latestMinAgoString = String(Int(deltaTime)) + " min ago"
  297. } else {
  298. MinAgoText.text = ""
  299. snoozer.MinAgoLabel.text = ""
  300. latestMinAgoString = ""
  301. }
  302. }
  303. //Clear the info data before next pull. This ensures we aren't displaying old data if something fails.
  304. func clearLastInfoData(index: Int){
  305. tableData[index].value = ""
  306. }
  307. func stringFromTimeInterval(interval: TimeInterval) -> String {
  308. let interval = Int(interval)
  309. let minutes = (interval / 60) % 60
  310. let hours = (interval / 3600)
  311. return String(format: "%02d:%02d", hours, minutes)
  312. }
  313. func showHideNSDetails() {
  314. var isHidden = false
  315. var isEnabled = true
  316. if UserDefaultsRepository.url.value == "" || !UserDefaultsRepository.loopUser.value {
  317. isHidden = true
  318. isEnabled = false
  319. }
  320. LoopStatusLabel.isHidden = isHidden
  321. PredictionLabel.isHidden = isHidden
  322. infoTable.isHidden = isHidden
  323. if UserDefaultsRepository.hideInfoTable.value {
  324. infoTable.isHidden = true
  325. }
  326. if UserDefaultsRepository.url.value != "" {
  327. isEnabled = true
  328. }
  329. guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return }
  330. nightscoutTab.isEnabled = isEnabled
  331. }
  332. func updateBadge(val: Int) {
  333. DispatchQueue.main.async {
  334. if UserDefaultsRepository.appBadge.value {
  335. let latestBG = String(val)
  336. UIApplication.shared.applicationIconBadgeNumber = Int(bgUnits.removePeriodForBadge(bgUnits.toDisplayUnits(latestBG))) ?? val
  337. } else {
  338. UIApplication.shared.applicationIconBadgeNumber = 0
  339. }
  340. // if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "updated badge") }
  341. }
  342. }
  343. func setBGTextColor() {
  344. if bgData.count > 0 {
  345. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  346. let latestBG = bgData[bgData.count - 1].sgv
  347. var color: NSUIColor = NSUIColor.label
  348. if UserDefaultsRepository.colorBGText.value {
  349. if Float(latestBG) >= UserDefaultsRepository.highLine.value {
  350. color = NSUIColor.systemYellow
  351. } else if Float(latestBG) <= UserDefaultsRepository.lowLine.value {
  352. color = NSUIColor.systemRed
  353. } else {
  354. color = NSUIColor.systemGreen
  355. }
  356. }
  357. BGText.textColor = color
  358. snoozer.BGLabel.textColor = color
  359. }
  360. }
  361. func bgDirectionGraphic(_ value:String)->String
  362. {
  363. if value == nil { return "-" }
  364. 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":"-"]
  365. graphics:[String:String]=["Flat":"→","DoubleUp":"↑↑","SingleUp":"↑","FortyFiveUp":"↗","FortyFiveDown":"↘︎","SingleDown":"↓","DoubleDown":"↓↓","None":"-","NONE":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-", "": "-"]
  366. return graphics[value]!
  367. }
  368. // Test code to save an image of graph for viewing on watch
  369. func saveChartImage() {
  370. var originalColor = BGChart.backgroundColor
  371. BGChart.backgroundColor = NSUIColor.black
  372. guard var image = BGChart.getChartImage(transparent: true) else {
  373. BGChart.backgroundColor = originalColor
  374. return }
  375. var newImage = image.resizeImage(448.0, landscape: false, opaque: false, contentMode: .scaleAspectFit)
  376. createAlbums(name1: "Loop Follow", name2: "Loop Follow Old")
  377. if let collection1 = fetchAssetCollection("Loop Follow"), let collection2 = fetchAssetCollection("Loop Follow Old") {
  378. deleteExistingImagesFromCollection(collection: collection1)
  379. saveImageToAssetCollection(image, collection1: collection1, collection2: collection2)
  380. }
  381. BGChart.backgroundColor = originalColor
  382. }
  383. func createAlbums(name1: String, name2: String) {
  384. if let collection1 = fetchAssetCollection(name1) {
  385. } else {
  386. // Album does not exist, create it and attempt to save the image
  387. PHPhotoLibrary.shared().performChanges({
  388. PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: name1)
  389. }, completionHandler: { (success: Bool, error: Error?) in
  390. guard success == true && error == nil else {
  391. NSLog("Could not create the album")
  392. if let err = error {
  393. NSLog("Error: \(err)")
  394. }
  395. return
  396. }
  397. if let newCollection1 = self.fetchAssetCollection(name1) {
  398. }
  399. })
  400. }
  401. if let collection2 = fetchAssetCollection(name2) {
  402. } else {
  403. // Album does not exist, create it and attempt to save the image
  404. PHPhotoLibrary.shared().performChanges({
  405. PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: name2)
  406. }, completionHandler: { (success: Bool, error: Error?) in
  407. guard success == true && error == nil else {
  408. NSLog("Could not create the album")
  409. if let err = error {
  410. NSLog("Error: \(err)")
  411. }
  412. return
  413. }
  414. if let newCollection2 = self.fetchAssetCollection(name2) {
  415. }
  416. })
  417. }
  418. }
  419. func fetchAssetCollection(_ name: String) -> PHAssetCollection? {
  420. let fetchOption = PHFetchOptions()
  421. fetchOption.predicate = NSPredicate(format: "title == '" + name + "'")
  422. let fetchResult = PHAssetCollection.fetchAssetCollections(
  423. with: PHAssetCollectionType.album,
  424. subtype: PHAssetCollectionSubtype.albumRegular,
  425. options: fetchOption)
  426. return fetchResult.firstObject
  427. }
  428. func deleteOldImages() {
  429. if let collection = fetchAssetCollection("Loop Follow Old") {
  430. let library = PHPhotoLibrary.shared()
  431. library.performChanges({
  432. let fetchOptions = PHFetchOptions()
  433. let allPhotos = PHAsset.fetchAssets(in: collection, options: .none)
  434. PHAssetChangeRequest.deleteAssets(allPhotos)
  435. }, completionHandler: { (success: Bool, error: Error?) in
  436. guard success == true && error == nil else {
  437. NSLog("Could not delete the image")
  438. if let err = error {
  439. NSLog("Error: " + err.localizedDescription)
  440. }
  441. return
  442. }
  443. })
  444. }
  445. self.sendGeneralNotification(self, title: "Watch Face Cleanup", subtitle: "", body: "Delete old watch face graph images", timer: 86400)
  446. }
  447. func deleteExistingImagesFromCollection(collection: PHAssetCollection) {
  448. // This code removes existing photos from collection but does not delete them
  449. if collection.estimatedAssetCount < 1 { return }
  450. PHPhotoLibrary.shared().performChanges( {
  451. if let request = PHAssetCollectionChangeRequest(for: collection) {
  452. request.removeAssets(at: [0])
  453. }
  454. }, completionHandler: { (success: Bool, error: Error?) in
  455. guard success == true && error == nil else {
  456. NSLog("Could not delete the image")
  457. if let err = error {
  458. NSLog("Error: " + err.localizedDescription)
  459. }
  460. return
  461. }
  462. })
  463. }
  464. func saveImageToAssetCollection(_ image: UIImage, collection1: PHAssetCollection, collection2: PHAssetCollection) {
  465. PHPhotoLibrary.shared().performChanges({
  466. let creationRequest = PHAssetCreationRequest.creationRequestForAsset(from: image)
  467. if let request = PHAssetCollectionChangeRequest(for: collection1),
  468. let placeHolder = creationRequest.placeholderForCreatedAsset {
  469. request.addAssets([placeHolder] as NSFastEnumeration)
  470. }
  471. if let request2 = PHAssetCollectionChangeRequest(for: collection2),
  472. let placeHolder2 = creationRequest.placeholderForCreatedAsset {
  473. request2.addAssets([placeHolder2] as NSFastEnumeration)
  474. }
  475. }, completionHandler: { (success: Bool, error: Error?) in
  476. guard success == true && error == nil else {
  477. NSLog("Could not save the image")
  478. if let err = error {
  479. NSLog("Error: " + err.localizedDescription)
  480. }
  481. return
  482. }
  483. })
  484. }
  485. // Write calendar
  486. func writeCalendar() {
  487. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Write calendar start") }
  488. store.requestAccess(to: .event) {(granted, error) in
  489. if !granted { return }
  490. if UserDefaultsRepository.calendarIdentifier.value == "" { return }
  491. if self.bgData.count < 1 { return }
  492. // This lets us fire the method to write Min Ago entries only once a minute starting after 6 minutes but allows new readings through
  493. if self.lastCalDate == self.bgData[self.bgData.count - 1].date
  494. && (self.calTimer.isValid || (dateTimeUtils.getNowTimeIntervalUTC() - self.lastCalDate) < 360) {
  495. return
  496. }
  497. // Create Event info
  498. let deltaBG = self.bgData[self.bgData.count - 1].sgv - self.bgData[self.bgData.count - 2].sgv as Int
  499. let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - self.bgData[self.bgData.count - 1].date) / 60
  500. var deltaString = ""
  501. if deltaBG < 0 {
  502. deltaString = bgUnits.toDisplayUnits(String(deltaBG))
  503. }
  504. else
  505. {
  506. deltaString = "+" + bgUnits.toDisplayUnits(String(deltaBG))
  507. }
  508. let direction = self.bgDirectionGraphic(self.bgData[self.bgData.count - 1].direction ?? "")
  509. var eventStartDate = Date(timeIntervalSince1970: self.bgData[self.bgData.count - 1].date)
  510. // if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Calendar start date") }
  511. var eventEndDate = eventStartDate.addingTimeInterval(60 * 10)
  512. var eventTitle = UserDefaultsRepository.watchLine1.value
  513. if (UserDefaultsRepository.watchLine2.value.count > 1) {
  514. eventTitle += "\n" + UserDefaultsRepository.watchLine2.value
  515. }
  516. eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: bgUnits.toDisplayUnits(String(self.bgData[self.bgData.count - 1].sgv)))
  517. eventTitle = eventTitle.replacingOccurrences(of: "%DIRECTION%", with: direction)
  518. eventTitle = eventTitle.replacingOccurrences(of: "%DELTA%", with: deltaString)
  519. if self.currentOverride != 1.0 {
  520. let val = Int( self.currentOverride*100)
  521. // let overrideText = String(format:"%f1", self.currentOverride*100)
  522. let text = String(val) + "%"
  523. eventTitle = eventTitle.replacingOccurrences(of: "%OVERRIDE%", with: text)
  524. } else {
  525. eventTitle = eventTitle.replacingOccurrences(of: "%OVERRIDE%", with: "")
  526. }
  527. eventTitle = eventTitle.replacingOccurrences(of: "%LOOP%", with: self.latestLoopStatusString)
  528. var minAgo = ""
  529. if deltaTime > 9 {
  530. // write old BG reading and continue pushing out end date to show last entry
  531. minAgo = String(Int(deltaTime)) + " min"
  532. eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60))
  533. }
  534. var cob = "0"
  535. if self.latestCOB != "" {
  536. cob = self.latestCOB
  537. }
  538. var basal = "~"
  539. if self.latestBasal != "" {
  540. basal = self.latestBasal
  541. }
  542. var iob = "0"
  543. if self.latestIOB != "" {
  544. iob = self.latestIOB
  545. }
  546. eventTitle = eventTitle.replacingOccurrences(of: "%MINAGO%", with: minAgo)
  547. eventTitle = eventTitle.replacingOccurrences(of: "%IOB%", with: iob)
  548. eventTitle = eventTitle.replacingOccurrences(of: "%COB%", with: cob)
  549. eventTitle = eventTitle.replacingOccurrences(of: "%BASAL%", with: basal)
  550. // Delete Events from last 2 hours and 2 hours in future
  551. var deleteStartDate = Date().addingTimeInterval(-60*60*2)
  552. var deleteEndDate = Date().addingTimeInterval(60*60*2)
  553. // guard solves for some ios upgrades removing the calendar
  554. guard let deleteCalendar = self.store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value) as? EKCalendar else { return }
  555. var predicate2 = self.store.predicateForEvents(withStart: deleteStartDate, end: deleteEndDate, calendars: [deleteCalendar])
  556. var eVDelete = self.store.events(matching: predicate2) as [EKEvent]?
  557. if eVDelete != nil {
  558. for i in eVDelete! {
  559. do {
  560. try self.store.remove(i, span: EKSpan.thisEvent, commit: true)
  561. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Calendar Delete") }
  562. } catch let error {
  563. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Error - Calendar Delete") }
  564. print(error)
  565. }
  566. }
  567. }
  568. // Write New Event
  569. var event = EKEvent(eventStore: self.store)
  570. event.title = eventTitle
  571. event.startDate = eventStartDate
  572. event.endDate = eventEndDate
  573. event.calendar = self.store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value)
  574. do {
  575. try self.store.save(event, span: .thisEvent, commit: true)
  576. self.calTimer.invalidate()
  577. self.startCalTimer(time: (60 * 1))
  578. self.lastCalDate = self.bgData[self.bgData.count - 1].date
  579. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Calendar Write: " + eventTitle) }
  580. //UserDefaultsRepository.savedEventID.value = event.eventIdentifier //save event id to access this particular event later
  581. } catch {
  582. // Display error to user
  583. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Error: Calendar Write") }
  584. }
  585. if UserDefaultsRepository.saveImage.value {
  586. DispatchQueue.main.async {
  587. self.saveChartImage()
  588. }
  589. }
  590. }
  591. }
  592. func persistentNotification(bgTime: TimeInterval)
  593. {
  594. if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 {
  595. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  596. snoozer.sendNotification(self, bgVal: bgUnits.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString, deltaVal: bgUnits.toDisplayUnits(String(latestDeltaString)), minAgoVal: latestMinAgoString, alertLabelVal: "Latest BG")
  597. }
  598. }
  599. func writeDebugLog(value: String) {
  600. DispatchQueue.main.async {
  601. var logText = "\n" + dateTimeUtils.printNow() + " - " + value
  602. print(logText)
  603. guard let debug = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  604. if debug.debugTextView.text.lengthOfBytes(using: .utf8) > 20000 {
  605. debug.debugTextView.text = ""
  606. }
  607. debug.debugTextView.text += logText
  608. }
  609. }
  610. // General Notifications
  611. func sendGeneralNotification(_ sender: Any, title: String, subtitle: String, body: String, timer: TimeInterval) {
  612. UNUserNotificationCenter.current().delegate = self
  613. let content = UNMutableNotificationContent()
  614. content.title = title
  615. content.subtitle = subtitle
  616. content.body = body
  617. content.categoryIdentifier = "noAction"
  618. content.sound = .default
  619. let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timer, repeats: false)
  620. let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
  621. UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
  622. }
  623. func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
  624. }
  625. }