From 87ccea9bbad18e7553f81c05d8adee053f49c956 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 21:47:03 +0000 Subject: [PATCH 01/23] Bump rexml from 3.2.6 to 3.2.8 Bumps [rexml](https://github.com/ruby/rexml) from 3.2.6 to 3.2.8. - [Release notes](https://github.com/ruby/rexml/releases) - [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md) - [Commits](https://github.com/ruby/rexml/compare/v3.2.6...v3.2.8) --- updated-dependencies: - dependency-name: rexml dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5ee82f222..a4818f81e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -172,7 +172,8 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.2.8) + strscan (>= 3.0.9) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -185,6 +186,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -216,6 +218,7 @@ PLATFORMS arm64-darwin-22 arm64-darwin-23 x86_64-darwin-19 + x86_64-linux DEPENDENCIES fastlane From 0e685a8981039050a5f282e783216c3d0760175f Mon Sep 17 00:00:00 2001 From: Paul Plant <37302780+paulplant@users.noreply.github.com> Date: Mon, 6 May 2024 19:51:27 +0200 Subject: [PATCH 02/23] dynamic notifications + snooze all implementation --- .../Base.lproj/MainInterface.storyboard | 119 ++++++++++ .../Info.plist | 28 +++ .../NotificationViewController.swift | 86 +++++++ ...otification Context Extension.entitlements | 10 + xdrip.xcodeproj/project.pbxproj | 211 +++++++++++++++++- xdrip/Constants/ConstantsAlerts.swift | 24 ++ .../ConstantsGlucoseChartSwiftUI.swift | 19 +- xdrip/Extensions/UIView.swift | 18 ++ xdrip/Extensions/UserDefaults.swift | 47 ++++ xdrip/Extensions/View.swift | 1 + xdrip/Managers/Alerts/AlertKind.swift | 59 ++--- xdrip/Managers/Alerts/AlertManager.swift | 191 +++++++++++----- .../Alerts/AlertNotificationDictionary.swift | 25 +++ xdrip/Managers/Alerts/AlertSnoozeStatus.swift | 31 +++ xdrip/Managers/Alerts/AlertUrgencyType.swift | 105 +++++++++ xdrip/Managers/Charts/GlucoseChartType.swift | 43 +++- xdrip/Storyboards/Base.lproj/Main.storyboard | 83 +++++-- xdrip/Texts/TextsAlerts.swift | 14 +- xdrip/Texts/TextsHomeView.swift | 16 ++ .../PickerViewController.swift | 3 + .../RootViewController.swift | 105 ++++++++- .../SnoozeViewController.swift | 125 +++++++++-- 22 files changed, 1210 insertions(+), 153 deletions(-) create mode 100644 xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard create mode 100644 xDrip Notification Context Extension/Info.plist create mode 100644 xDrip Notification Context Extension/NotificationViewController.swift create mode 100644 xDrip Notification Context Extension/xDrip Notification Context Extension.entitlements create mode 100644 xdrip/Extensions/UIView.swift create mode 100644 xdrip/Managers/Alerts/AlertNotificationDictionary.swift create mode 100644 xdrip/Managers/Alerts/AlertSnoozeStatus.swift create mode 100644 xdrip/Managers/Alerts/AlertUrgencyType.swift diff --git a/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard b/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard new file mode 100644 index 000000000..557f615e4 --- /dev/null +++ b/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xDrip Notification Context Extension/Info.plist b/xDrip Notification Context Extension/Info.plist new file mode 100644 index 000000000..b73e4f55b --- /dev/null +++ b/xDrip Notification Context Extension/Info.plist @@ -0,0 +1,28 @@ + + + + + AppGroupIdentifier + $(APP_GROUP_IDENTIFIER) + NSExtension + + NSExtensionAttributes + + UNNotificationExtensionCategory + snoozeCategoryIdentifier + UNNotificationExtensionDefaultContentHidden + + UNNotificationExtensionInitialContentSizeRatio + 0 + UNNotificationExtensionOverridesDefaultTitle + + UNNotificationExtensionUserInteractionEnabled + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.usernotifications.content-extension + + + diff --git a/xDrip Notification Context Extension/NotificationViewController.swift b/xDrip Notification Context Extension/NotificationViewController.swift new file mode 100644 index 000000000..353e104e3 --- /dev/null +++ b/xDrip Notification Context Extension/NotificationViewController.swift @@ -0,0 +1,86 @@ +// +// NotificationViewController.swift +// xDrip Notification Context Extension +// +// Created by Paul Plant on 6/5/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import UIKit +import UserNotifications +import UserNotificationsUI + +class NotificationViewController: UIViewController, UNNotificationContentExtension { + + @IBOutlet weak var alertIconOutlet: UIImageView! + @IBOutlet weak var alertTitleLabel: UILabel! + @IBOutlet weak var bannerOutlet: UIView! + + @IBOutlet weak var glucoseChartImage: UIImageView! + @IBOutlet weak var bgValueLabel: UILabel! + @IBOutlet weak var deltaLabel: UILabel! + @IBOutlet weak var unitLabel: UILabel! + + override func viewDidLoad() { + super.viewDidLoad() + + // set the notification view size and update it + self.preferredContentSize = CGSize(width: UIScreen.main.bounds.size.width, height: 310) + self.view.setNeedsUpdateConstraints() + self.view.setNeedsLayout() + } + + func didReceive(_ notification: UNNotification) { + + // pull the userInfo dictionary from the received notification + let userInfo = notification.request.content.userInfo + + // set the image and colours based upon the alertUrgencyType + if let alertUrgencyType = AlertUrgencyType(rawValue: userInfo["alertUrgencyTypeRawValue"] as? Int ?? 0) { + if let alertImageString = alertUrgencyType.alertImageString { + alertIconOutlet.image = UIImage(systemName: alertImageString) + alertIconOutlet.tintColor = alertUrgencyType.bannerTextColor + } + + alertTitleLabel.textColor = alertUrgencyType.bannerTextColor + bannerOutlet.backgroundColor = alertUrgencyType.bannerBackgroundColor + } + + // set the title label. This is common for all notifications + alertTitleLabel.text = userInfo["alertTitle"] as? String ?? "" + + var bgValueAndTrendString = userInfo["bgValueString"] as? String ?? "" + + if let trendString = userInfo["trendString"] as? String { + bgValueAndTrendString += trendString + } + + bgValueLabel.text = bgValueAndTrendString + + // set the bg value label color as per the rest of the app UI color + // it's a bit of a workaround to keep the dictionary as codable + if let BgRangeDescriptionAsInt = userInfo["BgRangeDescriptionAsInt"] as? Int { + switch BgRangeDescriptionAsInt { + case 0: // inRange + bgValueLabel.textColor = .green + case 1: // notUrgent + bgValueLabel.textColor = .yellow + default: // urgent + bgValueLabel.textColor = .red + } + } + + deltaLabel.text = userInfo["deltaString"] as? String ?? "-" + unitLabel.text = (userInfo["isMgDl"] as? Bool ?? true) ? Texts_Common.mgdl : Texts_Common.mmol + + // let's add the last attachment as an image to the view + // the first attachment is the thumbnail, the expanded image is the second/last one. + if let attachment = notification.request.content.attachments.last { + if attachment.url.startAccessingSecurityScopedResource() { + let data = NSData(contentsOfFile: attachment.url.path) + self.glucoseChartImage.image = UIImage(data: data! as Data) + attachment.url.stopAccessingSecurityScopedResource() + } + } + } +} diff --git a/xDrip Notification Context Extension/xDrip Notification Context Extension.entitlements b/xDrip Notification Context Extension/xDrip Notification Context Extension.entitlements new file mode 100644 index 000000000..96f2950de --- /dev/null +++ b/xDrip Notification Context Extension/xDrip Notification Context Extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup + + + diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index b3ee41348..6ab449475 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -34,6 +34,9 @@ 472596052B76301F00459D12 /* WatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472596042B76301F00459D12 /* WatchManager.swift */; }; 4733B93E2AD17C99001D609D /* FollowerBgReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4733B93D2AD17C99001D609D /* FollowerBgReading.swift */; }; 4733B9402AD17D15001D609D /* FollowerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4733B93F2AD17D15001D609D /* FollowerDelegate.swift */; }; + 4734FF552BEFE3F200C7115A /* AlertNotificationDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4734FF542BEFE3F200C7115A /* AlertNotificationDictionary.swift */; }; + 4734FF562BEFE3F200C7115A /* AlertNotificationDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4734FF542BEFE3F200C7115A /* AlertNotificationDictionary.swift */; }; + 4734FF572BF13B9C00C7115A /* TextsCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8BDD44F221CAA64006EAB84 /* TextsCommon.swift */; }; 474606642B95E48D00AC9214 /* ComplicationSharedUserDefaultsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474606632B95E48D00AC9214 /* ComplicationSharedUserDefaultsModel.swift */; }; 474606652B95E48D00AC9214 /* ComplicationSharedUserDefaultsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474606632B95E48D00AC9214 /* ComplicationSharedUserDefaultsModel.swift */; }; 474606692B9616AA00AC9214 /* AccessoryRectangularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474606682B9616AA00AC9214 /* AccessoryRectangularView.swift */; }; @@ -60,6 +63,7 @@ 4746068F2B963EA100AC9214 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746068D2B963EA100AC9214 /* View.swift */; }; 474606902B963EA100AC9214 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746068D2B963EA100AC9214 /* View.swift */; }; 474606912B963EA100AC9214 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746068D2B963EA100AC9214 /* View.swift */; }; + 474822412BFE62220052D4FB /* ConstantsCalendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E3A2AA23DA520B00E5E98A /* ConstantsCalendar.swift */; }; 4749EB9B25B36E010072DF8B /* LibreNFC.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4749EB9D25B36E010072DF8B /* LibreNFC.strings */; }; 47503382247420A200D2260B /* BluetoothPeripheralView.strings in Resources */ = {isa = PBXBuildFile; fileRef = 47503384247420A200D2260B /* BluetoothPeripheralView.strings */; }; 4752B400263570DA0081D551 /* ConstantsStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4752B3FF263570DA0081D551 /* ConstantsStatistics.swift */; }; @@ -100,6 +104,8 @@ 4793599A2B8A2A4E007D3CEE /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 479359992B8A2A4E007D3CEE /* InfoView.swift */; }; 4796C6062B9516FC00DE2210 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D602448E695001C9E5A /* Bundle.swift */; }; 4796C6072B9516FD00DE2210 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D602448E695001C9E5A /* Bundle.swift */; }; + 47976E362BF536BA005E86EC /* AlertSnoozeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47976E352BF536BA005E86EC /* AlertSnoozeStatus.swift */; }; + 47976E372BFBB870005E86EC /* TextsAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A7B1226A0878004BA588 /* TextsAlerts.swift */; }; 47A6ABE22B790CC60047A4BA /* xDripWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A6ABE12B790CC60047A4BA /* xDripWatchApp.swift */; }; 47A6ABE42B790CC60047A4BA /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A6ABE32B790CC60047A4BA /* MainView.swift */; }; 47A6ABE62B790CC70047A4BA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 47A6ABE52B790CC70047A4BA /* Assets.xcassets */; }; @@ -130,6 +136,7 @@ 47A6AC442B7D430D0047A4BA /* HomeView.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8B48A9E22B2FA7B009BCC01 /* HomeView.strings */; }; 47A6AC452B7D43100047A4BA /* Common.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8BDD444221C9D0D006EAB84 /* Common.strings */; }; 47A6AC462B7D43110047A4BA /* Common.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8BDD444221C9D0D006EAB84 /* Common.strings */; }; + 47A7DFB62BE4BE4900F1DA5F /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A7DFB52BE4BE4900F1DA5F /* UIView.swift */; }; 47AB72F327105EF4005E7CAB /* SettingsViewHelpSettingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AB72F227105EF4005E7CAB /* SettingsViewHelpSettingModel.swift */; }; 47ADD2DF27FAF8630025E2F4 /* ChartPointsScatterDownTrianglesLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47ADD2DE27FAF8630025E2F4 /* ChartPointsScatterDownTrianglesLayer.swift */; }; 47ADD2E127FB05EB0025E2F4 /* ChartPointsScatterDownTrianglesWithDropdownLineLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47ADD2E027FB05EB0025E2F4 /* ChartPointsScatterDownTrianglesWithDropdownLineLayer.swift */; }; @@ -143,6 +150,13 @@ 47CA61E72B97948000C2A597 /* FollowerDataSourceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DB06CF2A70141E00267BE3 /* FollowerDataSourceType.swift */; }; 47CA61E82B9796D200C2A597 /* ConstantsFollower.swift in Sources */ = {isa = PBXBuildFile; fileRef = 476FE8FE2B2F1D1700537E0A /* ConstantsFollower.swift */; }; 47CA61E92B97A6BD00C2A597 /* FollowerBackgroundKeepAliveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B7FC712B00CF4B004C872B /* FollowerBackgroundKeepAliveType.swift */; }; + 47CD64142BE9506C002BDA68 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47CD64132BE9506C002BDA68 /* UserNotifications.framework */; }; + 47CD64162BE9506C002BDA68 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47CD64152BE9506C002BDA68 /* UserNotificationsUI.framework */; }; + 47CD64192BE9506C002BDA68 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CD64182BE9506C002BDA68 /* NotificationViewController.swift */; }; + 47CD641C2BE9506C002BDA68 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = 47CD641B2BE9506C002BDA68 /* Base */; }; + 47CD64202BE9506C002BDA68 /* xDrip Notification Context Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 47CD64122BE9506C002BDA68 /* xDrip Notification Context Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 47CD642C2BEBEB0E002BDA68 /* AlertUrgencyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CD642B2BEBEB0E002BDA68 /* AlertUrgencyType.swift */; }; + 47CD642D2BEBEB0E002BDA68 /* AlertUrgencyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CD642B2BEBEB0E002BDA68 /* AlertUrgencyType.swift */; }; 47CF18B22B37689A00FA6160 /* TimeInRangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CF18B12B37689A00FA6160 /* TimeInRangeType.swift */; }; 47D08D5B2B5437F500B0BEA7 /* ConstantsGlucoseChartSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A50C2B416EE100419052 /* ConstantsGlucoseChartSwiftUI.swift */; }; 47D08D5E2B54390B00B0BEA7 /* LiveActivitySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D08D5D2B54390B00B0BEA7 /* LiveActivitySize.swift */; }; @@ -722,6 +736,13 @@ remoteGlobalIDString = 479359842B88B95A007D3CEE; remoteInfo = "xDrip Watch ComplicationExtension"; }; + 47CD641E2BE9506C002BDA68 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F8AC425221ADEBD60078C348 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 47CD64112BE9506C002BDA68; + remoteInfo = "xDrip Notification Context Extension"; + }; 47DE41B02B864EE50041DA19 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F8AC425221ADEBD60078C348 /* Project object */; @@ -771,6 +792,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + 47CD64202BE9506C002BDA68 /* xDrip Notification Context Extension.appex in Embed Foundation Extensions */, 4716A4FE2B406C3F00419052 /* xDrip Widget Extension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; @@ -865,6 +887,7 @@ 472596042B76301F00459D12 /* WatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchManager.swift; sourceTree = ""; }; 4733B93D2AD17C99001D609D /* FollowerBgReading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerBgReading.swift; sourceTree = ""; }; 4733B93F2AD17D15001D609D /* FollowerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerDelegate.swift; sourceTree = ""; }; + 4734FF542BEFE3F200C7115A /* AlertNotificationDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertNotificationDictionary.swift; sourceTree = ""; }; 474606632B95E48D00AC9214 /* ComplicationSharedUserDefaultsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationSharedUserDefaultsModel.swift; sourceTree = ""; }; 474606682B9616AA00AC9214 /* AccessoryRectangularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryRectangularView.swift; sourceTree = ""; }; 4746066A2B96185C00AC9214 /* AccessoryCircularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryCircularView.swift; sourceTree = ""; }; @@ -900,6 +923,7 @@ 4793598B2B88B95B007D3CEE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 4793598D2B88B95B007D3CEE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 479359992B8A2A4E007D3CEE /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = ""; }; + 47976E352BF536BA005E86EC /* AlertSnoozeStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertSnoozeStatus.swift; sourceTree = ""; }; 4798BAC727BA6AA8002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/LaunchScreen.strings; sourceTree = ""; }; 4798BAC827BA6AA8002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Main.strings; sourceTree = ""; }; 4798BACB27BA7688002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Alerts.strings; sourceTree = ""; }; @@ -928,6 +952,7 @@ 47A6ABE52B790CC70047A4BA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47A6ABE82B790CC70047A4BA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 47A6ABEE2B7949B80047A4BA /* WatchStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchStateModel.swift; sourceTree = ""; }; + 47A7DFB52BE4BE4900F1DA5F /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 47AB72F227105EF4005E7CAB /* SettingsViewHelpSettingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewHelpSettingModel.swift; sourceTree = ""; }; 47ADD2DE27FAF8630025E2F4 /* ChartPointsScatterDownTrianglesLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartPointsScatterDownTrianglesLayer.swift; sourceTree = ""; }; 47ADD2E027FB05EB0025E2F4 /* ChartPointsScatterDownTrianglesWithDropdownLineLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartPointsScatterDownTrianglesWithDropdownLineLayer.swift; sourceTree = ""; }; @@ -940,6 +965,14 @@ 47C210EF2B52A05B00005711 /* GlucoseChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseChartView.swift; sourceTree = ""; }; 47CA61E32B965E7100C2A597 /* AccessoryCircularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryCircularView.swift; sourceTree = ""; }; 47CA61E52B966A9700C2A597 /* AccessoryRectangularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryRectangularView.swift; sourceTree = ""; }; + 47CD64122BE9506C002BDA68 /* xDrip Notification Context Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "xDrip Notification Context Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 47CD64132BE9506C002BDA68 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + 47CD64152BE9506C002BDA68 /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; + 47CD64182BE9506C002BDA68 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; + 47CD641B2BE9506C002BDA68 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 47CD641D2BE9506C002BDA68 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 47CD64242BEA9E5A002BDA68 /* xDrip Notification Context Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "xDrip Notification Context Extension.entitlements"; sourceTree = ""; }; + 47CD642B2BEBEB0E002BDA68 /* AlertUrgencyType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertUrgencyType.swift; sourceTree = ""; }; 47CF18B12B37689A00FA6160 /* TimeInRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInRangeType.swift; sourceTree = ""; }; 47D08D5D2B54390B00B0BEA7 /* LiveActivitySize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySize.swift; sourceTree = ""; }; 47D2DB3A2B14F6D000C8EE6B /* ScreenLockDimmingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockDimmingType.swift; sourceTree = ""; }; @@ -1833,6 +1866,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 47CD640F2BE9506C002BDA68 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 47CD64162BE9506C002BDA68 /* UserNotificationsUI.framework in Frameworks */, + 47CD64142BE9506C002BDA68 /* UserNotifications.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F8AC425721ADEBD60078C348 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2002,6 +2044,25 @@ path = "Preview Content"; sourceTree = ""; }; + 47CD64172BE9506C002BDA68 /* xDrip Notification Context Extension */ = { + isa = PBXGroup; + children = ( + 47CD64242BEA9E5A002BDA68 /* xDrip Notification Context Extension.entitlements */, + 47CD641D2BE9506C002BDA68 /* Info.plist */, + 47CD64182BE9506C002BDA68 /* NotificationViewController.swift */, + 47CD64252BEAA538002BDA68 /* Extensions */, + 47CD641A2BE9506C002BDA68 /* MainInterface.storyboard */, + ); + path = "xDrip Notification Context Extension"; + sourceTree = ""; + }; + 47CD64252BEAA538002BDA68 /* Extensions */ = { + isa = PBXGroup; + children = ( + ); + path = Extensions; + sourceTree = ""; + }; 47DB06CC2A7013EF00267BE3 /* Followers */ = { isa = PBXGroup; children = ( @@ -2082,6 +2143,8 @@ F870D3D225126A49008967B0 /* NotificationCenter.framework */, 4716A4EE2B406C3D00419052 /* WidgetKit.framework */, 4716A4F02B406C3D00419052 /* SwiftUI.framework */, + 47CD64132BE9506C002BDA68 /* UserNotifications.framework */, + 47CD64152BE9506C002BDA68 /* UserNotificationsUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -2164,6 +2227,7 @@ F8BDD4232218790E006EAB84 /* UserDefaults.swift */, F80859282364D61B00F3829D /* UserDefaults+charts.swift */, 4746068D2B963EA100AC9214 /* View.swift */, + 47A7DFB52BE4BE4900F1DA5F /* UIView.swift */, ); path = Extensions; sourceTree = ""; @@ -2370,6 +2434,9 @@ children = ( F821CF49229BF43A005C1E43 /* AlertKind.swift */, F821CF4B229BF43A005C1E43 /* AlertManager.swift */, + 47976E352BF536BA005E86EC /* AlertSnoozeStatus.swift */, + 47CD642B2BEBEB0E002BDA68 /* AlertUrgencyType.swift */, + 4734FF542BEFE3F200C7115A /* AlertNotificationDictionary.swift */, F821CF4A229BF43A005C1E43 /* SnoozeParameters.swift */, ); path = Alerts; @@ -2683,6 +2750,7 @@ 4716A4F22B406C3D00419052 /* xDrip Widget */, 47A6ABE02B790CC60047A4BA /* xDrip Watch App */, 479359882B88B95A007D3CEE /* xDrip Watch Complication */, + 47CD64172BE9506C002BDA68 /* xDrip Notification Context Extension */, 48C0E851274A3BB6D42C6F20 /* Frameworks */, F8AC425B21ADEBD60078C348 /* Products */, F85DC29B21CFCEB800B9F74A /* Recovered References */, @@ -2698,6 +2766,7 @@ 4716A4ED2B406C3D00419052 /* xDrip Widget Extension.appex */, 47A6ABDF2B790CC60047A4BA /* xDrip Watch App.app */, 479359852B88B95A007D3CEE /* xDrip Watch Complication Extension.appex */, + 47CD64122BE9506C002BDA68 /* xDrip Notification Context Extension.appex */, ); name = Products; sourceTree = ""; @@ -2705,10 +2774,10 @@ F8AC425C21ADEBD60078C348 /* xdrip */ = { isa = PBXGroup; children = ( - E4C0061F2B3DE8C100D59303 /* AppIntents */, F821CF9822AE589E005C1E43 /* xdrip.entitlements */, F8A5EEC5257EDC910085E660 /* xdripDebug.entitlements */, F8A54B0A22D9215500934E7A /* xdrip-Bridging-Header.h */, + E4C0061F2B3DE8C100D59303 /* AppIntents */, F85DC2FB21D2CD7000B9F74A /* Application Delegate */, F8F971AD23A5914C00C3F17D /* BluetoothPeripheral */, F8F971B923A5915900C3F17D /* BluetoothTransmitter */, @@ -3739,6 +3808,23 @@ productReference = 47A6ABDF2B790CC60047A4BA /* xDrip Watch App.app */; productType = "com.apple.product-type.application"; }; + 47CD64112BE9506C002BDA68 /* xDrip Notification Context Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 47CD64232BE9506C002BDA68 /* Build configuration list for PBXNativeTarget "xDrip Notification Context Extension" */; + buildPhases = ( + 47CD640E2BE9506C002BDA68 /* Sources */, + 47CD640F2BE9506C002BDA68 /* Frameworks */, + 47CD64102BE9506C002BDA68 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "xDrip Notification Context Extension"; + productName = "xDrip Notification Context Extension"; + productReference = 47CD64122BE9506C002BDA68 /* xDrip Notification Context Extension.appex */; + productType = "com.apple.product-type.app-extension"; + }; F8AC425921ADEBD60078C348 /* xdrip */ = { isa = PBXNativeTarget; buildConfigurationList = F8AC426E21ADEBD70078C348 /* Build configuration list for PBXNativeTarget "xdrip" */; @@ -3755,6 +3841,7 @@ dependencies = ( 4716A4FD2B406C3F00419052 /* PBXTargetDependency */, 47DE41B12B864EE50041DA19 /* PBXTargetDependency */, + 47CD641F2BE9506C002BDA68 /* PBXTargetDependency */, ); name = xdrip; packageProductDependencies = ( @@ -3775,7 +3862,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; DefaultBuildSystemTypeForWorkspace = Latest; - LastSwiftUpdateCheck = 1520; + LastSwiftUpdateCheck = 1530; LastUpgradeCheck = 1500; ORGANIZATIONNAME = "Johan Degraeve"; TargetAttributes = { @@ -3788,6 +3875,9 @@ 47A6ABDE2B790CC60047A4BA = { CreatedOnToolsVersion = 15.2; }; + 47CD64112BE9506C002BDA68 = { + CreatedOnToolsVersion = 15.3; + }; F8AC425921ADEBD60078C348 = { CreatedOnToolsVersion = 10.1; LastSwiftMigration = 1030; @@ -3842,6 +3932,7 @@ 4716A4EC2B406C3D00419052 /* xDrip Widget Extension */, 47A6ABDE2B790CC60047A4BA /* xDrip Watch App */, 479359842B88B95A007D3CEE /* xDrip Watch Complication Extension */, + 47CD64112BE9506C002BDA68 /* xDrip Notification Context Extension */, ); }; /* End PBXProject section */ @@ -3881,6 +3972,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 47CD64102BE9506C002BDA68 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 47CD641C2BE9506C002BDA68 /* Base in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F8AC425821ADEBD60078C348 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4154,6 +4253,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 47CD640E2BE9506C002BDA68 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 47976E372BFBB870005E86EC /* TextsAlerts.swift in Sources */, + 47CD642D2BEBEB0E002BDA68 /* AlertUrgencyType.swift in Sources */, + 4734FF572BF13B9C00C7115A /* TextsCommon.swift in Sources */, + 4734FF562BEFE3F200C7115A /* AlertNotificationDictionary.swift in Sources */, + 47CD64192BE9506C002BDA68 /* NotificationViewController.swift in Sources */, + 474822412BFE62220052D4FB /* ConstantsCalendar.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F8AC425621ADEBD60078C348 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -4171,6 +4283,7 @@ 47ADD2E127FB05EB0025E2F4 /* ChartPointsScatterDownTrianglesWithDropdownLineLayer.swift in Sources */, 47228B152996BDD2008725DB /* BgReadingsView.swift in Sources */, F8F71D802B7EAA2E005076E8 /* DexcomG7+CoreDataClass.swift in Sources */, + 47A7DFB62BE4BE4900F1DA5F /* UIView.swift in Sources */, F81FA006228E09D40028C70F /* TextsCalibration.swift in Sources */, F816E0F724367137009EE65B /* GNSEntry+CoreDataClass.swift in Sources */, F8F9721923A5915900C3F17D /* CGMGNSEntryTransmitter.swift in Sources */, @@ -4269,6 +4382,7 @@ F8F9722B23A5915900C3F17D /* CGMTransmitterDelegate.swift in Sources */, F80859292364D61B00F3829D /* UserDefaults+charts.swift in Sources */, F8B955B7258D5E2000C06016 /* ConstantsHealthKit.swift in Sources */, + 47CD642C2BEBEB0E002BDA68 /* AlertUrgencyType.swift in Sources */, F8B3A7B2226A0878004BA588 /* TextsAlerts.swift in Sources */, D4BAF37627769B38009D3465 /* TreatmentTableViewCell.swift in Sources */, F8F71D782B7D2754005076E8 /* CGMG7TransmitterDelegate.swift in Sources */, @@ -4305,6 +4419,7 @@ F830992023C291E2005741DF /* WatlaaBluetoothTransmitter.swift in Sources */, F8AF36152455C6F700B5977B /* ConstantsTrace.swift in Sources */, F8F971B723A5914D00C3F17D /* BluetoothPeripheralType.swift in Sources */, + 4734FF552BEFE3F200C7115A /* AlertNotificationDictionary.swift in Sources */, F80610C4222D4E4D00D8F236 /* ActionClosureable-extension.swift in Sources */, F8F1670E27273EA7001AA3D8 /* GlucoseDataRxMessage.swift in Sources */, F8B3A835227F08AC004BA588 /* PickerViewController.swift in Sources */, @@ -4499,6 +4614,7 @@ F8F9723023A5915900C3F17D /* M5StackUtilities.swift in Sources */, F8297F52238ECA3200D74D66 /* BluetoothPeripheralViewController.swift in Sources */, F816E1312439E2DD009EE65B /* DexcomG4BluetoothPeripheralViewModel.swift in Sources */, + 47976E362BF536BA005E86EC /* AlertSnoozeStatus.swift in Sources */, F821CF66229EE68B005C1E43 /* NightScoutFollowManager.swift in Sources */, F8F9722423A5915900C3F17D /* LibreMeasurement.swift in Sources */, F869188C23A044340065B607 /* TextsM5StackView.swift in Sources */, @@ -4602,6 +4718,11 @@ target = 479359842B88B95A007D3CEE /* xDrip Watch Complication Extension */; targetProxy = 4793598E2B88B95B007D3CEE /* PBXContainerItemProxy */; }; + 47CD641F2BE9506C002BDA68 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 47CD64112BE9506C002BDA68 /* xDrip Notification Context Extension */; + targetProxy = 47CD641E2BE9506C002BDA68 /* PBXContainerItemProxy */; + }; 47DE41B12B864EE50041DA19 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 47A6ABDE2B790CC60047A4BA /* xDrip Watch App */; @@ -4707,6 +4828,14 @@ name = WatchComplication.strings; sourceTree = ""; }; + 47CD641A2BE9506C002BDA68 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 47CD641B2BE9506C002BDA68 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; 47EDD1472BDD56C800C5A286 /* WatchApp.strings */ = { isa = PBXVariantGroup; children = ( @@ -5436,6 +5565,75 @@ }; name = Release; }; + 47CD64212BE9506C002BDA68 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "xDrip Notification Context Extension/xDrip Notification Context Extension.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "xDrip Notification Context Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Johan Degraeve. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = "$(XDRIP_MARKETING_VERSION)"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).xDrip-Notification-Context-Extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 47CD64222BE9506C002BDA68 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "xDrip Notification Context Extension/xDrip Notification Context Extension.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "xDrip Notification Context Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Johan Degraeve. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = "$(XDRIP_MARKETING_VERSION)"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).xDrip-Notification-Context-Extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; F8AC426C21ADEBD70078C348 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 666E283826F7E54C00ACE4DF /* xDrip.xcconfig */; @@ -5652,6 +5850,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 47CD64232BE9506C002BDA68 /* Build configuration list for PBXNativeTarget "xDrip Notification Context Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 47CD64212BE9506C002BDA68 /* Debug */, + 47CD64222BE9506C002BDA68 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; F8AC425521ADEBD60078C348 /* Build configuration list for PBXProject "xdrip" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/xdrip/Constants/ConstantsAlerts.swift b/xdrip/Constants/ConstantsAlerts.swift index 4b37141f2..9ccea2785 100644 --- a/xdrip/Constants/ConstantsAlerts.swift +++ b/xdrip/Constants/ConstantsAlerts.swift @@ -6,4 +6,28 @@ enum ConstantsAlerts { /// - the actual delay used is first read from UserDefaults (settings), if not present then this value here is used static let defaultDelayBetweenAlertsOfSameKindInMinutes = 5 + /// when the snooze all picker is brought up, this will be the default selected mute time + /// unlike the specific alarms, we'll set this to a longer period such as 6 hours + static let defaultSnoozeAllPeriodInMinutes = 6 * 60 + + /// the snooze all banner background color when not activated + static let bannerBackgroundColorWhenNotAllSnoozed = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1) + + /// the snooze all banner text color when not activated + static let bannerTextColorWhenNotAllSnoozed = UIColor.gray + + /// snooze times in minutes + static let snoozeValueMinutes = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 75, 90, 120, 150, 180, 240, 300, 360, 420, 480, 540, 600, 720, 1440, 10080] + + /// snooze times as shown to the user, actual strings will be replaced during init + static var snoozeValueStrings = ["5 minutes", "10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes", + "40 minutes", "45 minutes", "50 minutes", "55 minutes", "1 hour", "1 hour 15 minutes", "1,5 hours", "2 hours", "2,5 hours", "3 hours", "4 hours", + "5 hours", "6 hours", "7 hours", "8 hours", "9 hours", "10 hours", "12 hours", "1 day", "1 week"] + + /// snooze all times in minutes - this can be much simpler than the individual alert snooze times... + static let snoozeAllValueMinutes = [15, 30, 60, 120, 240, 360, 480, 720, 1440, 2880, 10080] + + /// snooze all times as shown to the user + static var snoozeAllValueStrings = ["15 " + Texts_Common.minutes, "30 " + Texts_Common.minutes, "1 " + Texts_Common.hour, "2 " + Texts_Common.hours, "4 " + Texts_Common.hours, + "6 " + Texts_Common.hours, "12 " + Texts_Common.hours, "1 " + Texts_Common.day, "2 " + Texts_Common.day, "1 " + Texts_Common.week] } diff --git a/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift b/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift index 03c57f353..94111576d 100644 --- a/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift +++ b/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift @@ -114,7 +114,6 @@ enum ConstantsGlucoseChartSwiftUI { // ------------------------------------------ // ----- Siri Intent Chart ------------------ // ------------------------------------------ - // siri glucose intent response chart static let viewWidthWidgetSiriGlucoseIntent: CGFloat = 320 static let viewHeightWidgetSiriGlucoseIntent: CGFloat = 150 static let hoursToShowWidgetSiriGlucoseIntent: Double = 4 @@ -122,4 +121,22 @@ enum ConstantsGlucoseChartSwiftUI { static let cornerRadiusSiriGlucoseIntent: Double = 0 static let paddingSiriGlucoseIntent: Double = 10 static let backgroundColorSiriGlucoseIntent: Color = .black // Color(red: 0.18, green: 0.18, blue: 0.18) originally from gshaviv + + + // ------------------------------------------ + // ----- Notification Image Chart ----------- + // ------------------------------------------ + // thumbnail image + static let viewWidthNotificationThumbnailImage: CGFloat = 80 + static let viewHeightNotificationThumbnailImage: CGFloat = 80 + static let hoursToShowNotificationThumbnailImage: Double = 1 + static let glucoseCircleDiameterNotificationThumbnailImage: Double = 70 + static let filenameNotificationThumbnailImage: String = "notificationThumbnailImage" + + // expanded image + static let viewWidthNotificationExpandedImage: CGFloat = 373 //320 + static let viewHeightNotificationExpandedImage: CGFloat = 170 //150 + static let hoursToShowNotificationExpandedImage: Double = 3 + static let glucoseCircleDiameterNotificationExpandedImage: Double = 30 + static let filenameNotificationExpandedImage: String = "notificationExpandedImage" } diff --git a/xdrip/Extensions/UIView.swift b/xdrip/Extensions/UIView.swift new file mode 100644 index 000000000..256bd570f --- /dev/null +++ b/xdrip/Extensions/UIView.swift @@ -0,0 +1,18 @@ +// +// UIView.swift +// xdrip +// +// Created by Paul Plant on 3/5/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation + +//extension UIView { +// func asImage() -> UIImage { +// let renderer = UIGraphicsImageRenderer(bounds: bounds) +// return renderer.image { rendererContext in +// layer.render(in: rendererContext.cgContext) +// } +// } +//} diff --git a/xdrip/Extensions/UserDefaults.swift b/xdrip/Extensions/UserDefaults.swift index 6899aa6a0..3cab48ecc 100644 --- a/xdrip/Extensions/UserDefaults.swift +++ b/xdrip/Extensions/UserDefaults.swift @@ -101,6 +101,8 @@ extension UserDefaults { /// target value case targetMarkValue = "targetMarkValue" + + // Treatment settings /// should the treatments be shown on the main chart? @@ -135,6 +137,13 @@ extension UserDefaults { /// use the newer TITR of 70-140mg/dL to calculate the statistics? If false, we will use the conventional TIR of 70-180mg/dL case useTITRStatisticsRange = "useTITRStatisticsRange" + // Alert settings + + /// when did the user snooze all alarms + case snoozeAllAlertsFromDate = "snoozeAllAlertsFromDate" + /// for how long did the user snooze all alarms + case snoozeAllAlertsUntilDate = "snoozeAllAlertsUntilDate" + // Housekeeper settings /// For how many days should we keep Readings, Treatments and Calibrations? @@ -419,6 +428,10 @@ extension UserDefaults { /// how many seconds since the last heartbeat before we raise a disconnection warning case secondsUntilHeartBeatDisconnectWarning = "secondsUntilHeartBeatDisconnectWarning" + // snooze + /// used by the observer in RVC to update the UI for the snooze status + case updateSnoozeStatus = "updateSnoozeStatus" + } @@ -1183,6 +1196,29 @@ extension UserDefaults { } + // MARK: Alert Settings + + /// when did the user snooze all alerts. If this is nil, then the snooze all isn't activated + @objc dynamic var snoozeAllAlertsFromDate: Date? { + get { + return object(forKey: Key.snoozeAllAlertsFromDate.rawValue) as? Date + } + set { + set(newValue, forKey: Key.snoozeAllAlertsFromDate.rawValue) + } + } + + /// until when did the user snooze all alerts, can be nil until it's first set but unless snoozeAllAlertsDate != nil we'll ignore this value anyway + @objc dynamic var snoozeAllAlertsUntilDate: Date? { + get { + return object(forKey: Key.snoozeAllAlertsUntilDate.rawValue) as? Date + } + set { + set(newValue, forKey: Key.snoozeAllAlertsUntilDate.rawValue) + } + } + + // MARK: Sensor Info Settings /// active sensor serial number. Optional as should be set to nil if no successful login has happened and/or if no active sensor is returned @@ -2315,6 +2351,17 @@ extension UserDefaults { } } + // MARK: - Snooze + + /// used by the observer in RVC to update the UI for the snooze status + @objc dynamic var updateSnoozeStatus: Bool { + get { + return bool(forKey: Key.updateSnoozeStatus.rawValue) + } + set { + set(newValue, forKey: Key.updateSnoozeStatus.rawValue) + } + } } diff --git a/xdrip/Extensions/View.swift b/xdrip/Extensions/View.swift index 0163df692..a42a2a557 100644 --- a/xdrip/Extensions/View.swift +++ b/xdrip/Extensions/View.swift @@ -35,3 +35,4 @@ extension View { } } } + diff --git a/xdrip/Managers/Alerts/AlertKind.swift b/xdrip/Managers/Alerts/AlertKind.swift index eb2d026f6..02315b199 100644 --- a/xdrip/Managers/Alerts/AlertKind.swift +++ b/xdrip/Managers/Alerts/AlertKind.swift @@ -76,18 +76,12 @@ public enum AlertKind:Int, CaseIterable { /// if true, then this type of alert will (if raised) create an immediate notification which will have the current reading as text - simply means there's no need to create an additional notification with the current reading func createsImmediateNotificationWithBGReading() -> Bool { - switch self { - case .low, .high, .verylow, .veryhigh, .fastdrop, .fastrise: return true - case .missedreading, .batterylow, .calibration: return false - } - - } /// example, low alert needs a value = value below which alert needs to fire - there's actually no alert right now that doesn't need a value, in iosxdrip there was the iphonemuted alert, but I removed this here. Function remains, never now it might come back @@ -95,7 +89,7 @@ public enum AlertKind:Int, CaseIterable { /// probably only useful in UI - named AlertKind and not AlertType because there's already an AlertType which has a different goal func needsAlertValue() -> Bool { switch self { - case .low, .high, .verylow,.veryhigh,.missedreading,.calibration,.batterylow,.fastdrop,.fastrise: + case .low, .high, .verylow, .veryhigh, .missedreading, .calibration, .batterylow, .fastdrop, .fastrise: return true } } @@ -105,7 +99,6 @@ public enum AlertKind:Int, CaseIterable { /// will only be useful in UI func valueNeedsConversionToMmol() -> Bool { switch self { - case .low, .high, .verylow, .veryhigh, .fastdrop, .fastrise: return true case .missedreading, .calibration, .batterylow: @@ -116,7 +109,6 @@ public enum AlertKind:Int, CaseIterable { /// at initial startup, a default alertentry will be created for every kind of alert. This function defines the default value to be used func defaultAlertValue() -> Int { switch self { - case .low: return ConstantsDefaultAlertLevels.low case .high: @@ -145,7 +137,6 @@ public enum AlertKind:Int, CaseIterable { /// description of the alert to be used for logging func descriptionForLogging() -> String { switch self { - case .low: return "low" case .high: @@ -198,7 +189,8 @@ public enum AlertKind:Int, CaseIterable { if lastBgReading.calculatedValue == 0.0 {return (false, nil, nil, nil)} // now do the actual check if alert is applicable or not if lastBgReading.calculatedValue.bgValueRounded(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) < Double(currentAlertEntry.value).bgValueRounded(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) { - return (true, lastBgReading.unitizedDeltaString(previousBgReading: lastButOneBgReading, showUnit: true, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl), createAlertTitleForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), nil) +// return (true, lastBgReading.unitizedDeltaString(previousBgReading: lastButOneBgReading, showUnit: true, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl), createAlertTitleForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), nil) + return (true, createAlertBodyForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), createAlertTitleForBgReadingAlerts(alertKind: self), nil) } else {return (false, nil, nil, nil)} } else {return (false, nil, nil, nil)} @@ -211,7 +203,7 @@ public enum AlertKind:Int, CaseIterable { if lastBgReading.calculatedValue == 0.0 {return (false, nil, nil, nil)} // now do the actual check if alert is applicable or not if lastBgReading.calculatedValue.bgValueRounded(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) > Double(currentAlertEntry.value).bgValueRounded(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl){ - return (true, lastBgReading.unitizedDeltaString(previousBgReading: lastButOneBgReading, showUnit: true, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl), createAlertTitleForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), nil) + return (true, createAlertBodyForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), createAlertTitleForBgReadingAlerts(alertKind: self), nil) } else {return (false, nil, nil, nil)} } else {return (false, nil, nil, nil)} @@ -228,7 +220,7 @@ public enum AlertKind:Int, CaseIterable { if lastBgReading.calculatedValue == 0.0 || lastButOneBgReading.calculatedValue == 0.0 {return (false, nil, nil, nil)} // now do the actual check if alert is applicable or not if lastButOneBgReading.calculatedValue.bgValueRounded(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) - lastBgReading.calculatedValue.bgValueRounded(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) > Double(currentAlertEntry.value).bgValueRounded(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) { - return (true, lastBgReading.unitizedDeltaString(previousBgReading: lastButOneBgReading, showUnit: true, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl), createAlertTitleForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), nil) + return (true, createAlertBodyForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), createAlertTitleForBgReadingAlerts(alertKind: self), nil) } else {return (false, nil, nil, nil)} } else {return (false, nil, nil, nil)} @@ -248,7 +240,7 @@ public enum AlertKind:Int, CaseIterable { if lastBgReading.calculatedValue == 0.0 || lastButOneBgReading.calculatedValue == 0.0 {return (false, nil, nil, nil)} // now do the actual check if alert is applicable or not if lastBgReading.calculatedValue.bgValueRounded(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) - lastButOneBgReading.calculatedValue.bgValueRounded(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) > Double(currentAlertEntry.value).bgValueRounded(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) { - return (true, lastBgReading.unitizedDeltaString(previousBgReading: lastButOneBgReading, showUnit: true, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl), createAlertTitleForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), nil) + return (true, createAlertBodyForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), createAlertTitleForBgReadingAlerts(alertKind: self), nil) } else {return (false, nil, nil, nil)} } else {return (false, nil, nil, nil)} @@ -442,31 +434,46 @@ public enum AlertKind:Int, CaseIterable { } } } + + + /// this categorizes the different alert types into an AlertUrgencyType. Used for deciding how to display the UI and notification content + /// - Returns: the type of alert (i.e. if urgent, notUrgent etc) + func alertUrgencyType() -> AlertUrgencyType { + switch self { + case .verylow, .veryhigh, .fastdrop: + return .urgent + case .low, .high, .fastrise: + return .warning + default: + return .normal + } + } } // specifically for high, low, very high, very low because these need the same kind of alertTitle -fileprivate func createAlertTitleForBgReadingAlerts(bgReading:BgReading, alertKind:AlertKind) -> String { - var returnValue:String = "" - +fileprivate func createAlertTitleForBgReadingAlerts(alertKind: AlertKind) -> String { // the start of the body, which says like "High Alert" switch alertKind { - case .low: - returnValue = returnValue + Texts_Alerts.lowAlertTitle + return Texts_Alerts.lowAlertTitle case .high: - returnValue = returnValue + Texts_Alerts.highAlertTitle + return Texts_Alerts.highAlertTitle case .verylow: - returnValue = returnValue + Texts_Alerts.veryLowAlertTitle + return Texts_Alerts.veryLowAlertTitle case .veryhigh: - returnValue = returnValue + Texts_Alerts.veryHighAlertTitle + return Texts_Alerts.veryHighAlertTitle case .fastdrop: - returnValue = returnValue + Texts_Alerts.fastDropTitle + return Texts_Alerts.fastDropTitle case .fastrise: - returnValue = returnValue + Texts_Alerts.fastRiseTitle - + return Texts_Alerts.fastRiseTitle case .missedreading, .calibration, .batterylow: - return returnValue + return "" } +} + +// specifically for high, low, very high, very low because these need to show an alert body with the BG value etc +fileprivate func createAlertBodyForBgReadingAlerts(bgReading:BgReading, alertKind:AlertKind) -> String { + var returnValue:String = "" // add unit returnValue = returnValue + " " + bgReading.calculatedValue.mgdlToMmolAndToString(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) diff --git a/xdrip/Managers/Alerts/AlertManager.swift b/xdrip/Managers/Alerts/AlertManager.swift index 76e166431..aec0ced48 100644 --- a/xdrip/Managers/Alerts/AlertManager.swift +++ b/xdrip/Managers/Alerts/AlertManager.swift @@ -81,7 +81,7 @@ public class AlertManager:NSObject { snoozeParameters = SnoozeParametersAccessor(coreDataManager: coreDataManager).getSnoozeParameters() // in snoozeValueStrings, replace all occurrences of minutes, minute, etc... by language dependent value - for (index, _) in snoozeValueStrings.enumerated() { + for (index, _) in ConstantsAlerts.snoozeValueStrings.enumerated() { snoozeValueStrings[index] = snoozeValueStrings[index].replacingOccurrences(of: "minutes", with: Texts_Common.minutes).replacingOccurrences(of: "hours", with: Texts_Common.hours).replacingOccurrences(of: "hour", with: Texts_Common.hour).replacingOccurrences(of: "day", with: Texts_Common.day).replacingOccurrences(of: "week", with: Texts_Common.week) } @@ -117,6 +117,12 @@ public class AlertManager:NSObject { uNUserNotificationCenter.removeDeliveredNotifications(withIdentifiers: alertNotificationIdentifers) uNUserNotificationCenter.removeAllPendingNotificationRequests() + // check if "Snooze All" is activated. If so, then just return with nothing. + if let snoozeAllAlertsUntilDate = UserDefaults.standard.snoozeAllAlertsUntilDate, snoozeAllAlertsUntilDate > Date() { + trace("in alertNcheckAlertseeded, skipping all alarms as Snooze All is enabled until %{public}@.", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, snoozeAllAlertsUntilDate.formatted(date: .abbreviated, time: .standard)) + return false + } + /// this is the return value var immediateNotificationCreated = false @@ -141,16 +147,16 @@ public class AlertManager:NSObject { // reading is maxAgeOfLastBgReadingInSeconds seconds old, let's check the alerts // need to call checkAlert - // if latestBgReadings[1] exists then assign it to lastButOneBgREading - var lastButOneBgREading:BgReading? + // if latestBgReadings[1] exists then assign it to lastButOneBgReading + var lastButOneBgReading:BgReading? if latestBgReadings.count > 1 { - lastButOneBgREading = latestBgReadings[1] + lastButOneBgReading = latestBgReadings[1] } // alerts are checked in order of importance - there should be only one alert raised, except missed reading alert which will always be checked. // create helper to check and fire alerts - let checkAlertAndFireHelper = { (_ alertKind : AlertKind) -> Bool in self.checkAlertAndFire(alertKind: alertKind, lastBgReading: lastBgReading, lastButOneBgREading: lastButOneBgREading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo) } + let checkAlertAndFireHelper = { (_ alertKind : AlertKind) -> Bool in self.checkAlertAndFire(alertKind: alertKind, lastBgReading: lastBgReading, lastButOneBgReading: lastButOneBgReading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo) } // specify the order in which alerts should be checked and group those with related snoozes let alertGroupsByPreference: [[AlertKind]] = [[.fastdrop], [.verylow, .low], [.fastrise], [.veryhigh, .high], [.calibration], [.batterylow]] @@ -256,6 +262,34 @@ public class AlertManager:NSObject { public func getSnoozeParameters(alertKind: AlertKind) -> SnoozeParameters { return snoozeParameters[alertKind.rawValue] } + + /// check if any alerts are currently snoozed and return the correct status + public func snoozeStatus() -> AlertSnoozeStatus { + // set the default value to inactive. We'll then only override it as necessary + var snoozeStatus: AlertSnoozeStatus = .inactive + + if let snoozeAllAlertsUntilDate = UserDefaults.standard.snoozeAllAlertsUntilDate, snoozeAllAlertsUntilDate > Date() { + return .allSnoozed + } + + // loop through the alertKinds so that we can define if an urgent alert is snoozed, if just a non-urgent one, or none at all. + // this is used to update the root view controller snooze icon + for alertKind in AlertKind.allCases { + switch alertKind.alertUrgencyType() { + case .urgent: + if snoozeParameters[alertKind.rawValue].getSnoozeValue().isSnoozed { + snoozeStatus = .urgent + } + default: + // only overwrite with non-urgent if urgent hasn't already been assigned + if snoozeStatus != .urgent && snoozeParameters[alertKind.rawValue].getSnoozeValue().isSnoozed { + snoozeStatus = .notUrgent + } + } + } + + return snoozeStatus + } /// Function to be called that receives the notification actions. Will handle the response. completionHandler will not necessarily be called. Only if the identifier (response.notification.request.identifier) is one of the alert notification identifers, then it will handle the response and also call completionhandler. /// called when notification created while app is in foreground @@ -305,6 +339,7 @@ public class AlertManager:NSObject { // find the default snooze period, so we can set selectedRow in the pickerviewdata let defaultSnoozePeriodInMinutes = Int(alertEntriesAccessor.getCurrentAndNextAlertEntry(forAlertKind: alertKind, forWhen: Date(), alertTypesAccessor: alertTypesAccessor).currentAlertEntry.alertType.snoozeperiod) + var defaultRow = 0 for (index, _) in snoozeValueMinutes.enumerated() { if snoozeValueMinutes[index] > defaultSnoozePeriodInMinutes { @@ -316,58 +351,58 @@ public class AlertManager:NSObject { return PickerViewData(withMainTitle: alertKind.alertTitle(), withSubTitle: Texts_Alerts.selectSnoozeTime, withData: snoozeValueStrings, selectedRow: defaultRow, withPriority: .high, actionButtonText: Texts_Common.Ok, cancelButtonText: Texts_Common.Cancel, onActionClick: { - - (snoozeIndex:Int) -> Void in - - // if sound is currently playing then stop it - if let soundPlayer = self.soundPlayer { - soundPlayer.stopPlaying() - } - - // get snooze period - let snoozePeriod = self.snoozeValueMinutes[snoozeIndex] - - // snooze - trace(" snoozing alert %{public}@ for %{public}@ minutes (1)", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging(), snoozePeriod.description) - self.getSnoozeParameters(alertKind: alertKind).snooze(snoozePeriodInMinutes: snoozePeriod) - - // save changes in coredata - self.coreDataManager.saveChanges() - - // if it's a missed reading alert, then cancel any planned missed reading alerts and reschedule - // if content is not nil, then it means a missed reading alert went off, the user clicked it, app opens, user clicks snooze, snoozing must be set - // if content is nil, then this is an alert snoozed via presnooze button, missed reading alert needs to recalculated. - if alertKind == .missedreading { - - if let content = content { - - // schedule missed reading alert with same content - self.scheduleMissedReadingAlert(snoozePeriodInMinutes: snoozePeriod, content: content) - - } else if UserDefaults.standard.isMaster || (!UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType != .disabled && UserDefaults.standard.activeSensorStartDate != nil) { - - _ = self.checkAlertAndFire(alertKind: .missedreading, lastBgReading: nil, lastButOneBgREading: nil, lastCalibration: nil, transmitterBatteryInfo: nil) - - } - - } - - // if actionHandler supplied by caller not nil, then execute it - actionHandler?() - + + (snoozeIndex:Int) -> Void in + + // if sound is currently playing then stop it + if let soundPlayer = self.soundPlayer { + soundPlayer.stopPlaying() + } + + // get snooze period + let snoozePeriod = self.snoozeValueMinutes[snoozeIndex] + + // snooze + trace(" snoozing alert %{public}@ for %{public}@ minutes (1)", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging(), snoozePeriod.description) + self.getSnoozeParameters(alertKind: alertKind).snooze(snoozePeriodInMinutes: snoozePeriod) + + // save changes in coredata + self.coreDataManager.saveChanges() + + // if it's a missed reading alert, then cancel any planned missed reading alerts and reschedule + // if content is not nil, then it means a missed reading alert went off, the user clicked it, app opens, user clicks snooze, snoozing must be set + // if content is nil, then this is an alert snoozed via presnooze button, missed reading alert needs to recalculated. + if alertKind == .missedreading { + + if let content = content { + + // schedule missed reading alert with same content + self.scheduleMissedReadingAlert(snoozePeriodInMinutes: snoozePeriod, content: content) + + } else if UserDefaults.standard.isMaster || (!UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType != .disabled && UserDefaults.standard.activeSensorStartDate != nil) { + + _ = self.checkAlertAndFire(alertKind: .missedreading, lastBgReading: nil, lastButOneBgReading: nil, lastCalibration: nil, transmitterBatteryInfo: nil) + + } + + } + + // if actionHandler supplied by caller not nil, then execute it + actionHandler?() + }, onCancelClick: { - - () -> Void in - - // if sound is currently playing then stop it - if let soundPlayer = self.soundPlayer { - soundPlayer.stopPlaying() - } - - // if cancelHandler supplied by caller not nil, then execute it - cancelHandler?() - + + () -> Void in + + // if sound is currently playing then stop it + if let soundPlayer = self.soundPlayer { + soundPlayer.stopPlaying() + } + + // if cancelHandler supplied by caller not nil, then execute it + cancelHandler?() + }, didSelectRowHandler: nil ) @@ -430,7 +465,7 @@ public class AlertManager:NSObject { uNUserNotificationCenter.removeDeliveredNotifications(withIdentifiers: [AlertKind.missedreading.notificationIdentifier()]) uNUserNotificationCenter.removePendingNotificationRequests(withIdentifiers: [AlertKind.missedreading.notificationIdentifier()]) - _ = checkAlertAndFire(alertKind: .missedreading, lastBgReading: latestBgReadings[0], lastButOneBgREading: nil, lastCalibration: nil, transmitterBatteryInfo: nil) + _ = checkAlertAndFire(alertKind: .missedreading, lastBgReading: latestBgReadings[0], lastButOneBgReading: nil, lastCalibration: nil, transmitterBatteryInfo: nil) } @@ -500,7 +535,7 @@ public class AlertManager:NSObject { } /// will check if the alert of type alertKind needs to be fired and also fires it, plays the sound, and if yes returns true, otherwise false - private func checkAlertAndFire(alertKind:AlertKind, lastBgReading:BgReading?, lastButOneBgREading:BgReading?, lastCalibration:Calibration?, transmitterBatteryInfo:TransmitterBatteryInfo?) -> Bool { + private func checkAlertAndFire(alertKind:AlertKind, lastBgReading:BgReading?, lastButOneBgReading:BgReading?, lastCalibration:Calibration?, transmitterBatteryInfo:TransmitterBatteryInfo?) -> Bool { trace("in checkAlertAndFire for alert = %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging()) @@ -540,7 +575,7 @@ public class AlertManager:NSObject { let (currentAlertEntry, nextAlertEntry) = alertEntriesAccessor.getCurrentAndNextAlertEntry(forAlertKind: alertKind, forWhen: Date(), alertTypesAccessor: alertTypesAccessor) // check if alert is required - let (alertNeeded, alertBody, alertTitle, delayInSeconds) = alertKind.alertNeeded(currentAlertEntry: currentAlertEntry, nextAlertEntry: nextAlertEntry, lastBgReading: lastBgReading, lastButOneBgREading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo) + let (alertNeeded, alertBody, alertTitle, delayInSeconds) = alertKind.alertNeeded(currentAlertEntry: currentAlertEntry, nextAlertEntry: nextAlertEntry, lastBgReading: lastBgReading, lastButOneBgReading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo) // create a new property for delayInSeconds, if it's nil then set to 0 - because returnvalue might either be nil or 0, to be treated in the same way var delayInSecondsToUse = delayInSeconds == nil ? 0 : delayInSeconds! @@ -578,12 +613,46 @@ public class AlertManager:NSObject { } - // create the content for the alert notification, set body and text, category + // create the content for the alert notification, set body and text, category and also attachments and userInfo dict if available let content = UNMutableNotificationContent() - // set body, text - if let alertBody = alertBody {content.body = alertBody} + // set body, title if let alertTitle = alertTitle {content.title = alertTitle} + if let alertBody = alertBody {content.body = alertBody} + + var alertNotificationDictionary = AlertNotificationDictionary() + + if let lastBgReading = lastBgReading { + alertNotificationDictionary.bgValueString = lastBgReading.unitizedString(unitIsMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) + + // a bit of an ugly way of doing it, but it's the easiest way to make the notification payload codable and it's only used for this + switch lastBgReading.bgRangeDescription() { + case .inRange: + alertNotificationDictionary.BgRangeDescriptionAsInt = 0 + case .notUrgent: + alertNotificationDictionary.BgRangeDescriptionAsInt = 1 + case .urgent: + alertNotificationDictionary.BgRangeDescriptionAsInt = 2 + } + + if let lastButOneBgReading = lastButOneBgReading { + alertNotificationDictionary.deltaString = lastBgReading.unitizedDeltaString(previousBgReading: lastButOneBgReading, showUnit: false, highGranularity: false, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) + } + + alertNotificationDictionary.alertTitle = alertKind.alertTitle().uppercased() + alertNotificationDictionary.trendString = lastBgReading.slopeArrow() + alertNotificationDictionary.alertUrgencyTypeRawValue = alertKind.alertUrgencyType().rawValue + + if let userInfo = alertNotificationDictionary.asDictionary { + content.userInfo = userInfo + } + } + + let expandedAttachment = try! UNNotificationAttachment(identifier: "image", url: URL.documentsDirectory.appendingPathComponent("\(ConstantsGlucoseChartSwiftUI.filenameNotificationExpandedImage).png"), options: [UNNotificationAttachmentOptionsThumbnailHiddenKey: true]) + + let thumbnailAttachment = try! UNNotificationAttachment(identifier: "thumbnail", url: URL.documentsDirectory.appendingPathComponent("\(ConstantsGlucoseChartSwiftUI.filenameNotificationThumbnailImage).png"), options: [UNNotificationAttachmentOptionsThumbnailHiddenKey: false]) + + content.attachments = [expandedAttachment, thumbnailAttachment] // if snooze from notification in homescreen is needed then set the categoryIdentifier if applicableAlertType.snooze { diff --git a/xdrip/Managers/Alerts/AlertNotificationDictionary.swift b/xdrip/Managers/Alerts/AlertNotificationDictionary.swift new file mode 100644 index 000000000..dee447ae5 --- /dev/null +++ b/xdrip/Managers/Alerts/AlertNotificationDictionary.swift @@ -0,0 +1,25 @@ +// +// AlertNotificationDictionary.swift +// xdrip +// +// Created by Paul Plant on 11/5/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation + +/// model of the data we'll send to the notification content extension as userInfo for alerts +struct AlertNotificationDictionary: Codable { + var alertTitle: String? + var bgValueString: String? + var BgRangeDescriptionAsInt: Int? + var trendString: String? + var deltaString: String? + var isMgDl: Bool? + var alertUrgencyTypeRawValue: Int? + + var asDictionary: [String: Any]? { + guard let data = try? JSONEncoder().encode(self) else { return nil } + return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] } + } +} diff --git a/xdrip/Managers/Alerts/AlertSnoozeStatus.swift b/xdrip/Managers/Alerts/AlertSnoozeStatus.swift new file mode 100644 index 000000000..0f6292ea5 --- /dev/null +++ b/xdrip/Managers/Alerts/AlertSnoozeStatus.swift @@ -0,0 +1,31 @@ +// +// AlertSnoozeStatus.swift +// xdrip +// +// Created by Paul Plant on 15/5/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation + +/// defines if the snooze status is inactive, non-urgent are snoozed, or urgent alarms are snoozed +/// also used to udpate the UI to reflect the snooze status +public enum AlertSnoozeStatus: Int { + case inactive = 0 + case urgent = 1 + case notUrgent = 2 + case allSnoozed = 3 + + var description: String { + switch self { + case .inactive: + return "inactive" + case .urgent: + return "urgent" + case .notUrgent: + return "non-urgent" + case .allSnoozed: + return "all snoozed" + } + } +} diff --git a/xdrip/Managers/Alerts/AlertUrgencyType.swift b/xdrip/Managers/Alerts/AlertUrgencyType.swift new file mode 100644 index 000000000..a96c706af --- /dev/null +++ b/xdrip/Managers/Alerts/AlertUrgencyType.swift @@ -0,0 +1,105 @@ +// +// alertUrgencyType.swift +// xdrip +// +// Created by Paul Plant on 8/5/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import UIKit + +/// define urgency levels so that we can pass this to notifications to display them in a relevant style... +public enum AlertUrgencyType: Int { + case urgent = 0 + case warning = 1 + case normal = 2 + + public init?(rawValue: Int) { + switch rawValue { + case 0: + self = .urgent + case 1: + self = .warning + default: + self = .normal + } + } + + public var rawValue: Int { + switch self { + case .urgent: + return 0 + case .warning: + return 1 + case .normal: + return 2 + } + } + + var bgValueTextColor: UIColor { + switch self { + case .urgent: + return UIColor.systemRed + case .warning: + return UIColor.systemYellow + default: + return UIColor.systemGreen + } + } + + var bannerTextColor: UIColor { + switch self { + case .urgent: + return UIColor.white.withAlphaComponent(0.85) + case .warning: + return UIColor.black + default: + return UIColor.white.withAlphaComponent(0.85) + } + } + + var bannerBackgroundColor: UIColor { + switch self { + case .urgent: + return UIColor.systemRed + case .warning: + return UIColor.systemYellow + default: + return UIColor.clear + } + } + + var alertImageString: String? { + switch self { + case .urgent: + return "exclamationmark.triangle.fill" + case .warning: + return "exclamationmark.triangle" + default: + return "info.circle" + } + } + + var description: String { + switch self { + case .urgent: + return "Urgent" + case .warning: + return "Warning" + case .normal: + return "Normal" + } + } + + var alertTitleType: String { + switch self { + case .urgent: + return Texts_Alerts.alertTypeUrgent + case .warning: + return Texts_Alerts.alertTypeWarning + default: + return "" + } + } +} diff --git a/xdrip/Managers/Charts/GlucoseChartType.swift b/xdrip/Managers/Charts/GlucoseChartType.swift index bd96aa2d2..cf5f1da9b 100644 --- a/xdrip/Managers/Charts/GlucoseChartType.swift +++ b/xdrip/Managers/Charts/GlucoseChartType.swift @@ -12,11 +12,6 @@ import SwiftUI /// holds and returns the different parameters used for creating the newer (2024) SwiftUI glucose charts public enum GlucoseChartType: Int, CaseIterable { - - // when adding GlucoseChartType, add new cases at the end (ie 3, ...) - // if this is done in the middle then a database migration would be required, because the rawvalue is stored as Int16 in the coredata - // the order of the data source types will in the uiview is determined by the initializer init(forRowAt row: Int) - case liveActivity = 0 case dynamicIsland = 1 case watchApp = 2 @@ -26,6 +21,8 @@ public enum GlucoseChartType: Int, CaseIterable { case widgetSystemLarge = 6 case widgetAccessoryRectangular = 7 case siriGlucoseIntent = 8 + case notificationImageThumbnail = 9 + case notificationImageExpanded = 10 var description: String { switch self { @@ -47,6 +44,10 @@ public enum GlucoseChartType: Int, CaseIterable { return "Widget Chart .accessoryRectangular" case .siriGlucoseIntent: return "Siri Glucose Intent Chart" + case .notificationImageThumbnail: + return "Notification Thumbnail Image Chart" + case .notificationImageExpanded: + return "Notification Expanded Image Chart" } } @@ -77,6 +78,10 @@ public enum GlucoseChartType: Int, CaseIterable { return (ConstantsGlucoseChartSwiftUI.viewWidthWidgetAccessoryRectangular, ConstantsGlucoseChartSwiftUI.viewHeightWidgetAccessoryRectangular) case .siriGlucoseIntent: return (ConstantsGlucoseChartSwiftUI.viewWidthWidgetSiriGlucoseIntent, ConstantsGlucoseChartSwiftUI.viewHeightWidgetSiriGlucoseIntent) + case .notificationImageThumbnail: + return (ConstantsGlucoseChartSwiftUI.viewWidthNotificationThumbnailImage, ConstantsGlucoseChartSwiftUI.viewHeightNotificationThumbnailImage) + case .notificationImageExpanded: + return (ConstantsGlucoseChartSwiftUI.viewWidthNotificationExpandedImage, ConstantsGlucoseChartSwiftUI.viewHeightNotificationExpandedImage) } } @@ -105,6 +110,10 @@ public enum GlucoseChartType: Int, CaseIterable { return ConstantsGlucoseChartSwiftUI.hoursToShowWidgetAccessoryRectangular case .siriGlucoseIntent: return ConstantsGlucoseChartSwiftUI.hoursToShowWidgetSiriGlucoseIntent + case .notificationImageThumbnail: + return ConstantsGlucoseChartSwiftUI.hoursToShowNotificationThumbnailImage + case .notificationImageExpanded: + return ConstantsGlucoseChartSwiftUI.hoursToShowNotificationExpandedImage } } @@ -137,6 +146,10 @@ public enum GlucoseChartType: Int, CaseIterable { return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWidgetAccessoryRectangular case .siriGlucoseIntent: return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterSiriGlucoseIntent + case .notificationImageThumbnail: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterNotificationThumbnailImage + case .notificationImageExpanded: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterNotificationExpandedImage } } @@ -229,7 +242,7 @@ public enum GlucoseChartType: Int, CaseIterable { func yAxisShowLabels() -> Visibility { switch self { - case .siriGlucoseIntent, .widgetSystemLarge: + case .siriGlucoseIntent, .widgetSystemLarge, .notificationImageExpanded: return .automatic default: return .hidden @@ -238,7 +251,7 @@ public enum GlucoseChartType: Int, CaseIterable { func yAxisLabelOffsetX() -> CGFloat { switch self { - case .siriGlucoseIntent, .widgetSystemLarge: + case .siriGlucoseIntent, .widgetSystemLarge, .notificationImageExpanded: return ConstantsGlucoseChartSwiftUI.yAxisLabelOffsetX default: return 0 @@ -247,10 +260,24 @@ public enum GlucoseChartType: Int, CaseIterable { func yAxisLabelOffsetY() -> CGFloat { switch self { - case .siriGlucoseIntent, .widgetSystemLarge: + case .siriGlucoseIntent, .widgetSystemLarge, .notificationImageExpanded: return ConstantsGlucoseChartSwiftUI.yAxisLabelOffsetY default: return 0 } } + + + // MARK: - filename properties if generating an image of the chart/view + + func filename() -> String { + switch self { + case .notificationImageThumbnail: + return ConstantsGlucoseChartSwiftUI.filenameNotificationThumbnailImage + case .notificationImageExpanded: + return ConstantsGlucoseChartSwiftUI.filenameNotificationExpandedImage + default: + return "" + } + } } diff --git a/xdrip/Storyboards/Base.lproj/Main.storyboard b/xdrip/Storyboards/Base.lproj/Main.storyboard index 031a4d16c..4c1e7c635 100644 --- a/xdrip/Storyboards/Base.lproj/Main.storyboard +++ b/xdrip/Storyboards/Base.lproj/Main.storyboard @@ -2,8 +2,10 @@ - + + + @@ -943,7 +945,6 @@ - @@ -1062,8 +1063,45 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -1076,14 +1114,14 @@ @@ -1093,21 +1131,23 @@ - - + + + + - + + + + + + @@ -1116,6 +1156,10 @@ + + + + @@ -2370,17 +2414,20 @@ + + + - + - + - + - + diff --git a/xdrip/Texts/TextsAlerts.swift b/xdrip/Texts/TextsAlerts.swift index 26bb3244a..2870d3ca9 100644 --- a/xdrip/Texts/TextsAlerts.swift +++ b/xdrip/Texts/TextsAlerts.swift @@ -58,15 +58,15 @@ class Texts_Alerts { }() static let alertStart: String = { - return NSLocalizedString("alertstart", tableName: filename, bundle: Bundle.main, value: "Apply from:", comment: "an alentry is applicable as of a certain timestamp in the day, this is the text in the field in the settings screen that allows to modify this timestamp") + return NSLocalizedString("alertstart", tableName: filename, bundle: Bundle.main, value: "Apply from:", comment: "an alert is applicable as of a certain timestamp in the day, this is the text in the field in the settings screen that allows to modify this timestamp") }() static let alertValue: String = { - return NSLocalizedString("alertvalue", tableName: filename, bundle: Bundle.main, value: "Value", comment: "an alentry is applicable as of a certain value (eg low alert as of 70 mg/dl), this is the name of the field in the settings screen that allows to modify this valule") + return NSLocalizedString("alertvalue", tableName: filename, bundle: Bundle.main, value: "Value", comment: "an alert is applicable as of a certain value (eg low alert as of 70 mg/dl), this is the name of the field in the settings screen that allows to modify this valule") }() static let alerttype: String = { - return NSLocalizedString("alerttype", tableName: filename, bundle: Bundle.main, value: "Alarm Type", comment: "an alentry is applicable as of a certain value (eg low alert as of 70 mg/dl), this is the name of the field in the settings screen that allows to modify this valule") + return NSLocalizedString("alerttype", tableName: filename, bundle: Bundle.main, value: "Alarm Type", comment: "an alert is applicable as of a certain value (eg low alert as of 70 mg/dl), this is the name of the field in the settings screen that allows to modify this valule") }() static let changeAlertValue: String = { @@ -77,4 +77,12 @@ class Texts_Alerts { return NSLocalizedString("confirmdeletionalert", tableName: filename, bundle: Bundle.main, value: "Delete Alarm?", comment: "when trying to delete an alert, user needs to confirm first, this is the message") }() + static let alertTypeUrgent: String = { + return NSLocalizedString("alertTypeUrgent", tableName: filename, bundle: Bundle.main, value: "Urgent", comment: "text to show an urgent alarm type") + }() + + static let alertTypeWarning: String = { + return NSLocalizedString("alertTypeWarning", tableName: filename, bundle: Bundle.main, value: "Warning", comment: "text to show a warning alarm type") + }() + } diff --git a/xdrip/Texts/TextsHomeView.swift b/xdrip/Texts/TextsHomeView.swift index dc3ab6ad0..0cd935974 100644 --- a/xdrip/Texts/TextsHomeView.swift +++ b/xdrip/Texts/TextsHomeView.swift @@ -8,6 +8,22 @@ enum Texts_HomeView { return NSLocalizedString("presnooze", tableName: filename, bundle: Bundle.main, value: "Snooze", comment: "Text in button on home screen") }() + static let snoozeAllTitle:String = { + return NSLocalizedString("snoozeAllTitle", tableName: filename, bundle: Bundle.main, value: "Snooze All Alarms", comment: "snooze all text in snooze screen") + }() + + static let snoozeAllDisabled:String = { + return NSLocalizedString("snoozeAllDisabled", tableName: filename, bundle: Bundle.main, value: "Snooze All is Disabled", comment: "snooze all is disabled text in snooze screen") + }() + + static let snoozeAllSnoozedUntil:String = { + return NSLocalizedString("snoozeAllSnoozedUntil", tableName: filename, bundle: Bundle.main, value: "All alarms are snoozed until", comment: "snooze all until text in snooze screen") + }() + + static let snoozeUrgentAlarms:String = { + return NSLocalizedString("snoozeUrgentAlarms", tableName: filename, bundle: Bundle.main, value: "Some urgent alarms are snoozed", comment: "text to inform that some of the urgent alarms are snoozed") + }() + static let sensor:String = { return NSLocalizedString("sensor", tableName: filename, bundle: Bundle.main, value: "Sensor", comment: "Literally 'Sensor', used as name in the button in the home screen, but also in text in pop ups") }() diff --git a/xdrip/View Controllers/Helpers/Picker View Controller/PickerViewController.swift b/xdrip/View Controllers/Helpers/Picker View Controller/PickerViewController.swift index f611bfcdf..50654627e 100644 --- a/xdrip/View Controllers/Helpers/Picker View Controller/PickerViewController.swift +++ b/xdrip/View Controllers/Helpers/Picker View Controller/PickerViewController.swift @@ -50,6 +50,9 @@ final class PickerViewController : UIViewController { // remove the uiviewcontroller self.dismiss(animated: true, completion: nil) + + // force a state change so that the observer in RVC will pick it up and refresh the snooze icon state + UserDefaults.standard.updateSnoozeStatus = !UserDefaults.standard.updateSnoozeStatus } diff --git a/xdrip/View Controllers/Root View Controller/RootViewController.swift b/xdrip/View Controllers/Root View Controller/RootViewController.swift index 78d19858d..fe7260cfe 100644 --- a/xdrip/View Controllers/Root View Controller/RootViewController.swift +++ b/xdrip/View Controllers/Root View Controller/RootViewController.swift @@ -683,6 +683,8 @@ final class RootViewController: UIViewController, ObservableObject { } + self.updateSnoozeStatus() + IntentDonationManager.shared.donate(intent: GlucoseIntent()) } @@ -916,6 +918,12 @@ final class RootViewController: UIViewController, ObservableObject { // force a manual complication update UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.forceComplicationUpdate.rawValue, options: .new, context: nil) + // force the snooze icon status to be updated + UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.updateSnoozeStatus.rawValue, options: .new, context: nil) + + // if the snooze all until data changes, update the UI + UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.snoozeAllAlertsUntilDate.rawValue, options: .new, context: nil) + // setup delegate for UNUserNotificationCenter UNUserNotificationCenter.current().delegate = self @@ -997,6 +1005,8 @@ final class RootViewController: UIViewController, ObservableObject { // so need to donate here IntentDonationManager.shared.donate(intent: GlucoseIntent()) + self.updateSnoozeStatus() + // update the connection status immediately (this will give the user a visual feedback that the connection was lost in the background if they have disabled keep-alive) self.setFollowerConnectionAndHeartbeatStatus() @@ -1702,7 +1712,10 @@ final class RootViewController: UIViewController, ObservableObject { watchManager?.updateWatchApp(forceComplicationUpdate: true) UserDefaults.standard.forceComplicationUpdate = false } - + + case UserDefaults.Key.updateSnoozeStatus: + updateSnoozeStatus() + default: break @@ -2121,6 +2134,8 @@ final class RootViewController: UIViewController, ObservableObject { return } + let isMgDl = UserDefaults.standard.bloodGlucoseUnitIsMgDl + // get lastReading, with a calculatedValue - no check on activeSensor because in follower mode there is no active sensor let lastReading = bgReadingsAccessor.get2LatestBgReadings(minimumTimeIntervalInMinutes: 4.0) @@ -2158,7 +2173,7 @@ final class RootViewController: UIViewController, ObservableObject { guard readingValueForBadge > 12 else {return} // high limit to 400 if readingValueForBadge >= 400.0 {readingValueForBadge = 400.0} - // low limit ti 40 + // low limit to 40 if readingValueForBadge <= 40.0 {readingValueForBadge = 40.0} // check if notification on home screen is enabled in the settings @@ -2179,21 +2194,25 @@ final class RootViewController: UIViewController, ObservableObject { } notificationContent.badge = NSNumber(value: readingValueForBadge.rawValue) - } // Configure notificationContent title, which is bg value in correct unit, add also slopeArrow if !hideSlope and finally the difference with previous reading, if there is one - var calculatedValueAsString = lastReading[0].unitizedString(unitIsMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) + var calculatedValueAsString = lastReading[0].unitizedString(unitIsMgDl: isMgDl) + " " + (isMgDl ? Texts_Common.mgdl : Texts_Common.mmol) + if !lastReading[0].hideSlope { calculatedValueAsString = calculatedValueAsString + " " + lastReading[0].slopeArrow() } + if lastReading.count > 1 { - calculatedValueAsString = calculatedValueAsString + " " + lastReading[0].unitizedDeltaString(previousBgReading: lastReading[1], showUnit: true, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) + //calculatedValueAsString = calculatedValueAsString // + " " + lastReading[0].unitizedDeltaString(previousBgReading: lastReading[1], showUnit: true, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) + + notificationContent.body = lastReading[0].unitizedDeltaString(previousBgReading: lastReading[1], showUnit: true, highGranularity: true, mgdl: isMgDl) + } else { + // must set a body otherwise notification doesn't show up on iOS10 + notificationContent.body = " " } - notificationContent.title = calculatedValueAsString - // must set a body otherwise notification doesn't show up on iOS10 - notificationContent.body = " " + notificationContent.title = calculatedValueAsString // Create Notification Request let notificationRequest = UNNotificationRequest(identifier: ConstantsNotifications.NotificationIdentifierForBgReading.bgReadingNotificationRequest, content: notificationContent, trigger: nil) @@ -2215,7 +2234,7 @@ final class RootViewController: UIViewController, ObservableObject { if UserDefaults.standard.showReadingInAppBadge && (UserDefaults.standard.isMaster || (!UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType != .disabled)) { // rescale of unit is mmol - readingValueForBadge = readingValueForBadge.mgdlToMmol(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) + readingValueForBadge = readingValueForBadge.mgdlToMmol(mgdl: isMgDl) // if unit is mmol and if value needs to be multiplied by 10, then multiply by 10 if !UserDefaults.standard.bloodGlucoseUnitIsMgDl && UserDefaults.standard.multipleAppBadgeValueWith10 { @@ -2618,6 +2637,8 @@ final class RootViewController: UIViewController, ObservableObject { // unwrap alerts and check alerts if let alertManager = alertManager { + createNotificationImages() + // check if an immediate alert went off that shows the current reading if alertManager.checkAlerts(maxAgeOfLastBgReadingInSeconds: ConstantsFollower.maximumBgReadingAgeForAlertsInSeconds) { @@ -3530,7 +3551,7 @@ final class RootViewController: UIViewController, ObservableObject { let dataSourceDescription = UserDefaults.standard.isMaster ? UserDefaults.standard.activeSensorDescription ?? "" : UserDefaults.standard.followerDataSourceType.fullDescription - var showLiveActivity: Bool = UserDefaults.standard.isMaster || (!UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType == .heartbeat) + var showLiveActivity: Bool = true // UserDefaults.standard.isMaster || (!UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType == .heartbeat) if showLiveActivity { // now that we've got the current BG value, let's refine the check to see if we should run/show the live activity @@ -3578,6 +3599,69 @@ final class RootViewController: UIViewController, ObservableObject { WidgetCenter.shared.reloadAllTimelines() } } + + /// store notification glucose chart images in the app container documents folder + private func createNotificationImages() { + // create a small thumbnail glucose chart image to show in the standard iOS notification banner + createNotificationImage(glucoseChartType: .notificationImageThumbnail) + + // create a bigger, full glucose chart image to display in the Notification Content Extension view + createNotificationImage(glucoseChartType: .notificationImageExpanded) + + /// create an image based upon a glucose chart view and save it to the app container documents directory + /// - Parameter glucoseChartType: the type of glucose chart type we want to generate (i.e. thumbnail or full notification chart) + func createNotificationImage(glucoseChartType: GlucoseChartType) { + if let bgReadingsAccessor = self.bgReadingsAccessor { + let bgReadings = bgReadingsAccessor.getLatestBgReadings(limit: nil, fromDate: Date().addingTimeInterval(-3600 * glucoseChartType.hoursToShow(liveActivitySize: .normal)), forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false) + + if bgReadings.count > 0 { + var bgReadingValues: [Double] = [] + var bgReadingDates: [Date] = [] + + for bgReading in bgReadings { + bgReadingValues.append(bgReading.calculatedValue) + bgReadingDates.append(bgReading.timeStamp) + } + + // create a chart view with just bg reading values and dates + let glucoseChartView = GlucoseChartView(glucoseChartType: glucoseChartType, bgReadingValues: bgReadingValues, bgReadingDates: bgReadingDates, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, liveActivitySize: .normal, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil) + + // render the glucose chart view as an image object + guard let notificationImage = ImageRenderer(content: glucoseChartView).uiImage else { return } + + // try and save the image to the documents directory in the app container + if let imageToSave = notificationImage.pngData() { + let fileUrl = URL.documentsDirectory.appendingPathComponent("\(glucoseChartType.filename()).png") + try? imageToSave.write(to: fileUrl) + } + } + } + } + } + + // updates the toolbar UI to show the current snooze status of the app + private func updateSnoozeStatus() { + if let alertManager = alertManager { + switch alertManager.snoozeStatus() { + case .allSnoozed: + // all alerts are snoozed so let's make it clear - change to red filled icon + preSnoozeToolbarButtonOutlet.tintColor = .red + preSnoozeToolbarButtonOutlet.image = UIImage(systemName: "speaker.slash.circle.fill") + case .urgent: + // urgent low, low or fast drop alerts are snoozed - change to red outline icon + preSnoozeToolbarButtonOutlet.tintColor = .red + preSnoozeToolbarButtonOutlet.image = UIImage(systemName: "speaker.slash.circle") + case .notUrgent: + // some other alert except urgent low, low or fast drop is snoozed so let's just change the icon + preSnoozeToolbarButtonOutlet.tintColor = nil + preSnoozeToolbarButtonOutlet.image = UIImage(systemName: "speaker.slash.circle") + default: + // no alerts are snoozed so show default icon/colour + preSnoozeToolbarButtonOutlet.tintColor = nil + preSnoozeToolbarButtonOutlet.image = UIImage(systemName: "speaker.wave.2.circle") + } + } + } private func setNightscoutSyncTreatmentsRequiredToTrue() { if (UserDefaults.standard.timeStampLatestNightScoutTreatmentSyncRequest ?? Date.distantPast).timeIntervalSinceNow < -ConstantsNightScout.minimiumTimeBetweenTwoTreatmentSyncsInSeconds { @@ -3742,7 +3826,6 @@ extension RootViewController: UNUserNotificationCenterDelegate { // this will verify if it concerns an alert notification, if not pickerviewData will be nil } else if let pickerViewData = alertManager?.userNotificationCenter(center, willPresent: notification, withCompletionHandler: completionHandler) { - PickerViewController.displayPickerViewController(pickerViewData: pickerViewData, parentController: self) } else if notification.request.identifier == ConstantsNotifications.notificationIdentifierForVolumeTest { diff --git a/xdrip/View Controllers/Root View Controller/SnoozeViewController.swift b/xdrip/View Controllers/Root View Controller/SnoozeViewController.swift index ccbf4fd8e..539ee8c58 100644 --- a/xdrip/View Controllers/Root View Controller/SnoozeViewController.swift +++ b/xdrip/View Controllers/Root View Controller/SnoozeViewController.swift @@ -7,12 +7,63 @@ final class SnoozeViewController: UIViewController { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var snoozeAllLabel: UILabel! + @IBOutlet weak var snoozeAllUISwitch: UISwitch! + @IBOutlet weak var snoozeAllBannerView: UIView! + @IBOutlet weak var snoozeAllBannerText: UILabel! + + // if the UISwitch is tapped, change it's state. If changed to isOn then show Snooze Picker and pass the value back to the view + @IBAction func snoozeAllUISwitchAction(_ sender: Any) { + if UserDefaults.standard.snoozeAllAlertsFromDate == nil { + let defaultSnoozeAllPeriodInMinutes = ConstantsAlerts.defaultSnoozeAllPeriodInMinutes + let snoozeAllValueMinutes = ConstantsAlerts.snoozeAllValueMinutes + var defaultRow = 0 + + for (index, _) in snoozeAllValueMinutes.enumerated() { + if snoozeAllValueMinutes[index] > defaultSnoozeAllPeriodInMinutes { + break + } else { + defaultRow = index + } + } + + let pickerViewData = PickerViewData(withMainTitle: Texts_HomeView.snoozeAllTitle, + withSubTitle: Texts_Alerts.selectSnoozeTime, + withData: ConstantsAlerts.snoozeAllValueStrings, + selectedRow: defaultRow, + withPriority: .high, + actionButtonText: Texts_Common.Ok, + cancelButtonText: Texts_Common.Cancel, + onActionClick: { + (snoozeIndex:Int) -> Void in + + // get snooze period + let snoozePeriod = snoozeAllValueMinutes[snoozeIndex] + + UserDefaults.standard.snoozeAllAlertsFromDate = Date() + UserDefaults.standard.snoozeAllAlertsUntilDate = Date().addingTimeInterval(Double(snoozePeriod) * 60) + self.configureSnoozeAllView() + }, + onCancelClick: { () -> Void in }, + didSelectRowHandler: nil + ) + + // create and display pickerViewData + PickerViewController.displayPickerViewController(pickerViewData: pickerViewData, parentController: self) + } else { + UserDefaults.standard.snoozeAllAlertsFromDate = nil + UserDefaults.standard.snoozeAllAlertsUntilDate = nil + } + + configureSnoozeAllView() + } + // reference to alertManager - private var alertManager:AlertManager? + private var alertManager: AlertManager? // MARK: - Public functions - public func configure(alertManager:AlertManager?) { + public func configure(alertManager: AlertManager?) { self.alertManager = alertManager } @@ -22,29 +73,30 @@ final class SnoozeViewController: UIViewController { super.viewDidLoad() titleLabel.text = Texts_HomeView.snoozeButton + snoozeAllLabel.text = Texts_HomeView.snoozeAllTitle + snoozeAllBannerView.layer.cornerRadius = 10 + snoozeAllBannerView.layer.masksToBounds = true + setupView() } override func viewWillAppear(_ animated: Bool) { - // restrict rotation of the Snooze View to just portrait. This is important as it is a child view of RootViewController (UIApplication.shared.delegate as! AppDelegate).restrictRotation = .portrait + configureSnoozeAllView() } override func viewWillDisappear(_ animated: Bool) { - // as the snooze view is removed, all the RootViewController to rotate again if permitted if UserDefaults.standard.allowScreenRotation { - (UIApplication.shared.delegate as! AppDelegate).restrictRotation = .allButUpsideDown - } else { - (UIApplication.shared.delegate as! AppDelegate).restrictRotation = .portrait - } + // force a state change so that the observer in RVC will pick it up and refresh the snooze icon state + UserDefaults.standard.updateSnoozeStatus = !UserDefaults.standard.updateSnoozeStatus } // MARK: - private helper functions @@ -61,10 +113,31 @@ final class SnoozeViewController: UIViewController { tableView.separatorInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) tableView.dataSource = self tableView.delegate = self - } } + private func configureSnoozeAllView() { + if alertManager?.snoozeStatus() == .allSnoozed, let snoozeAllAlertsUntilDate = UserDefaults.standard.snoozeAllAlertsUntilDate { + // if snoozed till after 00:00 then show date and time when it ends, else only show time + self.snoozeAllUISwitch.isOn = true + snoozeAllBannerText.text = "\u{26A0} " + Texts_HomeView.snoozeAllSnoozedUntil + " " + (snoozeAllAlertsUntilDate.toMidnight() > Date() ? snoozeAllAlertsUntilDate.formatted(date: .abbreviated, time: .shortened) : snoozeAllAlertsUntilDate.formatted(date: .omitted, time: .shortened)) + snoozeAllBannerText.textColor = .white + snoozeAllBannerView.backgroundColor = .systemRed + snoozeAllLabel.textColor = .systemRed + } else if alertManager?.snoozeStatus() == .urgent { + self.snoozeAllUISwitch.isOn = false + snoozeAllBannerView.backgroundColor = ConstantsAlerts.bannerBackgroundColorWhenNotAllSnoozed + snoozeAllBannerText.text = "\u{2757}" + Texts_HomeView.snoozeUrgentAlarms + snoozeAllBannerText.textColor = .systemRed + snoozeAllLabel.textColor = UIColor(named: "colorPrimary") + } else { + self.snoozeAllUISwitch.isOn = false + snoozeAllBannerView.backgroundColor = ConstantsAlerts.bannerBackgroundColorWhenNotAllSnoozed + snoozeAllBannerText.text = Texts_HomeView.snoozeAllDisabled + snoozeAllBannerText.textColor = ConstantsAlerts.bannerTextColorWhenNotAllSnoozed + snoozeAllLabel.textColor = UIColor(named: "colorPrimary") + } + } } // MARK: - Conform to UITableViewDataSource @@ -77,7 +150,7 @@ extension SnoozeViewController: UITableViewDataSource { return AlertKind.allCases.count } - + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // just one row per alarm type @@ -100,26 +173,28 @@ extension SnoozeViewController: UITableViewDataSource { } // get snoozeParameters for the alertKind - let (isSnoozed, remainingSeconds) = alertManager.getSnoozeParameters(alertKind: alertKind).getSnoozeValue() - + let (isSnoozed, remainingSeconds) = alertManager.getSnoozeParameters(alertKind: alertKind).getSnoozeValue() + if isSnoozed { guard let remainingSeconds = remainingSeconds else { fatalError("In SnoozeViewController, remainingSeconds is nil but alert is snoozed") } - + // till when snoozed, as Date let snoozedTillDate = Date(timeIntervalSinceNow: Double(remainingSeconds)) // if snoozed till after 00:00 then show date and time when it ends, else only show time let showDate = snoozedTillDate.toMidnight() > Date() - cell.textLabel?.text = TextsSnooze.snoozed_until + " " + snoozedTillDate.toStringInUserLocale(timeStyle: .short, dateStyle: showDate ? .short : .none) + cell.textLabel?.text = TextsSnooze.snoozed_until + " " + (showDate ? snoozedTillDate.formatted(date: .abbreviated, time: .shortened) : snoozedTillDate.formatted(date: .omitted, time: .shortened))//.toStringInUserLocale(timeStyle: .short, dateStyle: showDate ? .short : .none) // "\u{26A0} " + + cell.textLabel?.textColor = .white } else { - + cell.textLabel?.text = TextsSnooze.not_snoozed - + cell.textLabel?.textColor = .gray + } // no detailed text to be shown, the snooze time is already given in the textLabel @@ -136,19 +211,23 @@ extension SnoozeViewController: UITableViewDataSource { // changing from off to on. Means user wants to pre-snooze if isOn { - + // create and display pickerViewData - PickerViewController.displayPickerViewController(pickerViewData: alertManager.createPickerViewData(forAlertKind: alertKind, content: nil, actionHandler: { reloadRow() }, cancelHandler: { reloadRow() }), parentController: self) + PickerViewController.displayPickerViewController(pickerViewData: alertManager.createPickerViewData(forAlertKind: alertKind, content: nil, actionHandler: { + reloadRow() + self.configureSnoozeAllView() + }, cancelHandler: { reloadRow() }), parentController: self) } else { // changing from on to off. Means user wants to unsnooze - + alertManager.unSnooze(alertKind: alertKind) + self.configureSnoozeAllView() + reloadRow() } - }) return cell @@ -161,10 +240,10 @@ extension SnoozeViewController: UITableViewDataSource { fatalError("In titleForHeaderInSection, could not create alertKind") } - return alertKind.alertTitle() + return (alertKind.alertUrgencyType() == .urgent ? "\u{2757}" : "") + alertKind.alertTitle() } - + } // MARK: - Conform to UITableViewDelegate @@ -180,7 +259,7 @@ extension SnoozeViewController: UITableViewDelegate { } } - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) From 6bfec91780916d8258e43d7d481ededa60c74894 Mon Sep 17 00:00:00 2001 From: Paul Plant <37302780+paulplant@users.noreply.github.com> Date: Thu, 30 May 2024 20:34:25 +0200 Subject: [PATCH 03/23] add custom watch notifications - match iOS custom notification style --- .../Base.lproj/MainInterface.storyboard | 28 ++--- .../NotificationViewController.swift | 38 +++---- .../DataModels/NotificationController.swift | 59 +++++++++++ xDrip Watch App/Views/NotificationView.swift | 100 ++++++++++++++++++ xDrip Watch App/xDripWatchApp.swift | 3 + xdrip.xcodeproj/project.pbxproj | 30 ++++-- xdrip/Constants/ConstantsAlerts.swift | 20 ++++ .../ConstantsGlucoseChartSwiftUI.swift | 14 ++- xdrip/Managers/Alerts/AlertManager.swift | 14 +++ .../Alerts/AlertNotificationDictionary.swift | 1 + xdrip/Managers/Alerts/AlertUrgencyType.swift | 59 ++++++----- xdrip/Managers/Charts/GlucoseChartType.swift | 19 ++++ .../RootViewController.swift | 3 + 13 files changed, 303 insertions(+), 85 deletions(-) create mode 100644 xDrip Watch App/DataModels/NotificationController.swift create mode 100644 xDrip Watch App/Views/NotificationView.swift diff --git a/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard b/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard index 557f615e4..5e4f4a749 100644 --- a/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard +++ b/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard @@ -5,7 +5,6 @@ - @@ -19,13 +18,13 @@ - + - - - - - - - - - + - - + @@ -78,11 +68,10 @@ + - - @@ -90,7 +79,6 @@ - @@ -112,8 +100,8 @@ - - - + + + diff --git a/xDrip Notification Context Extension/NotificationViewController.swift b/xDrip Notification Context Extension/NotificationViewController.swift index 353e104e3..f81a2d578 100644 --- a/xDrip Notification Context Extension/NotificationViewController.swift +++ b/xDrip Notification Context Extension/NotificationViewController.swift @@ -10,6 +10,7 @@ import UIKit import UserNotifications import UserNotificationsUI +/// customer content view controller to modify how the notifications are displayed to the user (only used for notifications with the snoozeCategory (which is most of them)) class NotificationViewController: UIViewController, UNNotificationContentExtension { @IBOutlet weak var alertIconOutlet: UIImageView! @@ -22,7 +23,7 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi @IBOutlet weak var unitLabel: UILabel! override func viewDidLoad() { - super.viewDidLoad() + super.viewDidLoad() // set the notification view size and update it self.preferredContentSize = CGSize(width: UIScreen.main.bounds.size.width, height: 310) @@ -35,30 +36,20 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi // pull the userInfo dictionary from the received notification let userInfo = notification.request.content.userInfo - // set the image and colours based upon the alertUrgencyType - if let alertUrgencyType = AlertUrgencyType(rawValue: userInfo["alertUrgencyTypeRawValue"] as? Int ?? 0) { - if let alertImageString = alertUrgencyType.alertImageString { - alertIconOutlet.image = UIImage(systemName: alertImageString) - alertIconOutlet.tintColor = alertUrgencyType.bannerTextColor - } - - alertTitleLabel.textColor = alertUrgencyType.bannerTextColor - bannerOutlet.backgroundColor = alertUrgencyType.bannerBackgroundColor + // set the text and banner colours based upon the alertUrgencyType + if let alertUrgencyType = AlertUrgencyType(rawValue: userInfo["alertUrgencyTypeRawValue"] as? Int ?? 2) { + alertTitleLabel.textColor = alertUrgencyType.bannerTextUIColor + bannerOutlet.backgroundColor = alertUrgencyType.bannerBackgroundUIColor } - // set the title label. This is common for all notifications + // set the text labels from the userInfo dictionary (assuming any of them could be nil) alertTitleLabel.text = userInfo["alertTitle"] as? String ?? "" - - var bgValueAndTrendString = userInfo["bgValueString"] as? String ?? "" - - if let trendString = userInfo["trendString"] as? String { - bgValueAndTrendString += trendString - } - - bgValueLabel.text = bgValueAndTrendString + bgValueLabel.text = (userInfo["bgValueString"] as? String ?? "") + (userInfo["trendString"] as? String ?? "") + deltaLabel.text = userInfo["deltaString"] as? String ?? "-" + unitLabel.text = (userInfo["isMgDl"] as? Bool ?? true) ? Texts_Common.mgdl : Texts_Common.mmol // set the bg value label color as per the rest of the app UI color - // it's a bit of a workaround to keep the dictionary as codable + // it's a bit of a workaround using Ints to keep the dictionary as codable if let BgRangeDescriptionAsInt = userInfo["BgRangeDescriptionAsInt"] as? Int { switch BgRangeDescriptionAsInt { case 0: // inRange @@ -70,15 +61,12 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi } } - deltaLabel.text = userInfo["deltaString"] as? String ?? "-" - unitLabel.text = (userInfo["isMgDl"] as? Bool ?? true) ? Texts_Common.mgdl : Texts_Common.mmol - // let's add the last attachment as an image to the view - // the first attachment is the thumbnail, the expanded image is the second/last one. + // the first attachment is the thumbnail, the expanded image is the second/last one if let attachment = notification.request.content.attachments.last { if attachment.url.startAccessingSecurityScopedResource() { let data = NSData(contentsOfFile: attachment.url.path) - self.glucoseChartImage.image = UIImage(data: data! as Data) + glucoseChartImage.image = UIImage(data: data! as Data) attachment.url.stopAccessingSecurityScopedResource() } } diff --git a/xDrip Watch App/DataModels/NotificationController.swift b/xDrip Watch App/DataModels/NotificationController.swift new file mode 100644 index 000000000..51b84673e --- /dev/null +++ b/xDrip Watch App/DataModels/NotificationController.swift @@ -0,0 +1,59 @@ +// +// NotificationController.swift +// xDrip Watch App +// +// Created by Paul Plant on 24/5/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import WatchKit +import SwiftUI +import UserNotifications + +class NotificationController: WKUserNotificationHostingController { + var alertTitle: String? + var bgValueAndTrend: String? + var delta: String? + var unit: String? + var alertUrgencyType: AlertUrgencyType? + var bgRangeDescriptionAsInt: Int? + var glucoseChartImage: UIImage? + + override var body: NotificationView { + NotificationView( + alertTitle: alertTitle, + bgValueAndTrend: bgValueAndTrend, + delta: delta, + unit: unit, + alertUrgencyType: alertUrgencyType, + bgRangeDescriptionAsInt: bgRangeDescriptionAsInt, + glucoseChartImage: glucoseChartImage + ) + } + + override func didReceive(_ notification: UNNotification) { + // pull the userInfo dictionary from the received notification + let userInfo = notification.request.content.userInfo + + // set the image and colours based upon the alertUrgencyType + alertUrgencyType = AlertUrgencyType(rawValue: userInfo["alertUrgencyTypeRawValue"] as? Int ?? 0) + + // set the title label. This is common for all notifications + alertTitle = userInfo["alertTitle"] as? String ?? "" + + // set the bg value and trend arrow if available + bgValueAndTrend = (userInfo["bgValueString"] as? String ?? "") + (userInfo["trendString"] as? String ?? "") + + // set the bg value label color as per the rest of the app UI color + // it's a bit of a workaround to keep the dictionary as codable + bgRangeDescriptionAsInt = userInfo["BgRangeDescriptionAsInt"] as? Int ?? 0 + + delta = userInfo["deltaString"] as? String ?? "-" + unit = (userInfo["isMgDl"] as? Bool ?? true) ? Texts_Common.mgdl : Texts_Common.mmol + + // recode the image from the sent userInfo + if let watchNotificationImageData = userInfo["watchNotificationImageAsString"] as? String, let imageData = Data(base64Encoded: watchNotificationImageData, options: .ignoreUnknownCharacters) { + glucoseChartImage = UIImage(data: imageData) + } + } +} diff --git a/xDrip Watch App/Views/NotificationView.swift b/xDrip Watch App/Views/NotificationView.swift new file mode 100644 index 000000000..e70c95864 --- /dev/null +++ b/xDrip Watch App/Views/NotificationView.swift @@ -0,0 +1,100 @@ +// +// NotificationView.swift +// xDrip Watch App +// +// Created by Paul Plant on 24/5/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI +import UIKit + + +struct NotificationView: View { + var alertTitle: String? + var bgValueAndTrend: String? + var delta: String? + var unit: String? + var alertUrgencyType: AlertUrgencyType? + var bgRangeDescriptionAsInt: Int? + var glucoseChartImage: UIImage? + + var body: some View { + + VStack { + Text("\(alertTitle ?? "xDrip4iOS")") + .font(.headline) + .foregroundStyle(alertUrgencyType?.bannerTextColor ?? .white.opacity(0.85)) + .lineLimit(1) + .minimumScaleFactor(0.2) + .frame(maxWidth: .infinity) + .padding(.top, -8) + .padding(.bottom, 8) + .background(alertUrgencyType?.bannerBackgroundColor ?? .black) + + HStack(alignment: .firstTextBaseline) { + Text("\(bgValueAndTrend ?? "-")") + .font(.title3).bold() + .foregroundStyle(bgColor()) + + Spacer() + + HStack(alignment: .firstTextBaseline, spacing: 2) { + Text("\(delta ?? "?")") + .font(.body).bold() + .foregroundStyle(.colorPrimary) + Text("\(unit ?? "")") + .font(.body) + .foregroundStyle(.colorSecondary) + } + } + .padding(.top, 2) + .padding(.bottom, 2) + .padding(.leading, 2) + .padding(.trailing, 2) + + if let glucoseChartImage = glucoseChartImage { + Image(uiImage: glucoseChartImage).resizable() + .frame(width: ConstantsGlucoseChartSwiftUI.viewWidthNotificationWatchImage, height: ConstantsGlucoseChartSwiftUI.viewHeightNotificationWatchImage) + } + } + .background(ConstantsAlerts.notificationBackgroundColor) + } + + func alertTitleColor() -> Color { + if let alertUrgencyType = alertUrgencyType { + switch alertUrgencyType { + case .urgent: + return .red + case .warning: + return .yellow + default: + return .colorPrimary + } + } + + return .colorPrimary + } + + func bgColor() -> Color { + if let bgRangeDescriptionAsInt = bgRangeDescriptionAsInt { + switch bgRangeDescriptionAsInt { + case 0: + return .green + case 1: + return .yellow + default: + return .red + } + } + + return .green + } + +} + + +#Preview { + NotificationView() +} diff --git a/xDrip Watch App/xDripWatchApp.swift b/xDrip Watch App/xDripWatchApp.swift index 15a9ae56c..d4b20cade 100644 --- a/xDrip Watch App/xDripWatchApp.swift +++ b/xDrip Watch App/xDripWatchApp.swift @@ -18,5 +18,8 @@ struct xDrip_Watch_AppApp: App { MainView() }.environmentObject(watchState) } + + // assign the custom view controller to show all watch notifications with snoozeCategory (which will be most of them) + WKNotificationScene(controller: NotificationController.self, category: "snoozeCategoryIdentifier") } } diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index 6ab449475..05c93aec7 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 47037B8E2C072346000BD6BD /* ConstantsAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88EC279260120C000DF0EAF /* ConstantsAlerts.swift */; }; + 47037B8F2C072973000BD6BD /* ConstantsAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88EC279260120C000DF0EAF /* ConstantsAlerts.swift */; }; + 47037B902C072974000BD6BD /* ConstantsAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88EC279260120C000DF0EAF /* ConstantsAlerts.swift */; }; + 47037B912C07B15E000BD6BD /* ConstantsAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88EC279260120C000DF0EAF /* ConstantsAlerts.swift */; }; 47046EA42A6E8BA700A6F736 /* TextsBgReadings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47046EA32A6E8BA700A6F736 /* TextsBgReadings.swift */; }; 47046EA72A6E8F7B00A6F736 /* BgReadings.strings in Resources */ = {isa = PBXBuildFile; fileRef = 47046EA92A6E8F7B00A6F736 /* BgReadings.strings */; }; 470824D2297484B500C52317 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = 470824D1297484B500C52317 /* SwiftCharts */; }; @@ -64,6 +68,10 @@ 474606902B963EA100AC9214 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746068D2B963EA100AC9214 /* View.swift */; }; 474606912B963EA100AC9214 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746068D2B963EA100AC9214 /* View.swift */; }; 474822412BFE62220052D4FB /* ConstantsCalendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E3A2AA23DA520B00E5E98A /* ConstantsCalendar.swift */; }; + 474822442C0108EA0052D4FB /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474822432C0108EA0052D4FB /* NotificationView.swift */; }; + 474822462C01097D0052D4FB /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474822452C01097D0052D4FB /* NotificationController.swift */; }; + 474822472C010DED0052D4FB /* AlertUrgencyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CD642B2BEBEB0E002BDA68 /* AlertUrgencyType.swift */; }; + 474822482C010F380052D4FB /* TextsAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A7B1226A0878004BA588 /* TextsAlerts.swift */; }; 4749EB9B25B36E010072DF8B /* LibreNFC.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4749EB9D25B36E010072DF8B /* LibreNFC.strings */; }; 47503382247420A200D2260B /* BluetoothPeripheralView.strings in Resources */ = {isa = PBXBuildFile; fileRef = 47503384247420A200D2260B /* BluetoothPeripheralView.strings */; }; 4752B400263570DA0081D551 /* ConstantsStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4752B3FF263570DA0081D551 /* ConstantsStatistics.swift */; }; @@ -901,6 +909,8 @@ 4746067D2B962FBD00AC9214 /* SystemLargeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemLargeView.swift; sourceTree = ""; }; 4746067F2B96308A00AC9214 /* WidgetSharedUserDefaultsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetSharedUserDefaultsModel.swift; sourceTree = ""; }; 4746068D2B963EA100AC9214 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; + 474822432C0108EA0052D4FB /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; + 474822452C01097D0052D4FB /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = ""; }; 4749EB9C25B36E010072DF8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/LibreNFC.strings; sourceTree = ""; }; 47503383247420A200D2260B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BluetoothPeripheralView.strings; sourceTree = ""; }; 4752B3FF263570DA0081D551 /* ConstantsStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsStatistics.swift; sourceTree = ""; }; @@ -2016,6 +2026,7 @@ isa = PBXGroup; children = ( 47A6ABEE2B7949B80047A4BA /* WatchStateModel.swift */, + 474822452C01097D0052D4FB /* NotificationController.swift */, ); path = DataModels; sourceTree = ""; @@ -2050,19 +2061,11 @@ 47CD64242BEA9E5A002BDA68 /* xDrip Notification Context Extension.entitlements */, 47CD641D2BE9506C002BDA68 /* Info.plist */, 47CD64182BE9506C002BDA68 /* NotificationViewController.swift */, - 47CD64252BEAA538002BDA68 /* Extensions */, 47CD641A2BE9506C002BDA68 /* MainInterface.storyboard */, ); path = "xDrip Notification Context Extension"; sourceTree = ""; }; - 47CD64252BEAA538002BDA68 /* Extensions */ = { - isa = PBXGroup; - children = ( - ); - path = Extensions; - sourceTree = ""; - }; 47DB06CC2A7013EF00267BE3 /* Followers */ = { isa = PBXGroup; children = ( @@ -2095,6 +2098,7 @@ isa = PBXGroup; children = ( 47A6ABE32B790CC60047A4BA /* MainView.swift */, + 474822432C0108EA0052D4FB /* NotificationView.swift */, 478A92542B8F95930084C394 /* SubViews */, ); path = Views; @@ -3155,9 +3159,9 @@ F8DF765923E350B100063910 /* Dexcom */ = { isa = PBXGroup; children = ( - F8F71D842B7EC19A005076E8 /* G7 */, F816E1222439DB42009EE65B /* G4 */, F8DF765A23E350B100063910 /* G5 */, + F8F71D842B7EC19A005076E8 /* G7 */, ); path = Dexcom; sourceTree = ""; @@ -4170,6 +4174,7 @@ 4746068A2B96380200AC9214 /* ConstantsLibreLinkUp.swift in Sources */, 47A6AC2C2B7D3E170047A4BA /* Double.swift in Sources */, 47EDD1402BDD4F5D00C5A286 /* ConstantsWidgetExtension.swift in Sources */, + 47037B8F2C072973000BD6BD /* ConstantsAlerts.swift in Sources */, 47A6AC412B7D42EC0047A4BA /* TextsCommon.swift in Sources */, 478A92572B8FA1E30084C394 /* ConstantsBGGraphBuilder.swift in Sources */, 4746068F2B963EA100AC9214 /* View.swift in Sources */, @@ -4185,6 +4190,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 47037B902C072974000BD6BD /* ConstantsAlerts.swift in Sources */, 474606912B963EA100AC9214 /* View.swift in Sources */, 4746066D2B9618B800AC9214 /* AccessoryCornerView.swift in Sources */, 478A92652B90AB040084C394 /* GlucoseChartView.swift in Sources */, @@ -4222,6 +4228,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 474822462C01097D0052D4FB /* NotificationController.swift in Sources */, 47A6AC2B2B7D3DE30047A4BA /* GlucoseChartView.swift in Sources */, 47DE41B52B8693CB0041DA19 /* HeaderView.swift in Sources */, 47A6AC342B7D3E260047A4BA /* ConstantsBloodGlucose.swift in Sources */, @@ -4236,6 +4243,7 @@ 47A6AC372B7D3E650047A4BA /* GlucoseChartType.swift in Sources */, 47A6ABE42B790CC60047A4BA /* MainView.swift in Sources */, 474606882B9637F600AC9214 /* TextsSettingsView.swift in Sources */, + 474822482C010F380052D4FB /* TextsAlerts.swift in Sources */, 4793599A2B8A2A4E007D3CEE /* InfoView.swift in Sources */, 47A6ABEF2B7949B80047A4BA /* WatchStateModel.swift in Sources */, 47A6ABE22B790CC60047A4BA /* xDripWatchApp.swift in Sources */, @@ -4244,12 +4252,15 @@ 47A6AC2F2B7D3E170047A4BA /* Date.swift in Sources */, 47CA61E92B97A6BD00C2A597 /* FollowerBackgroundKeepAliveType.swift in Sources */, 47DE41AE2B863D370041DA19 /* WatchState.swift in Sources */, + 47037B8E2C072346000BD6BD /* ConstantsAlerts.swift in Sources */, 47A6AC422B7D42ED0047A4BA /* TextsCommon.swift in Sources */, 47A6AC3B2B7D3F9B0047A4BA /* ConstantsGlucoseChartSwiftUI.swift in Sources */, + 474822442C0108EA0052D4FB /* NotificationView.swift in Sources */, 47DE41B92B87B2680041DA19 /* ConstantsAppleWatch.swift in Sources */, 47CA61E82B9796D200C2A597 /* ConstantsFollower.swift in Sources */, 4746068B2B96380200AC9214 /* ConstantsLibreLinkUp.swift in Sources */, 47A6AC352B7D3E260047A4BA /* ConstantsUI.swift in Sources */, + 474822472C010DED0052D4FB /* AlertUrgencyType.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4263,6 +4274,7 @@ 4734FF562BEFE3F200C7115A /* AlertNotificationDictionary.swift in Sources */, 47CD64192BE9506C002BDA68 /* NotificationViewController.swift in Sources */, 474822412BFE62220052D4FB /* ConstantsCalendar.swift in Sources */, + 47037B912C07B15E000BD6BD /* ConstantsAlerts.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/xdrip/Constants/ConstantsAlerts.swift b/xdrip/Constants/ConstantsAlerts.swift index 9ccea2785..8d1debfdb 100644 --- a/xdrip/Constants/ConstantsAlerts.swift +++ b/xdrip/Constants/ConstantsAlerts.swift @@ -1,4 +1,6 @@ import Foundation +import SwiftUI +import UIKit enum ConstantsAlerts { @@ -10,12 +12,29 @@ enum ConstantsAlerts { /// unlike the specific alarms, we'll set this to a longer period such as 6 hours static let defaultSnoozeAllPeriodInMinutes = 6 * 60 + + // iOS App using UIKit + /// the background color to be used for the notifications (and chart etc) - iOS App using UIKit + static let notificationBackgroundUIColor = UIColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 1) + + /// the background color to be used for the alert title banner of the notifications - iOS App using UIKit + static let notificationBannerBackgroundUIColor = UIColor(red: 0.15, green: 0.15, blue: 0.15, alpha: 1) + /// the snooze all banner background color when not activated static let bannerBackgroundColorWhenNotAllSnoozed = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1) /// the snooze all banner text color when not activated static let bannerTextColorWhenNotAllSnoozed = UIColor.gray + + + // WatchOS App using SwiftUI + /// the background color to be used for the notifications (and chart etc) - WatchOS App using SwiftUI + static let notificationBackgroundColor = Color(red: 0.1, green: 0.1, blue: 0.1, opacity: 1) + + /// the background color to be used for the alert title banner of the notifications - WatchOS App using SwiftUI + static let notificationBannerBackgroundColor = Color(red: 0.15, green: 0.15, blue: 0.15, opacity: 1) + /// snooze times in minutes static let snoozeValueMinutes = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 75, 90, 120, 150, 180, 240, 300, 360, 420, 480, 540, 600, 720, 1440, 10080] @@ -24,6 +43,7 @@ enum ConstantsAlerts { "40 minutes", "45 minutes", "50 minutes", "55 minutes", "1 hour", "1 hour 15 minutes", "1,5 hours", "2 hours", "2,5 hours", "3 hours", "4 hours", "5 hours", "6 hours", "7 hours", "8 hours", "9 hours", "10 hours", "12 hours", "1 day", "1 week"] + /// snooze all times in minutes - this can be much simpler than the individual alert snooze times... static let snoozeAllValueMinutes = [15, 30, 60, 120, 240, 360, 480, 720, 1440, 2880, 10080] diff --git a/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift b/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift index 94111576d..2dda4eb09 100644 --- a/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift +++ b/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift @@ -28,7 +28,7 @@ enum ConstantsGlucoseChartSwiftUI { static let xAxisLabelFirstClippingInMinutes: Double = 8 * 60 static let xAxisLabelLastClippingInMinutes: Double = 12 * 60 - static let cornerRadius: CGFloat = 2 + static let cornerRadius: CGFloat = 0 // ------------------------------------------ @@ -134,9 +134,17 @@ enum ConstantsGlucoseChartSwiftUI { static let filenameNotificationThumbnailImage: String = "notificationThumbnailImage" // expanded image - static let viewWidthNotificationExpandedImage: CGFloat = 373 //320 - static let viewHeightNotificationExpandedImage: CGFloat = 170 //150 + static let viewWidthNotificationExpandedImage: CGFloat = 373 + static let viewHeightNotificationExpandedImage: CGFloat = 170 static let hoursToShowNotificationExpandedImage: Double = 3 static let glucoseCircleDiameterNotificationExpandedImage: Double = 30 static let filenameNotificationExpandedImage: String = "notificationExpandedImage" + + // watch notification image + static let viewWidthNotificationWatchImage: CGFloat = 170 + static let viewHeightNotificationWatchImage: CGFloat = 80 + static let hoursToShowNotificationWatchImage: Double = 2 + static let glucoseCircleDiameterNotificationWatchImage: Double = 30 + static let paddingNotificationWatchImage: Double = 2 + static let filenameNotificationWatchImage: String = "notificationWatchImage" } diff --git a/xdrip/Managers/Alerts/AlertManager.swift b/xdrip/Managers/Alerts/AlertManager.swift index aec0ced48..b835a4467 100644 --- a/xdrip/Managers/Alerts/AlertManager.swift +++ b/xdrip/Managers/Alerts/AlertManager.swift @@ -643,11 +643,25 @@ public class AlertManager:NSObject { alertNotificationDictionary.trendString = lastBgReading.slopeArrow() alertNotificationDictionary.alertUrgencyTypeRawValue = alertKind.alertUrgencyType().rawValue + + // as we can't use the local notification attachments in Watch app + // we need to encode the image as string data and then re-encode it as an image later + let imageURL = URL.documentsDirectory.appendingPathComponent("\(ConstantsGlucoseChartSwiftUI.filenameNotificationWatchImage).png") + let image = UIImage(contentsOfFile: imageURL.path) + let imageData = image?.jpegData(compressionQuality: 1) // no need to try to reduce the quality + + if imageData != nil { + alertNotificationDictionary.watchNotificationImageAsString = imageData?.base64EncodedString(options: .endLineWithLineFeed) + } + + // check that we can correctly serialize the struct and if so, add it to the notification content if let userInfo = alertNotificationDictionary.asDictionary { content.userInfo = userInfo } } + // add the last generated BG chart images as attachments to the notification content + // the bigger expanded image will be hidden from being a thumbnail so the notification short view will pick up the other one let expandedAttachment = try! UNNotificationAttachment(identifier: "image", url: URL.documentsDirectory.appendingPathComponent("\(ConstantsGlucoseChartSwiftUI.filenameNotificationExpandedImage).png"), options: [UNNotificationAttachmentOptionsThumbnailHiddenKey: true]) let thumbnailAttachment = try! UNNotificationAttachment(identifier: "thumbnail", url: URL.documentsDirectory.appendingPathComponent("\(ConstantsGlucoseChartSwiftUI.filenameNotificationThumbnailImage).png"), options: [UNNotificationAttachmentOptionsThumbnailHiddenKey: false]) diff --git a/xdrip/Managers/Alerts/AlertNotificationDictionary.swift b/xdrip/Managers/Alerts/AlertNotificationDictionary.swift index dee447ae5..3683245f3 100644 --- a/xdrip/Managers/Alerts/AlertNotificationDictionary.swift +++ b/xdrip/Managers/Alerts/AlertNotificationDictionary.swift @@ -17,6 +17,7 @@ struct AlertNotificationDictionary: Codable { var deltaString: String? var isMgDl: Bool? var alertUrgencyTypeRawValue: Int? + var watchNotificationImageAsString: String? var asDictionary: [String: Any]? { guard let data = try? JSONEncoder().encode(self) else { return nil } diff --git a/xdrip/Managers/Alerts/AlertUrgencyType.swift b/xdrip/Managers/Alerts/AlertUrgencyType.swift index a96c706af..b0eb93ce1 100644 --- a/xdrip/Managers/Alerts/AlertUrgencyType.swift +++ b/xdrip/Managers/Alerts/AlertUrgencyType.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +import SwiftUI /// define urgency levels so that we can pass this to notifications to display them in a relevant style... public enum AlertUrgencyType: Int { @@ -37,69 +38,71 @@ public enum AlertUrgencyType: Int { } } - var bgValueTextColor: UIColor { + var description: String { switch self { case .urgent: - return UIColor.systemRed + return "Urgent" case .warning: - return UIColor.systemYellow - default: - return UIColor.systemGreen + return "Warning" + case .normal: + return "Normal" } } - - var bannerTextColor: UIColor { + + var alertTitleType: String { switch self { case .urgent: - return UIColor.white.withAlphaComponent(0.85) + return Texts_Alerts.alertTypeUrgent case .warning: - return UIColor.black + return Texts_Alerts.alertTypeWarning default: - return UIColor.white.withAlphaComponent(0.85) + return "" } } - - var bannerBackgroundColor: UIColor { + + // UIKit colors + var bannerTextUIColor: UIColor { switch self { case .urgent: - return UIColor.systemRed + return UIColor.white.withAlphaComponent(0.85) case .warning: - return UIColor.systemYellow + return UIColor.black default: - return UIColor.clear + return UIColor.white.withAlphaComponent(0.85) } } - var alertImageString: String? { + var bannerBackgroundUIColor: UIColor { switch self { case .urgent: - return "exclamationmark.triangle.fill" + return UIColor.red case .warning: - return "exclamationmark.triangle" + return UIColor.yellow default: - return "info.circle" + return ConstantsAlerts.notificationBannerBackgroundUIColor } } - var description: String { + // SwiftUI colors + var bannerTextColor: Color { switch self { case .urgent: - return "Urgent" + return .white.opacity(0.85) case .warning: - return "Warning" - case .normal: - return "Normal" + return .black + default: + return .white.opacity(0.85) } } - var alertTitleType: String { + var bannerBackgroundColor: Color { switch self { case .urgent: - return Texts_Alerts.alertTypeUrgent + return .red case .warning: - return Texts_Alerts.alertTypeWarning + return .yellow default: - return "" + return ConstantsAlerts.notificationBannerBackgroundColor } } } diff --git a/xdrip/Managers/Charts/GlucoseChartType.swift b/xdrip/Managers/Charts/GlucoseChartType.swift index cf5f1da9b..46d7338f1 100644 --- a/xdrip/Managers/Charts/GlucoseChartType.swift +++ b/xdrip/Managers/Charts/GlucoseChartType.swift @@ -23,6 +23,7 @@ public enum GlucoseChartType: Int, CaseIterable { case siriGlucoseIntent = 8 case notificationImageThumbnail = 9 case notificationImageExpanded = 10 + case notificationWatchImage = 11 var description: String { switch self { @@ -48,6 +49,8 @@ public enum GlucoseChartType: Int, CaseIterable { return "Notification Thumbnail Image Chart" case .notificationImageExpanded: return "Notification Expanded Image Chart" + case .notificationWatchImage: + return "Notification Watch Image Chart" } } @@ -82,6 +85,8 @@ public enum GlucoseChartType: Int, CaseIterable { return (ConstantsGlucoseChartSwiftUI.viewWidthNotificationThumbnailImage, ConstantsGlucoseChartSwiftUI.viewHeightNotificationThumbnailImage) case .notificationImageExpanded: return (ConstantsGlucoseChartSwiftUI.viewWidthNotificationExpandedImage, ConstantsGlucoseChartSwiftUI.viewHeightNotificationExpandedImage) + case .notificationWatchImage: + return (ConstantsGlucoseChartSwiftUI.viewWidthNotificationWatchImage, ConstantsGlucoseChartSwiftUI.viewHeightNotificationWatchImage) } } @@ -114,6 +119,8 @@ public enum GlucoseChartType: Int, CaseIterable { return ConstantsGlucoseChartSwiftUI.hoursToShowNotificationThumbnailImage case .notificationImageExpanded: return ConstantsGlucoseChartSwiftUI.hoursToShowNotificationExpandedImage + case .notificationWatchImage: + return ConstantsGlucoseChartSwiftUI.hoursToShowNotificationWatchImage } } @@ -150,6 +157,8 @@ public enum GlucoseChartType: Int, CaseIterable { return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterNotificationThumbnailImage case .notificationImageExpanded: return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterNotificationExpandedImage + case .notificationWatchImage: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterNotificationWatchImage } } @@ -159,6 +168,12 @@ public enum GlucoseChartType: Int, CaseIterable { return ConstantsGlucoseChartSwiftUI.backgroundColorSiriGlucoseIntent case .watchAccessoryRectangular: return ConstantsGlucoseChartSwiftUI.backgroundColorWatchAccessoryRectangular + case .notificationImageExpanded: + // use this value from the alert constants to keep everything the same + return ConstantsAlerts.notificationBackgroundColor + case .notificationWatchImage: + // use this value from the alert constants to keep everything the same + return ConstantsAlerts.notificationBackgroundColor default: return .black } @@ -195,6 +210,8 @@ public enum GlucoseChartType: Int, CaseIterable { switch self { case .siriGlucoseIntent: return (true, ConstantsGlucoseChartSwiftUI.paddingSiriGlucoseIntent) + case .notificationWatchImage: + return (true, ConstantsGlucoseChartSwiftUI.paddingNotificationWatchImage) default: return (false, 0) } @@ -276,6 +293,8 @@ public enum GlucoseChartType: Int, CaseIterable { return ConstantsGlucoseChartSwiftUI.filenameNotificationThumbnailImage case .notificationImageExpanded: return ConstantsGlucoseChartSwiftUI.filenameNotificationExpandedImage + case .notificationWatchImage: + return ConstantsGlucoseChartSwiftUI.filenameNotificationWatchImage default: return "" } diff --git a/xdrip/View Controllers/Root View Controller/RootViewController.swift b/xdrip/View Controllers/Root View Controller/RootViewController.swift index fe7260cfe..0bc37daba 100644 --- a/xdrip/View Controllers/Root View Controller/RootViewController.swift +++ b/xdrip/View Controllers/Root View Controller/RootViewController.swift @@ -3608,6 +3608,9 @@ final class RootViewController: UIViewController, ObservableObject { // create a bigger, full glucose chart image to display in the Notification Content Extension view createNotificationImage(glucoseChartType: .notificationImageExpanded) + // create a smaller glucose chart image to display in the Watch notification + createNotificationImage(glucoseChartType: .notificationWatchImage) + /// create an image based upon a glucose chart view and save it to the app container documents directory /// - Parameter glucoseChartType: the type of glucose chart type we want to generate (i.e. thumbnail or full notification chart) func createNotificationImage(glucoseChartType: GlucoseChartType) { From ff85225bac5a714a095072f094b6a16f091c301a Mon Sep 17 00:00:00 2001 From: Paul Plant <37302780+paulplant@users.noreply.github.com> Date: Sun, 2 Jun 2024 10:43:46 +0200 Subject: [PATCH 04/23] add StandBy mode to the .systemSmall iOS widget Will detect if the .systemSmall widget is being used in the StandBy mode and provide a simplified view with bigger text etc. If the current time is between 22hrs and 08hrs, then we will *also* force a high contrast view so that it renders nicely in the StandBy Night Mode. This can be disabled in the developer settings. It is enabled by default. --- xDrip Watch App/Views/MainView.swift | 2 +- .../Views/AccessoryRectangularView.swift | 2 +- .../Constants/ConstantsWidgetExtension.swift | 6 ++ xDrip Widget/Views/SystemLargeView.swift | 2 +- xDrip Widget/Views/SystemMediumView.swift | 2 +- xDrip Widget/Views/SystemSmallView.swift | 91 ++++++++++++------- xDrip Widget/XDripWidget+Entry.swift | 4 +- xDrip Widget/XDripWidget+EntryView.swift | 15 ++- xDrip Widget/XDripWidgetLiveActivity.swift | 6 +- xdrip.xcodeproj/project.pbxproj | 4 +- .../ConstantsGlucoseChartSwiftUI.swift | 15 ++- xdrip/Extensions/UserDefaults.swift | 13 +++ xdrip/GlucoseIntent.swift | 2 +- xdrip/Managers/Charts/GlucoseChartType.swift | 50 ++++++++-- .../WidgetSharedUserDefaultsModel.swift | 1 + xdrip/SwiftUIViews/GlucoseChartView.swift | 29 +++--- xdrip/Texts/TextsSettingsView.swift | 4 + .../RootViewController.swift | 4 +- ...ingsViewDevelopmentSettingsViewModel.swift | 20 +++- 19 files changed, 202 insertions(+), 70 deletions(-) diff --git a/xDrip Watch App/Views/MainView.swift b/xDrip Watch App/Views/MainView.swift index 36328d7b1..834594000 100644 --- a/xDrip Watch App/Views/MainView.swift +++ b/xDrip Watch App/Views/MainView.swift @@ -38,7 +38,7 @@ struct MainView: View { watchState.requestWatchStateUpdate() } - GlucoseChartView(glucoseChartType: .watchApp, bgReadingValues: watchState.bgReadingValues, bgReadingDates: watchState.bgReadingDates, isMgDl: watchState.isMgDl, urgentLowLimitInMgDl: watchState.urgentLowLimitInMgDl, lowLimitInMgDl: watchState.lowLimitInMgDl, highLimitInMgDl: watchState.highLimitInMgDl, urgentHighLimitInMgDl: watchState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: hoursToShow[hoursToShowIndex], glucoseCircleDiameterScalingHours: 4, overrideChartHeight: overrideChartHeight, overrideChartWidth: overrideChartWidth) + GlucoseChartView(glucoseChartType: .watchApp, bgReadingValues: watchState.bgReadingValues, bgReadingDates: watchState.bgReadingDates, isMgDl: watchState.isMgDl, urgentLowLimitInMgDl: watchState.urgentLowLimitInMgDl, lowLimitInMgDl: watchState.lowLimitInMgDl, highLimitInMgDl: watchState.highLimitInMgDl, urgentHighLimitInMgDl: watchState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: hoursToShow[hoursToShowIndex], glucoseCircleDiameterScalingHours: 4, overrideChartHeight: overrideChartHeight, overrideChartWidth: overrideChartWidth, highContrast: nil) .gesture( DragGesture(minimumDistance: 80, coordinateSpace: .local) .onEnded({ value in diff --git a/xDrip Watch Complication/Views/AccessoryRectangularView.swift b/xDrip Watch Complication/Views/AccessoryRectangularView.swift index 7b770b1ac..1505902df 100644 --- a/xDrip Watch Complication/Views/AccessoryRectangularView.swift +++ b/xDrip Watch Complication/Views/AccessoryRectangularView.swift @@ -37,7 +37,7 @@ extension XDripWatchComplication.EntryView { } .padding(0) - GlucoseChartView(glucoseChartType: .watchAccessoryRectangular, bgReadingValues: entry.widgetState.bgReadingValues, bgReadingDates: entry.widgetState.bgReadingDates, isMgDl: entry.widgetState.isMgDl, urgentLowLimitInMgDl: entry.widgetState.urgentLowLimitInMgDl, lowLimitInMgDl: entry.widgetState.lowLimitInMgDl, highLimitInMgDl: entry.widgetState.highLimitInMgDl, urgentHighLimitInMgDl: entry.widgetState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: entry.widgetState.overrideChartHeight(), overrideChartWidth: entry.widgetState.overrideChartWidth()) + GlucoseChartView(glucoseChartType: .watchAccessoryRectangular, bgReadingValues: entry.widgetState.bgReadingValues, bgReadingDates: entry.widgetState.bgReadingDates, isMgDl: entry.widgetState.isMgDl, urgentLowLimitInMgDl: entry.widgetState.urgentLowLimitInMgDl, lowLimitInMgDl: entry.widgetState.lowLimitInMgDl, highLimitInMgDl: entry.widgetState.highLimitInMgDl, urgentHighLimitInMgDl: entry.widgetState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: entry.widgetState.overrideChartHeight(), overrideChartWidth: entry.widgetState.overrideChartWidth(), highContrast: nil) if entry.widgetState.keepAliveIsDisabled { HStack(alignment: .center, spacing: 4) { diff --git a/xDrip Widget/Constants/ConstantsWidgetExtension.swift b/xDrip Widget/Constants/ConstantsWidgetExtension.swift index 40b5f5456..2c6a7bda1 100644 --- a/xDrip Widget/Constants/ConstantsWidgetExtension.swift +++ b/xDrip Widget/Constants/ConstantsWidgetExtension.swift @@ -15,6 +15,12 @@ enum ConstantsWidgetExtension { /// the time in minutes until the last BG value is considered too old to show static let bgReadingDateVeryStaleInMinutes = 20.0 * 60 + /// from what time we will consider that the widget is being displayed at night + static let nightModeFromHour: Int = 22 // 22:00hrs / 10pm + + /// until what time we will consider that the widget is being displayed at night + static let nightModeUntilHour: Int = 8 // 082:00hrs / 8am + /// an array of placeholder BG values static let bgReadingValuesPlaceholderData: [Double] = [109, 109, 109, 110, 110, 111, 110, 112, 114, 114, 118, 118, 121, 123, 126, 128, 130, 133, 137, 139, 142, 144, 146, 146, 142, 138, 135, 131, 128, 126, 124, 122, 121, 120, 120, 118, 116, 112, 109, 106, 103, 101, 98, 97, 97, 97, 96, 96, 97, 96, 92, 89, 85, 78, 70, 65, 62, 63, 67, 72, 77, 81, 86, 88, 90, 92, 94, 95, 96, 99, 101, 102, 104, 106, 108, 110, 112, 114, 116, 116, 117, 120, 120, 121, 121, 120, 118, 115, 111, 108, 105, 103, 101, 101, 102, 106, 107, 109, 112, 114, 115, 117, 119, 121, 120, 119, 117, 115, 114, 115, 116, 117, 118, 118, 119, 122, 123, 125, 125, 123, 119, 110, 103, 99, 102, 101, 101, 101, 101, 101, 137, 140, 136, 141, 153, 154, 148, 147, 148, 142, 132, 128, 132, 134] diff --git a/xDrip Widget/Views/SystemLargeView.swift b/xDrip Widget/Views/SystemLargeView.swift index ee9d65044..d713b6aad 100644 --- a/xDrip Widget/Views/SystemLargeView.swift +++ b/xDrip Widget/Views/SystemLargeView.swift @@ -35,7 +35,7 @@ extension XDripWidget.EntryView { } .padding(.bottom, 6) - GlucoseChartView(glucoseChartType: .widgetSystemLarge, bgReadingValues: entry.widgetState.bgReadingValues, bgReadingDates: entry.widgetState.bgReadingDates, isMgDl: entry.widgetState.isMgDl, urgentLowLimitInMgDl: entry.widgetState.urgentLowLimitInMgDl, lowLimitInMgDl: entry.widgetState.lowLimitInMgDl, highLimitInMgDl: entry.widgetState.highLimitInMgDl, urgentHighLimitInMgDl: entry.widgetState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil) + GlucoseChartView(glucoseChartType: .widgetSystemLarge, bgReadingValues: entry.widgetState.bgReadingValues, bgReadingDates: entry.widgetState.bgReadingDates, isMgDl: entry.widgetState.isMgDl, urgentLowLimitInMgDl: entry.widgetState.urgentLowLimitInMgDl, lowLimitInMgDl: entry.widgetState.lowLimitInMgDl, highLimitInMgDl: entry.widgetState.highLimitInMgDl, urgentHighLimitInMgDl: entry.widgetState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil, highContrast: nil) HStack(alignment: .center) { if let keepAliveImageString = entry.widgetState.keepAliveImageString { diff --git a/xDrip Widget/Views/SystemMediumView.swift b/xDrip Widget/Views/SystemMediumView.swift index d993bfe27..cf6cd7f0a 100644 --- a/xDrip Widget/Views/SystemMediumView.swift +++ b/xDrip Widget/Views/SystemMediumView.swift @@ -36,7 +36,7 @@ extension XDripWidget.EntryView { .padding(.top, -6) .padding(.bottom, 6) - GlucoseChartView(glucoseChartType: .widgetSystemMedium, bgReadingValues: entry.widgetState.bgReadingValues, bgReadingDates: entry.widgetState.bgReadingDates, isMgDl: entry.widgetState.isMgDl, urgentLowLimitInMgDl: entry.widgetState.urgentLowLimitInMgDl, lowLimitInMgDl: entry.widgetState.lowLimitInMgDl, highLimitInMgDl: entry.widgetState.highLimitInMgDl, urgentHighLimitInMgDl: entry.widgetState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil) + GlucoseChartView(glucoseChartType: .widgetSystemMedium, bgReadingValues: entry.widgetState.bgReadingValues, bgReadingDates: entry.widgetState.bgReadingDates, isMgDl: entry.widgetState.isMgDl, urgentLowLimitInMgDl: entry.widgetState.urgentLowLimitInMgDl, lowLimitInMgDl: entry.widgetState.lowLimitInMgDl, highLimitInMgDl: entry.widgetState.highLimitInMgDl, urgentHighLimitInMgDl: entry.widgetState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil, highContrast: nil) HStack(alignment: .center) { if let keepAliveImageString = entry.widgetState.keepAliveImageString { diff --git a/xDrip Widget/Views/SystemSmallView.swift b/xDrip Widget/Views/SystemSmallView.swift index 6c7f7ee53..8f7012b83 100644 --- a/xDrip Widget/Views/SystemSmallView.swift +++ b/xDrip Widget/Views/SystemSmallView.swift @@ -12,45 +12,74 @@ import SwiftUI extension XDripWidget.EntryView { var systemSmallView: some View { VStack(spacing: 0) { - HStack(alignment: .center) { - Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow())") - .font(.title).fontWeight(.bold) - .foregroundStyle(entry.widgetState.bgTextColor()) - .minimumScaleFactor(0.5) - .lineLimit(1) + if isNotBeingUsedInStandByMode { - Spacer() + // this is the standard widget view + HStack(alignment: .center) { + Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow())") + .font(.title).fontWeight(.bold) + .foregroundStyle(entry.widgetState.bgTextColor()) + .minimumScaleFactor(0.5) + .lineLimit(1) + + Spacer() + + Text(entry.widgetState.deltaChangeStringInUserChosenUnit()) + .font(.title).fontWeight(.semibold) + .foregroundStyle(entry.widgetState.deltaChangeTextColor()) + .minimumScaleFactor(0.5) + .lineLimit(1) + } + .padding(.top, -6) + .padding(.bottom, 6) - Text(entry.widgetState.deltaChangeStringInUserChosenUnit()) - .font(.title).fontWeight(.semibold) - .foregroundStyle(entry.widgetState.deltaChangeTextColor()) - .minimumScaleFactor(0.5) - .lineLimit(1) - } - .padding(.top, -6) - .padding(.bottom, 6) - - GlucoseChartView(glucoseChartType: .widgetSystemSmall, bgReadingValues: entry.widgetState.bgReadingValues, bgReadingDates: entry.widgetState.bgReadingDates, isMgDl: entry.widgetState.isMgDl, urgentLowLimitInMgDl: entry.widgetState.urgentLowLimitInMgDl, lowLimitInMgDl: entry.widgetState.lowLimitInMgDl, highLimitInMgDl: entry.widgetState.highLimitInMgDl, urgentHighLimitInMgDl: entry.widgetState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil) - - HStack(alignment: .center) { - if let keepAliveImageString = entry.widgetState.keepAliveImageString { - Image(systemName: keepAliveImageString) + GlucoseChartView(glucoseChartType: .widgetSystemSmall, bgReadingValues: entry.widgetState.bgReadingValues, bgReadingDates: entry.widgetState.bgReadingDates, isMgDl: entry.widgetState.isMgDl, urgentLowLimitInMgDl: entry.widgetState.urgentLowLimitInMgDl, lowLimitInMgDl: entry.widgetState.lowLimitInMgDl, highLimitInMgDl: entry.widgetState.highLimitInMgDl, urgentHighLimitInMgDl: entry.widgetState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil, highContrast: nil) + + HStack(alignment: .center) { + if let keepAliveImageString = entry.widgetState.keepAliveImageString { + Image(systemName: keepAliveImageString) + .font(.caption) + .foregroundStyle(.colorTertiary) + .padding(.trailing, -4) + } + + Text(entry.widgetState.dataSourceDescription) + .font(.caption).bold() + .foregroundStyle(.colorSecondary) + + Spacer() + + Text("\(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")") .font(.caption) .foregroundStyle(.colorTertiary) - .padding(.trailing, -4) } - - Text(entry.widgetState.dataSourceDescription) - .font(.caption).bold() - .foregroundStyle(.colorSecondary) + .padding(.top, 6) + + } else { - Spacer() + // this is the simpler widget view to be used in StandBy mode - it has bigger fonts and less info + // if the time is at night, then we'll force a high contrast view which will render + // nicely in red with the StandBy Night Mode + HStack(alignment: .center) { + Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow())") + .font(.largeTitle).fontWeight(.bold) + .foregroundStyle(isAtNight() ? .white : entry.widgetState.bgTextColor()) + .minimumScaleFactor(0.5) + .lineLimit(1) + + Spacer() + + Text(entry.widgetState.deltaChangeStringInUserChosenUnit()) + .font(.title).fontWeight(.bold) + .foregroundStyle(isAtNight() ? .white : entry.widgetState.deltaChangeTextColor()) + .minimumScaleFactor(0.5) + .lineLimit(1) + } + .padding(.top, 0) + .padding(.bottom, 2) - Text("\(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")") - .font(.caption) - .foregroundStyle(.colorTertiary) + GlucoseChartView(glucoseChartType: .widgetSystemSmallStandBy, bgReadingValues: entry.widgetState.bgReadingValues, bgReadingDates: entry.widgetState.bgReadingDates, isMgDl: entry.widgetState.isMgDl, urgentLowLimitInMgDl: entry.widgetState.urgentLowLimitInMgDl, lowLimitInMgDl: entry.widgetState.lowLimitInMgDl, highLimitInMgDl: entry.widgetState.highLimitInMgDl, urgentHighLimitInMgDl: entry.widgetState.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil, highContrast: isAtNight()) } - .padding(.top, 6) } .widgetBackground(backgroundView: Color.black) } diff --git a/xDrip Widget/XDripWidget+Entry.swift b/xDrip Widget/XDripWidget+Entry.swift index 0fc878495..f966afec9 100644 --- a/xDrip Widget/XDripWidget+Entry.swift +++ b/xDrip Widget/XDripWidget+Entry.swift @@ -34,6 +34,7 @@ extension XDripWidget.Entry { var highLimitInMgDl: Double var urgentHighLimitInMgDl: Double var dataSourceDescription: String + var allowStandByHighContrast: Bool var keepAliveImageString: String? var bgUnitString: String @@ -41,7 +42,7 @@ extension XDripWidget.Entry { var bgReadingDate: Date? var bgValueStringInUserChosenUnit: String - init(bgReadingValues: [Double]? = nil, bgReadingDates: [Date]? = nil, isMgDl: Bool? = true, slopeOrdinal: Int? = 0, deltaChangeInMgDl: Double? = nil, urgentLowLimitInMgDl: Double? = 60, lowLimitInMgDl: Double? = 80, highLimitInMgDl: Double? = 180, urgentHighLimitInMgDl: Double? = 250, dataSourceDescription: String? = "", keepAliveImageString: String?) { + init(bgReadingValues: [Double]? = nil, bgReadingDates: [Date]? = nil, isMgDl: Bool? = true, slopeOrdinal: Int? = 0, deltaChangeInMgDl: Double? = nil, urgentLowLimitInMgDl: Double? = 60, lowLimitInMgDl: Double? = 80, highLimitInMgDl: Double? = 180, urgentHighLimitInMgDl: Double? = 250, dataSourceDescription: String? = "", allowStandByHighContrast: Bool? = true, keepAliveImageString: String?) { self.bgReadingValues = bgReadingValues self.bgReadingDates = bgReadingDates self.isMgDl = isMgDl ?? true @@ -52,6 +53,7 @@ extension XDripWidget.Entry { self.highLimitInMgDl = highLimitInMgDl ?? 180 self.urgentHighLimitInMgDl = urgentHighLimitInMgDl ?? 250 self.dataSourceDescription = dataSourceDescription ?? "" + self.allowStandByHighContrast = allowStandByHighContrast ?? true self.keepAliveImageString = keepAliveImageString diff --git a/xDrip Widget/XDripWidget+EntryView.swift b/xDrip Widget/XDripWidget+EntryView.swift index 614f17242..258f881e8 100644 --- a/xDrip Widget/XDripWidget+EntryView.swift +++ b/xDrip Widget/XDripWidget+EntryView.swift @@ -15,7 +15,20 @@ extension XDripWidget { // get the widget's family so that we can show the correct view @Environment(\.widgetFamily) private var widgetFamily - @Environment(\.colorScheme) var colorScheme + // check if the widget container background has been removed by iOS + // this allows us to check if the widget is being displayed in StandBy mode + @Environment(\.showsWidgetContainerBackground) var isNotBeingUsedInStandByMode + + // check if we should consider that we're during the night and use this + // to display the widget in high-contrast mode + // will only be used in conjunction with isNotInStandByMode + func isAtNight() -> Bool { + if let currentHour = Calendar.current.dateComponents([.hour], from: Date()).hour, currentHour > ConstantsWidgetExtension.nightModeFromHour || currentHour < ConstantsWidgetExtension.nightModeUntilHour { + return entry.widgetState.allowStandByHighContrast + } else { + return false + } + } var entry: Entry diff --git a/xDrip Widget/XDripWidgetLiveActivity.swift b/xDrip Widget/XDripWidgetLiveActivity.swift index 566d967c2..aa04897e0 100644 --- a/xDrip Widget/XDripWidgetLiveActivity.swift +++ b/xDrip Widget/XDripWidgetLiveActivity.swift @@ -84,7 +84,7 @@ struct XDripWidgetLiveActivity: Widget { } ZStack { - GlucoseChartView(glucoseChartType: .liveActivity, bgReadingValues: context.state.bgReadingValues, bgReadingDates: context.state.bgReadingDates, isMgDl: context.state.isMgDl, urgentLowLimitInMgDl: context.state.urgentLowLimitInMgDl, lowLimitInMgDl: context.state.lowLimitInMgDl, highLimitInMgDl: context.state.highLimitInMgDl, urgentHighLimitInMgDl: context.state.urgentHighLimitInMgDl, liveActivitySize: .normal, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil) + GlucoseChartView(glucoseChartType: .liveActivity, bgReadingValues: context.state.bgReadingValues, bgReadingDates: context.state.bgReadingDates, isMgDl: context.state.isMgDl, urgentLowLimitInMgDl: context.state.urgentLowLimitInMgDl, lowLimitInMgDl: context.state.lowLimitInMgDl, highLimitInMgDl: context.state.highLimitInMgDl, urgentHighLimitInMgDl: context.state.urgentHighLimitInMgDl, liveActivitySize: .normal, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil, highContrast: nil) if context.state.warnUserToOpenApp { VStack(alignment: .center) { @@ -138,7 +138,7 @@ struct XDripWidgetLiveActivity: Widget { .padding(.leading, 15) .padding(.trailing, 15) - GlucoseChartView(glucoseChartType: .liveActivity, bgReadingValues: context.state.bgReadingValues, bgReadingDates: context.state.bgReadingDates, isMgDl: context.state.isMgDl, urgentLowLimitInMgDl: context.state.urgentLowLimitInMgDl, lowLimitInMgDl: context.state.lowLimitInMgDl, highLimitInMgDl: context.state.highLimitInMgDl, urgentHighLimitInMgDl: context.state.urgentHighLimitInMgDl, liveActivitySize: .large, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil) + GlucoseChartView(glucoseChartType: .liveActivity, bgReadingValues: context.state.bgReadingValues, bgReadingDates: context.state.bgReadingDates, isMgDl: context.state.isMgDl, urgentLowLimitInMgDl: context.state.urgentLowLimitInMgDl, lowLimitInMgDl: context.state.lowLimitInMgDl, highLimitInMgDl: context.state.highLimitInMgDl, urgentHighLimitInMgDl: context.state.urgentHighLimitInMgDl, liveActivitySize: .large, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil, highContrast: nil) HStack { Text(context.state.dataSourceDescription) @@ -200,7 +200,7 @@ struct XDripWidgetLiveActivity: Widget { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) } DynamicIslandExpandedRegion(.bottom) { - GlucoseChartView(glucoseChartType: .dynamicIsland, bgReadingValues: context.state.bgReadingValues, bgReadingDates: context.state.bgReadingDates, isMgDl: context.state.isMgDl, urgentLowLimitInMgDl: context.state.urgentLowLimitInMgDl, lowLimitInMgDl: context.state.lowLimitInMgDl, highLimitInMgDl: context.state.highLimitInMgDl, urgentHighLimitInMgDl: context.state.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil) + GlucoseChartView(glucoseChartType: .dynamicIsland, bgReadingValues: context.state.bgReadingValues, bgReadingDates: context.state.bgReadingDates, isMgDl: context.state.isMgDl, urgentLowLimitInMgDl: context.state.urgentLowLimitInMgDl, lowLimitInMgDl: context.state.lowLimitInMgDl, highLimitInMgDl: context.state.highLimitInMgDl, urgentHighLimitInMgDl: context.state.urgentHighLimitInMgDl, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil, highContrast: nil) } } compactLeading: { Text("\(context.state.bgValueStringInUserChosenUnit)\(context.state.trendArrow())") diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index 05c93aec7..0fa63ed62 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -1969,11 +1969,11 @@ 474606782B962F3300AC9214 /* Views */ = { isa = PBXGroup; children = ( + 47CA61E32B965E7100C2A597 /* AccessoryCircularView.swift */, + 47CA61E52B966A9700C2A597 /* AccessoryRectangularView.swift */, 4746067D2B962FBD00AC9214 /* SystemLargeView.swift */, 4746067B2B962F8500AC9214 /* SystemMediumView.swift */, 474606792B962F4C00AC9214 /* SystemSmallView.swift */, - 47CA61E32B965E7100C2A597 /* AccessoryCircularView.swift */, - 47CA61E52B966A9700C2A597 /* AccessoryRectangularView.swift */, ); path = Views; sourceTree = ""; diff --git a/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift b/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift index 2dda4eb09..44b02322d 100644 --- a/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift +++ b/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift @@ -21,7 +21,7 @@ enum ConstantsGlucoseChartSwiftUI { static let yAxisLowHighLineColor = Color(white: 0.7) static let yAxisUrgentLowHighLineColor = Color(white: 0.6) - static let xAxisGridLineColor = Color(white: 0.45) + static let xAxisGridLineColor = Color(white: 0.4) static let xAxisLabelOffsetX: CGFloat = -12 static let xAxisLabelOffsetY: CGFloat = -2 static let xAxisIntervalBetweenValues: Int = 1 @@ -92,6 +92,15 @@ enum ConstantsGlucoseChartSwiftUI { static let hoursToShowWidgetSystemSmall: Double = 3 static let glucoseCircleDiameterWidgetSystemSmall: Double = 20 + // widget systemSmall StandBy chart + static let viewWidthWidgetSystemSmallStandBy: CGFloat = 140 + static let viewHeightWidgetSystemSmallStandBy: CGFloat = 100 + static let hoursToShowWidgetSystemSmallStandBy: Double = 43 + static let glucoseCircleDiameterWidgetSystemSmallStandBy: Double = 20 + static let yAxisLineSizeSystemSmallStandBy: Double = 1.0 + static let yAxisLowHighLineColorSystemSmallStandBy = Color(white: 1.0) + static let yAxisUrgentLowHighLineColorSystemSmallStandBy = Color(white: 0.8) + // widget systemMedium chart static let viewWidthWidgetSystemMedium: CGFloat = 300 static let viewHeightWidgetSystemMedium: CGFloat = 80 @@ -142,8 +151,8 @@ enum ConstantsGlucoseChartSwiftUI { // watch notification image static let viewWidthNotificationWatchImage: CGFloat = 170 - static let viewHeightNotificationWatchImage: CGFloat = 80 - static let hoursToShowNotificationWatchImage: Double = 2 + static let viewHeightNotificationWatchImage: CGFloat = 60 + static let hoursToShowNotificationWatchImage: Double = 3 static let glucoseCircleDiameterNotificationWatchImage: Double = 30 static let paddingNotificationWatchImage: Double = 2 static let filenameNotificationWatchImage: String = "notificationWatchImage" diff --git a/xdrip/Extensions/UserDefaults.swift b/xdrip/Extensions/UserDefaults.swift index 3cab48ecc..c76456ac6 100644 --- a/xdrip/Extensions/UserDefaults.swift +++ b/xdrip/Extensions/UserDefaults.swift @@ -432,6 +432,8 @@ extension UserDefaults { /// used by the observer in RVC to update the UI for the snooze status case updateSnoozeStatus = "updateSnoozeStatus" + /// should the app allow a high contrast mode for the .systemSmall widget when shown in StandBy mode at night? + case allowStandByHighContrast = "allowStandByHighContrast" } @@ -2108,6 +2110,17 @@ extension UserDefaults { } } + /// should the app allow a high contrast mode for the .systemSmall widget when shown in StandBy mode at night? + var allowStandByHighContrast: Bool { + // default value for bool in userdefaults is false, as default we want the app to allow high contrast for StandBy as needed + get { + return !bool(forKey: Key.allowStandByHighContrast.rawValue) + } + set { + set(!newValue, forKey: Key.allowStandByHighContrast.rawValue) + } + } + // MARK: - ===== technical settings for testing ====== diff --git a/xdrip/GlucoseIntent.swift b/xdrip/GlucoseIntent.swift index 08b07d035..cbee863b8 100644 --- a/xdrip/GlucoseIntent.swift +++ b/xdrip/GlucoseIntent.swift @@ -72,7 +72,7 @@ struct GlucoseIntent: AppIntent { return .result( value: value, dialog: dialogString, - view: GlucoseChartView(glucoseChartType: .siriGlucoseIntent, bgReadingValues: bgReadingValues, bgReadingDates: bgReadingDates, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil) + view: GlucoseChartView(glucoseChartType: .siriGlucoseIntent, bgReadingValues: bgReadingValues, bgReadingDates: bgReadingDates, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, liveActivitySize: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil, highContrast: nil) ) } } diff --git a/xdrip/Managers/Charts/GlucoseChartType.swift b/xdrip/Managers/Charts/GlucoseChartType.swift index 46d7338f1..e2af11096 100644 --- a/xdrip/Managers/Charts/GlucoseChartType.swift +++ b/xdrip/Managers/Charts/GlucoseChartType.swift @@ -17,13 +17,14 @@ public enum GlucoseChartType: Int, CaseIterable { case watchApp = 2 case watchAccessoryRectangular = 3 case widgetSystemSmall = 4 - case widgetSystemMedium = 5 - case widgetSystemLarge = 6 - case widgetAccessoryRectangular = 7 - case siriGlucoseIntent = 8 - case notificationImageThumbnail = 9 - case notificationImageExpanded = 10 - case notificationWatchImage = 11 + case widgetSystemSmallStandBy = 5 + case widgetSystemMedium = 6 + case widgetSystemLarge = 7 + case widgetAccessoryRectangular = 8 + case siriGlucoseIntent = 9 + case notificationImageThumbnail = 10 + case notificationImageExpanded = 11 + case notificationWatchImage = 12 var description: String { switch self { @@ -37,6 +38,8 @@ public enum GlucoseChartType: Int, CaseIterable { return "Watch Chart .accessoryRectangular" case .widgetSystemSmall: return "Widget Chart .systemSmall" + case .widgetSystemSmallStandBy: + return "Widget Chart .systemSmall for StandBy mode" case .widgetSystemMedium: return "Widget Chart .systemMedium" case .widgetSystemLarge: @@ -73,6 +76,8 @@ public enum GlucoseChartType: Int, CaseIterable { return (ConstantsGlucoseChartSwiftUI.viewWidthWatchAccessoryRectangular, ConstantsGlucoseChartSwiftUI.viewHeightWatchAccessoryRectangular) case .widgetSystemSmall: return (ConstantsGlucoseChartSwiftUI.viewWidthWidgetSystemSmall, ConstantsGlucoseChartSwiftUI.viewHeightWidgetSystemSmall) + case .widgetSystemSmallStandBy: + return (ConstantsGlucoseChartSwiftUI.viewWidthWidgetSystemSmallStandBy, ConstantsGlucoseChartSwiftUI.viewHeightWidgetSystemSmallStandBy) case .widgetSystemMedium: return (ConstantsGlucoseChartSwiftUI.viewWidthWidgetSystemMedium, ConstantsGlucoseChartSwiftUI.viewHeightWidgetSystemMedium) case .widgetSystemLarge: @@ -105,7 +110,7 @@ public enum GlucoseChartType: Int, CaseIterable { return ConstantsGlucoseChartSwiftUI.hoursToShowWatchApp case .watchAccessoryRectangular: return ConstantsGlucoseChartSwiftUI.hoursToShowWatchAccessoryRectangular - case .widgetSystemSmall: + case .widgetSystemSmall, .widgetSystemSmallStandBy: return ConstantsGlucoseChartSwiftUI.hoursToShowWidgetSystemSmall case .widgetSystemMedium: return ConstantsGlucoseChartSwiftUI.hoursToShowWidgetSystemMedium @@ -145,6 +150,8 @@ public enum GlucoseChartType: Int, CaseIterable { return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWatchAccessoryRectangular case .widgetSystemSmall: return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWidgetSystemSmall + case .widgetSystemSmallStandBy: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWidgetSystemSmallStandBy case .widgetSystemMedium: return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWidgetSystemMedium case .widgetSystemLarge: @@ -284,6 +291,33 @@ public enum GlucoseChartType: Int, CaseIterable { } } + func yAxisLineSize() -> Double { + switch self { + case .widgetSystemSmallStandBy: + return ConstantsGlucoseChartSwiftUI.yAxisLineSizeSystemSmallStandBy + default: + return ConstantsGlucoseChartSwiftUI.yAxisLineSize + } + } + + func yAxisLowHighLineColor() -> Color { + switch self { + case .widgetSystemSmallStandBy: + return ConstantsGlucoseChartSwiftUI.yAxisLowHighLineColorSystemSmallStandBy + default: + return ConstantsGlucoseChartSwiftUI.yAxisLowHighLineColor + } + } + + func yAxisUrgentLowHighLineColor() -> Color { + switch self { + case .widgetSystemSmallStandBy: + return ConstantsGlucoseChartSwiftUI.yAxisUrgentLowHighLineColorSystemSmallStandBy + default: + return ConstantsGlucoseChartSwiftUI.yAxisUrgentLowHighLineColor + } + } + // MARK: - filename properties if generating an image of the chart/view diff --git a/xdrip/Managers/Widgets/WidgetSharedUserDefaultsModel.swift b/xdrip/Managers/Widgets/WidgetSharedUserDefaultsModel.swift index f05022db7..4bc039a2d 100644 --- a/xdrip/Managers/Widgets/WidgetSharedUserDefaultsModel.swift +++ b/xdrip/Managers/Widgets/WidgetSharedUserDefaultsModel.swift @@ -20,5 +20,6 @@ struct WidgetSharedUserDefaultsModel: Codable { var highLimitInMgDl: Double var urgentHighLimitInMgDl: Double var dataSourceDescription: String + var allowStandByHighContrast: Bool var keepAliveImageString: String? } diff --git a/xdrip/SwiftUIViews/GlucoseChartView.swift b/xdrip/SwiftUIViews/GlucoseChartView.swift index 78cd1d76b..9cdca1361 100644 --- a/xdrip/SwiftUIViews/GlucoseChartView.swift +++ b/xdrip/SwiftUIViews/GlucoseChartView.swift @@ -26,8 +26,9 @@ struct GlucoseChartView: View { let glucoseCircleDiameter: Double let chartHeight: Double let chartWidth: Double + let showHighContrast: Bool - init(glucoseChartType: GlucoseChartType, bgReadingValues: [Double]?, bgReadingDates: [Date]?, isMgDl: Bool, urgentLowLimitInMgDl: Double, lowLimitInMgDl: Double, highLimitInMgDl: Double, urgentHighLimitInMgDl: Double, liveActivitySize: LiveActivitySize?, hoursToShowScalingHours: Double?, glucoseCircleDiameterScalingHours: Double?, overrideChartHeight: Double?, overrideChartWidth: Double?) { + init(glucoseChartType: GlucoseChartType, bgReadingValues: [Double]?, bgReadingDates: [Date]?, isMgDl: Bool, urgentLowLimitInMgDl: Double, lowLimitInMgDl: Double, highLimitInMgDl: Double, urgentHighLimitInMgDl: Double, liveActivitySize: LiveActivitySize?, hoursToShowScalingHours: Double?, glucoseCircleDiameterScalingHours: Double?, overrideChartHeight: Double?, overrideChartWidth: Double?, highContrast: Bool?) { self.chartType = glucoseChartType self.isMgDl = isMgDl @@ -36,6 +37,7 @@ struct GlucoseChartView: View { self.highLimitInMgDl = highLimitInMgDl self.urgentHighLimitInMgDl = urgentHighLimitInMgDl self.liveActivitySize = liveActivitySize ?? .normal + self.showHighContrast = highContrast ?? false // here we want to automatically set the hoursToShow based upon the chart type, but some chart instances might need // this to be overriden such as for zooming in/out of the chart (i.e. the Watch App) @@ -67,13 +69,18 @@ struct GlucoseChartView: View { /// Blood glucose color dependant on the user defined limit values /// - Returns: a Color object either red, yellow or green func bgColor(bgValueInMgDl: Double) -> Color { - if bgValueInMgDl >= urgentHighLimitInMgDl || bgValueInMgDl <= urgentLowLimitInMgDl { - return .red - } else if bgValueInMgDl >= highLimitInMgDl || bgValueInMgDl <= lowLimitInMgDl { - return .yellow + if chartType != .widgetSystemSmallStandBy || !showHighContrast { + if bgValueInMgDl >= urgentHighLimitInMgDl || bgValueInMgDl <= urgentLowLimitInMgDl { + return .red + } else if bgValueInMgDl >= highLimitInMgDl || bgValueInMgDl <= lowLimitInMgDl { + return .yellow + } else { + return .green + } } else { - return .green + return .white } + } // adapted from generateXAxisValues() from GlucoseChartManager.swift in xDrip target @@ -106,31 +113,31 @@ struct GlucoseChartView: View { var body: some View { let domain = (min((bgReadingValues.min() ?? 40), urgentLowLimitInMgDl) - 6) ... (max((bgReadingValues.max() ?? urgentHighLimitInMgDl), urgentHighLimitInMgDl) + 6) - let yAxisLineSize = ConstantsGlucoseChartSwiftUI.yAxisLineSize + let yAxisLineSize = chartType.yAxisLineSize() Chart { if domain.contains(urgentLowLimitInMgDl) { RuleMark(y: .value("", urgentLowLimitInMgDl)) .lineStyle(StrokeStyle(lineWidth: yAxisLineSize, dash: [2 * yAxisLineSize, 6 * yAxisLineSize])) - .foregroundStyle(ConstantsGlucoseChartSwiftUI.yAxisUrgentLowHighLineColor) + .foregroundStyle(chartType.yAxisUrgentLowHighLineColor()) } if domain.contains(urgentHighLimitInMgDl) { RuleMark(y: .value("", urgentHighLimitInMgDl)) .lineStyle(StrokeStyle(lineWidth: yAxisLineSize, dash: [2 * yAxisLineSize, 6 * yAxisLineSize])) - .foregroundStyle(ConstantsGlucoseChartSwiftUI.yAxisUrgentLowHighLineColor) + .foregroundStyle(chartType.yAxisUrgentLowHighLineColor()) } if domain.contains(lowLimitInMgDl) { RuleMark(y: .value("", lowLimitInMgDl)) .lineStyle(StrokeStyle(lineWidth: yAxisLineSize, dash: [4 * yAxisLineSize, 3 * yAxisLineSize])) - .foregroundStyle(ConstantsGlucoseChartSwiftUI.yAxisLowHighLineColor) + .foregroundStyle(chartType.yAxisLowHighLineColor()) } if domain.contains(highLimitInMgDl) { RuleMark(y: .value("", highLimitInMgDl)) .lineStyle(StrokeStyle(lineWidth: yAxisLineSize, dash: [4 * yAxisLineSize, 3 * yAxisLineSize])) - .foregroundStyle(ConstantsGlucoseChartSwiftUI.yAxisLowHighLineColor) + .foregroundStyle(chartType.yAxisLowHighLineColor()) } // add a phantom glucose point at the beginning of the timeline to fix the start point in case there are no glucose values at that time (for instances after starting a new sensor) diff --git a/xdrip/Texts/TextsSettingsView.swift b/xdrip/Texts/TextsSettingsView.swift index b73d0859e..04ec1111e 100644 --- a/xdrip/Texts/TextsSettingsView.swift +++ b/xdrip/Texts/TextsSettingsView.swift @@ -648,6 +648,10 @@ class Texts_SettingsView { return NSLocalizedString("appleWatchForceManualComplicationUpdateMessage", tableName: filename, bundle: Bundle.main, value: "This will manually force an update of the Apple Watch complications.\n\nIt will use up one of the remaining transfers available for today", comment: "Apple Watch Developer Settings - message explaining how to manually force a complication update") }() + static let allowStandByHighContrast: String = { + return NSLocalizedString("allowStandByHighContrast", tableName: filename, bundle: Bundle.main, value: "StandBy Night Mode", comment: "should we allow the StandBy mode to show a specific high contrast view at night") + }() + // MARK: - Calendar Events static let calendarEventsSectionTitle: String = { diff --git a/xdrip/View Controllers/Root View Controller/RootViewController.swift b/xdrip/View Controllers/Root View Controller/RootViewController.swift index 0bc37daba..9f6e0cd0b 100644 --- a/xdrip/View Controllers/Root View Controller/RootViewController.swift +++ b/xdrip/View Controllers/Root View Controller/RootViewController.swift @@ -3586,7 +3586,7 @@ final class RootViewController: UIViewController, ObservableObject { date.timeIntervalSince1970 } - let widgetSharedUserDefaultsModel = WidgetSharedUserDefaultsModel(bgReadingValues: bgReadingValues, bgReadingDatesAsDouble: bgReadingDatesAsDouble, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, slopeOrdinal: slopeOrdinal, deltaChangeInMgDl: deltaChangeInMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, dataSourceDescription: dataSourceDescription, keepAliveImageString: !UserDefaults.standard.isMaster ? UserDefaults.standard.followerBackgroundKeepAliveType.keepAliveImageString : nil) + let widgetSharedUserDefaultsModel = WidgetSharedUserDefaultsModel(bgReadingValues: bgReadingValues, bgReadingDatesAsDouble: bgReadingDatesAsDouble, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, slopeOrdinal: slopeOrdinal, deltaChangeInMgDl: deltaChangeInMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, dataSourceDescription: dataSourceDescription, allowStandByHighContrast: UserDefaults.standard.allowStandByHighContrast, keepAliveImageString: !UserDefaults.standard.isMaster ? UserDefaults.standard.followerBackgroundKeepAliveType.keepAliveImageString : nil) // store the model in the shared user defaults using a name that is uniquely specific to this copy of the app as installed on // the user's device - this allows several copies of the app to be installed without cross-contamination of widget data @@ -3627,7 +3627,7 @@ final class RootViewController: UIViewController, ObservableObject { } // create a chart view with just bg reading values and dates - let glucoseChartView = GlucoseChartView(glucoseChartType: glucoseChartType, bgReadingValues: bgReadingValues, bgReadingDates: bgReadingDates, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, liveActivitySize: .normal, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil) + let glucoseChartView = GlucoseChartView(glucoseChartType: glucoseChartType, bgReadingValues: bgReadingValues, bgReadingDates: bgReadingDates, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, liveActivitySize: .normal, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil, highContrast: nil) // render the glucose chart view as an image object guard let notificationImage = ImageRenderer(content: glucoseChartView).uiImage else { return } diff --git a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift index 79d001f60..df51a4610 100644 --- a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift +++ b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift @@ -34,6 +34,9 @@ fileprivate enum Setting:Int, CaseIterable { /// number of remaining forced complication updates available today case remainingComplicationUserInfoTransfers = 9 + /// allow StandBy mode to show a high contrast version of the widget at night + case allowStandByHighContrast = 10 + } class SettingsViewDevelopmentSettingsViewModel: NSObject, SettingsViewModelProtocol { @@ -91,6 +94,9 @@ class SettingsViewDevelopmentSettingsViewModel: NSObject, SettingsViewModelProto case .remainingComplicationUserInfoTransfers: return Texts_SettingsView.appleWatchRemainingComplicationUserInfoTransfers + + case .allowStandByHighContrast: + return Texts_SettingsView.allowStandByHighContrast } } @@ -100,7 +106,7 @@ class SettingsViewDevelopmentSettingsViewModel: NSObject, SettingsViewModelProto switch setting { - case .showDeveloperSettings, .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .shareToLoopOnceEvery5Minutes, .suppressLoopShare: + case .showDeveloperSettings, .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .shareToLoopOnceEvery5Minutes, .suppressLoopShare, .allowStandByHighContrast: return .none case .loopDelay, .libreLinkUpVersion, .remainingComplicationUserInfoTransfers: @@ -115,7 +121,7 @@ class SettingsViewDevelopmentSettingsViewModel: NSObject, SettingsViewModelProto switch setting { - case .showDeveloperSettings, .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .suppressLoopShare, .shareToLoopOnceEvery5Minutes, .loopDelay: + case .showDeveloperSettings, .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .suppressLoopShare, .shareToLoopOnceEvery5Minutes, .loopDelay, .allowStandByHighContrast: return nil case .libreLinkUpVersion: @@ -203,6 +209,14 @@ class SettingsViewDevelopmentSettingsViewModel: NSObject, SettingsViewModelProto }) + case .allowStandByHighContrast: + return UISwitch(isOn: UserDefaults.standard.allowStandByHighContrast, action: { + (isOn:Bool) in + + UserDefaults.standard.allowStandByHighContrast = isOn + + }) + case .remainingComplicationUserInfoTransfers, .loopDelay, .libreLinkUpVersion: return nil @@ -220,7 +234,7 @@ class SettingsViewDevelopmentSettingsViewModel: NSObject, SettingsViewModelProto switch setting { - case .showDeveloperSettings, .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .shareToLoopOnceEvery5Minutes, .suppressLoopShare: + case .showDeveloperSettings, .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .shareToLoopOnceEvery5Minutes, .suppressLoopShare, .allowStandByHighContrast: return .nothing case .loopDelay: From 55da2b8c39b002c746887934c4e3318003196526 Mon Sep 17 00:00:00 2001 From: Paul Plant <37302780+paulplant@users.noreply.github.com> Date: Sun, 2 Jun 2024 11:12:46 +0200 Subject: [PATCH 05/23] autostart internal sensor session from G6 glucose data message --- .../CGM/Dexcom/G5/CGMG5Transmitter.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/xdrip/BluetoothTransmitter/CGM/Dexcom/G5/CGMG5Transmitter.swift b/xdrip/BluetoothTransmitter/CGM/Dexcom/G5/CGMG5Transmitter.swift index 1d37c1db6..e80d39260 100644 --- a/xdrip/BluetoothTransmitter/CGM/Dexcom/G5/CGMG5Transmitter.swift +++ b/xdrip/BluetoothTransmitter/CGM/Dexcom/G5/CGMG5Transmitter.swift @@ -1284,13 +1284,19 @@ class CGMG5Transmitter:BluetoothTransmitter, CGMTransmitter { // setting glucoseTxSent to true, because we received just glucose data // possibly a glucoseTx message (was sent by other app on the same device, so it's not necessary to send a new one glucoseTxSent = true + + var forceNewSensor = false + + if let sensorStartDate = self.sensorStartDate, let activeSensorStartDate = UserDefaults.standard.activeSensorStartDate, activeSensorStartDate < sensorStartDate.addingTimeInterval(-15.0) { + forceNewSensor = true + } // this is a valid sensor state, now it's time to process receivedSensorStartDate if it exists if let receivedSensorStartDate = receivedSensorStartDate { // if current sensorStartDate is < receivedSensorStartDate then it seems a new sensor // adding an interval of 15 seconds, because sensorStartDate reported by transmitter can vary a second - if sensorStartDate == nil || (sensorStartDate! < receivedSensorStartDate.addingTimeInterval(-15.0)) { + if forceNewSensor || sensorStartDate == nil || (sensorStartDate! < receivedSensorStartDate.addingTimeInterval(-15.0)) { if let sensorStartDate = sensorStartDate { trace(" Currently known sensorStartDate = %{public}@.", log: log, category: ConstantsLog.categoryCGMG5, type: .info, sensorStartDate.toString(timeStyle: .long, dateStyle: .long)) @@ -1311,7 +1317,7 @@ class CGMG5Transmitter:BluetoothTransmitter, CGMTransmitter { sensorStartDate = receivedSensorStartDate // reset receivedSensorStartDate to nil - self.receivedSensorStartDate = nil + //self.receivedSensorStartDate = nil } @@ -1390,8 +1396,11 @@ class CGMG5Transmitter:BluetoothTransmitter, CGMTransmitter { // set timeStampLastSensorStartTimeRead timeStampLastSensorStartTimeRead = Date() + print("sensorStartDate = \(sensorStartDate)") + print("receivedSensorStartDate = \(receivedSensorStartDate)") + print("self.receivedSensorStartDate = \(self.receivedSensorStartDate)") // if current sensorStartDate is < from receivedSensorStartDate then it seems a new sensor - if sensorStartDate == nil || (sensorStartDate! < receivedSensorStartDate.addingTimeInterval(-15.0)) { + if self.receivedSensorStartDate == nil || sensorStartDate == nil || (sensorStartDate! < receivedSensorStartDate.addingTimeInterval(-15.0)) { if let sensorStartDate = sensorStartDate { trace(" Currently known sensorStartDate = %{public}@.", log: log, category: ConstantsLog.categoryCGMG5, type: .info, sensorStartDate.toString(timeStyle: .long, dateStyle: .long)) From 2ab0cab3749b57f195ea37b5fcd8d238873a1367 Mon Sep 17 00:00:00 2001 From: Paul Plant <37302780+paulplant@users.noreply.github.com> Date: Sun, 2 Jun 2024 11:19:02 +0200 Subject: [PATCH 06/23] Watch notification updates --- xDrip Watch App/Views/NotificationView.swift | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/xDrip Watch App/Views/NotificationView.swift b/xDrip Watch App/Views/NotificationView.swift index e70c95864..08936a796 100644 --- a/xDrip Watch App/Views/NotificationView.swift +++ b/xDrip Watch App/Views/NotificationView.swift @@ -11,7 +11,7 @@ import SwiftUI import UIKit -struct NotificationView: View { +struct NotificationView: View { var alertTitle: String? var bgValueAndTrend: String? var delta: String? @@ -23,8 +23,8 @@ struct NotificationView: View { var body: some View { VStack { - Text("\(alertTitle ?? "xDrip4iOS")") - .font(.headline) + Text("\(alertTitle ?? "LOW ALARM")") + .font(.headline).fontWeight(.semibold) .foregroundStyle(alertUrgencyType?.bannerTextColor ?? .white.opacity(0.85)) .lineLimit(1) .minimumScaleFactor(0.2) @@ -33,19 +33,19 @@ struct NotificationView: View { .padding(.bottom, 8) .background(alertUrgencyType?.bannerBackgroundColor ?? .black) - HStack(alignment: .firstTextBaseline) { - Text("\(bgValueAndTrend ?? "-")") - .font(.title3).bold() + HStack(alignment: .center) { + Text("\(bgValueAndTrend ?? "123")") + .font(.title2).fontWeight(.bold) .foregroundStyle(bgColor()) Spacer() HStack(alignment: .firstTextBaseline, spacing: 2) { - Text("\(delta ?? "?")") - .font(.body).bold() + Text("\(delta ?? "-2")") + .font(.title3).fontWeight(.semibold) .foregroundStyle(.colorPrimary) - Text("\(unit ?? "")") - .font(.body) + Text("\(unit ?? "mg/dL")") + .font(.title3) .foregroundStyle(.colorSecondary) } } From 02261c6075277a361f2dbe96a1d9ba2b46c557b2 Mon Sep 17 00:00:00 2001 From: Paul Plant <37302780+paulplant@users.noreply.github.com> Date: Sun, 2 Jun 2024 12:22:47 +0200 Subject: [PATCH 07/23] notification changes iOS/WatchOS --- .../Base.lproj/MainInterface.storyboard | 20 +++++++++---------- .../Info.plist | 2 +- xDrip Watch App/Views/NotificationView.swift | 20 +++++++------------ .../ConstantsGlucoseChartSwiftUI.swift | 6 +++--- xdrip/Managers/Charts/GlucoseChartType.swift | 3 --- .../RootViewController.swift | 6 +++++- 6 files changed, 26 insertions(+), 31 deletions(-) diff --git a/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard b/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard index 5e4f4a749..69ce91462 100644 --- a/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard +++ b/xDrip Notification Context Extension/Base.lproj/MainInterface.storyboard @@ -30,32 +30,32 @@ - + -