diff --git a/.github/workflows/add_identifiers.yml b/.github/workflows/add_identifiers.yml index cef4986f..b87b7feb 100644 --- a/.github/workflows/add_identifiers.yml +++ b/.github/workflows/add_identifiers.yml @@ -20,7 +20,7 @@ jobs: # Checks-out the repo - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Patch Fastlane Match to not print tables - name: Patch Match Tables diff --git a/.github/workflows/build_xdrip.yml b/.github/workflows/build_xdrip.yml index d88e2e33..5898b231 100644 --- a/.github/workflows/build_xdrip.yml +++ b/.github/workflows/build_xdrip.yml @@ -97,7 +97,7 @@ jobs: if: | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false') - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: token: ${{ secrets.GH_PAT }} ref: alive @@ -107,7 +107,7 @@ jobs: needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'JohanDegraeve' id: sync - uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 + uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1 with: target_sync_branch: ${{ env.ALIVE_BRANCH }} shallow_since: 6 months ago @@ -173,7 +173,7 @@ jobs: if: | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && vars.SCHEDULED_SYNC != 'false' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: token: ${{ secrets.GH_PAT }} ref: ${{ env.TARGET_BRANCH }} @@ -183,7 +183,7 @@ jobs: needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'JohanDegraeve' id: sync - uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 + uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1 with: target_sync_branch: ${{ env.TARGET_BRANCH }} shallow_since: 6 months ago @@ -213,7 +213,7 @@ jobs: echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT - name: Checkout Repo for building - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: token: ${{ secrets.GH_PAT }} submodules: recursive @@ -256,7 +256,7 @@ jobs: # Upload Build artifacts - name: Upload build log, IPA and Symbol artifacts if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-artifacts path: | diff --git a/.github/workflows/create_certs.yml b/.github/workflows/create_certs.yml index bb92663d..793a46a8 100644 --- a/.github/workflows/create_certs.yml +++ b/.github/workflows/create_certs.yml @@ -20,7 +20,7 @@ jobs: # Checks-out the repo - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Patch Fastlane Match to not print tables - name: Patch Match Tables diff --git a/.github/workflows/validate_secrets.yml b/.github/workflows/validate_secrets.yml index b3c5062b..398a6443 100644 --- a/.github/workflows/validate_secrets.yml +++ b/.github/workflows/validate_secrets.yml @@ -123,7 +123,7 @@ jobs: TEAMID: ${{ secrets.TEAMID }} steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Project Dependencies run: bundle install diff --git a/.gitignore b/.gitignore index a984b638..f851a8c8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ xDripConfigOverride.xcconfig UserInterfaceState.xcuserstate iBreakpoints_v2.xcbkptlist xdrip.xcodeproj/xcuserdata/johandegraeve.xcuserdatad/xcdebugger/ +Package.resolved # Ruby version settings .ruby-version diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/32.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/32.png deleted file mode 100644 index 8a2e07f5..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/32.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/40.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/40.png deleted file mode 100644 index 83702032..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/40.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json deleted file mode 100644 index 07892b8d..00000000 --- a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x" - }, - { - "filename" : "32.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "filename" : "40.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "auto-scaling" : "auto" - } -} diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json deleted file mode 100644 index e8b3252e..00000000 --- a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "assets" : [ - { - "filename" : "Circular.imageset", - "idiom" : "watch", - "role" : "circular" - }, - { - "filename" : "Extra Large.imageset", - "idiom" : "watch", - "role" : "extra-large" - }, - { - "filename" : "Graphic Bezel.imageset", - "idiom" : "watch", - "role" : "graphic-bezel" - }, - { - "filename" : "Graphic Circular.imageset", - "idiom" : "watch", - "role" : "graphic-circular" - }, - { - "filename" : "Graphic Corner.imageset", - "idiom" : "watch", - "role" : "graphic-corner" - }, - { - "filename" : "Graphic Extra Large.imageset", - "idiom" : "watch", - "role" : "graphic-extra-large" - }, - { - "filename" : "Graphic Large Rectangular.imageset", - "idiom" : "watch", - "role" : "graphic-large-rectangular" - }, - { - "filename" : "Modular.imageset", - "idiom" : "watch", - "role" : "modular" - }, - { - "filename" : "Utilitarian.imageset", - "idiom" : "watch", - "role" : "utilitarian" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/182.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/182.png deleted file mode 100644 index a3f00e8b..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/182.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/224.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/224.png deleted file mode 100644 index 958af12d..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/224.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json deleted file mode 100644 index 941d1d1c..00000000 --- a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x" - }, - { - "filename" : "182.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "filename" : "224.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "auto-scaling" : "auto" - } -} diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/94.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/94.png deleted file mode 100644 index 900f486b..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/94.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json deleted file mode 100644 index c80af019..00000000 --- a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x" - }, - { - "filename" : "94.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "auto-scaling" : "auto" - } -} diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/94.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/94.png deleted file mode 100644 index 900f486b..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/94.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json deleted file mode 100644 index c80af019..00000000 --- a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x" - }, - { - "filename" : "94.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "auto-scaling" : "auto" - } -} diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/44.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/44.png deleted file mode 100644 index 5f21be18..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/44.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json deleted file mode 100644 index b2a89a4d..00000000 --- a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x" - }, - { - "filename" : "44.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "auto-scaling" : "auto" - } -} diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/206.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/206.png deleted file mode 100644 index bb424b25..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/206.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/264.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/264.png deleted file mode 100644 index 27ba8966..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/264.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json deleted file mode 100644 index a2ea6fd8..00000000 --- a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x" - }, - { - "filename" : "206.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "filename" : "264.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "auto-scaling" : "auto" - } -} diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json deleted file mode 100644 index 6e184db8..00000000 --- a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "auto-scaling" : "auto" - } -} diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/52.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/52.png deleted file mode 100644 index 7a709412..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/52.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/64.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/64.png deleted file mode 100644 index 6a322360..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/64.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json deleted file mode 100644 index 061ff460..00000000 --- a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x" - }, - { - "filename" : "52.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "filename" : "64.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "auto-scaling" : "auto" - } -} diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/40.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/40.png deleted file mode 100644 index a9aaf332..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/40.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/50.png b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/50.png deleted file mode 100644 index 05ae4375..00000000 Binary files a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/50.png and /dev/null differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json b/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json deleted file mode 100644 index bc470fba..00000000 --- a/Watch App WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x" - }, - { - "filename" : "40.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "filename" : "50.png", - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "auto-scaling" : "auto" - } -} diff --git a/Watch App WatchKit Extension/ComplicationController.swift b/Watch App WatchKit Extension/ComplicationController.swift deleted file mode 100644 index 152dc224..00000000 --- a/Watch App WatchKit Extension/ComplicationController.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// ComplicationController.swift -// Watch App WatchKit Extension -// -// Created by Paul Plant on 5/10/21. -// Copyright © 2021 Johan Degraeve. All rights reserved. -// - -import ClockKit -import WatchKit - - -class ComplicationController: NSObject, CLKComplicationDataSource { - - - // MARK: - Complication Configuration - - // at the moment we're just going to define two complication family types and we'll leave them blank so that the complication image displays as a shortcut to open the app - func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) { - let descriptors = [ - CLKComplicationDescriptor(identifier: "complication", displayName: "xDrip4iO5", supportedFamilies: [CLKComplicationFamily.modularSmall, CLKComplicationFamily.graphicCircular]) - // Multiple complication support can be added here with more descriptors - ] - - // Call the handler with the currently supported complication descriptors - handler(descriptors) - } - - func handleSharedComplicationDescriptors(_ complicationDescriptors: [CLKComplicationDescriptor]) { - // Do any necessary work to support these newly shared complication descriptors - } - - // MARK: - Timeline Configuration - - func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { - // Call the handler with the last entry date you can currently provide or nil if you can't support future timelines - handler(nil) - } - - func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) { - // Call the handler with your desired behavior when the device is locked - handler(.showOnLockScreen) - } - - // MARK: - Timeline Population - func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { - - handler(nil) - } - - func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) { - // Call the handler with the timeline entries after the given date - handler(nil) - } - - // MARK: - Sample Templates - - func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) { - let template = getLocalizableSampleTemplate(for: complication.family) - handler(template) - } - - // basic templates copied from LoopKit and updated for WatchOS 7.0 - func getLocalizableSampleTemplate(for family: CLKComplicationFamily) -> CLKComplicationTemplate? { - - return nil - } - -} diff --git a/Watch App WatchKit Extension/ConstantsWatchApp.swift b/Watch App WatchKit Extension/ConstantsWatchApp.swift deleted file mode 100644 index 0e82d51a..00000000 --- a/Watch App WatchKit Extension/ConstantsWatchApp.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ConstantsWatchApp.swift -// Watch App WatchKit Extension -// -// Created by Paul Plant on 8/10/21. -// Copyright © 2021 Johan Degraeve. All rights reserved. -// - -import Foundation -import UIKit - -enum ConstantsWatchApp { - /// how long until the minutes ago label changes colour to warn the user? - static let minutesAgoWarningMinutes = 11 - static let minutesAgoUrgentMinutes = 21 - - /// define the colours used for each state - static let minsAgoLabelColor = UIColor.lightGray - static let minsAgoLabelColorWarning = UIColor.yellow - static let minsAgoLabelColorUrgent = UIColor.red - static let minsAgoLabelColorDeactivated = UIColor.gray - - static let deltaLabelColor = UIColor.white - static let deltaLabelColorDeactivated = UIColor.gray - - static let valueLabelColorDeactivated = UIColor.gray - - /// glucose colors - for values in range - static let glucoseInRangeColor = UIColor.green - - /// glucose colors - for values higher than urgentHighMarkValue or lower than urgent LowMarkValue - static let glucoseUrgentRangeColor = UIColor.red - - /// glucose colors - for values between highMarkValue and urgentHighMarkValue or between urgentLowMarkValue and lowMarkValue - static let glucoseNotUrgentRangeColor = UIColor.yellow - - - -} diff --git a/Watch App WatchKit Extension/ExtensionDelegate.swift b/Watch App WatchKit Extension/ExtensionDelegate.swift deleted file mode 100644 index 2741444d..00000000 --- a/Watch App WatchKit Extension/ExtensionDelegate.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// ExtensionDelegate.swift -// Watch App WatchKit Extension -// -// Created by Paul Plant on 5/10/21. -// Copyright © 2021 Johan Degraeve. All rights reserved. -// - -import WatchKit -import ClockKit - -@main -class ExtensionDelegate: NSObject, WKApplicationDelegate { - - static func shared() -> ExtensionDelegate { - return WKApplication.shared().extensionDelegate - } - - func applicationDidFinishLaunching() { - // Perform any final initialization of your application. - //scheduleNextReload() - } - - func applicationDidBecomeActive() { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillResignActive() { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, etc. - //reloadActiveComplications() - } - - func handle(_ backgroundTasks: Set) { - // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one. - for task in backgroundTasks { - // Use a switch statement to check the task type - switch task { - case let backgroundTask as WKApplicationRefreshBackgroundTask: - // Be sure to complete the background task once you’re done. - backgroundTask.setTaskCompletedWithSnapshot(false) - case let snapshotTask as WKSnapshotRefreshBackgroundTask: - // Snapshot tasks have a unique completion call, make sure to set your expiration date - snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil) - case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask: - // Be sure to complete the connectivity task once you’re done. - connectivityTask.setTaskCompletedWithSnapshot(false) - case let urlSessionTask as WKURLSessionRefreshBackgroundTask: - // Be sure to complete the URL session task once you’re done. - urlSessionTask.setTaskCompletedWithSnapshot(false) - case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask: - // Be sure to complete the relevant-shortcut task once you're done. - relevantShortcutTask.setTaskCompletedWithSnapshot(false) - case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask: - // Be sure to complete the intent-did-run task once you're done. - intentDidRunTask.setTaskCompletedWithSnapshot(false) - default: - // make sure to complete unhandled task types - task.setTaskCompletedWithSnapshot(false) - } - } - } -} - -fileprivate extension WKApplication { - var extensionDelegate: ExtensionDelegate! { - return delegate as? ExtensionDelegate - } -} diff --git a/Watch App WatchKit Extension/InterfaceController.swift b/Watch App WatchKit Extension/InterfaceController.swift deleted file mode 100644 index 7688a67a..00000000 --- a/Watch App WatchKit Extension/InterfaceController.swift +++ /dev/null @@ -1,255 +0,0 @@ -// -// InterfaceController.swift -// Watch App WatchKit Extension -// -// Created by Paul Plant on 5/10/21. -// Copyright © 2021 Johan Degraeve. All rights reserved. -// - -import WatchKit -import Foundation -import WatchConnectivity - - -class InterfaceController: WKInterfaceController { - - // MARK: - Properties - Outlets and Actions for buttons and labels in home screen - - @IBOutlet weak var minutesAgoLabelOutlet: WKInterfaceLabel! - @IBOutlet weak var deltaLabelOutlet: WKInterfaceLabel! - @IBOutlet weak var valueLabelOutlet: WKInterfaceLabel! - @IBOutlet weak var iconImageOutlet: WKInterfaceImage! - - /// we can attach this action to the value label (or whatever) and use it to force refresh the data if it is needed for some reason. We'll set it to require a 2 second push so it should not be triggered accidentally - @IBAction func longPressToRefresh(_ sender: Any) { - - // set all label outlets to deactivated and show a message to the user to acknowledge that a refresh has been requested - deltaLabelOutlet.setTextColor(ConstantsWatchApp.deltaLabelColorDeactivated) - - minutesAgoLabelOutlet.setText("Refreshing...") - minutesAgoLabelOutlet.setTextColor(ConstantsWatchApp.minsAgoLabelColorDeactivated) - - valueLabelOutlet.setTextColor(ConstantsWatchApp.valueLabelColorDeactivated) - - requestBGData() - - } - - // MARK: - Properties - other private properties - - // WatchConnectivity session needed for messaging with the companion app - private let session = WCSession.default - - - // declare and initialise app-wide variables - var currentBGValue: Double = 0 - var currentBGValueText: String = "" - var currentBGValueTextFull: String = "" - var currentBGValueTrend: String = "" - var currentBGTimestamp: Date = Date() - var deltaTextLocalized: String = "" - var minutesAgoText: String = "" - var minutesAgoTextLocalized: String = "" - var urgentLowMarkValueInUserChosenUnit: Double = 0 - var lowMarkValueInUserChosenUnit: Double = 0 - var highMarkValueInUserChosenUnit: Double = 0 - var urgentHighMarkValueInUserChosenUnit: Double = 0 - - - - // MARK: - overriden functions - - // we don't need to do much here except configure the session and delegate - override func awake(withContext context: Any?) { - - super.awake(withContext: context) - - session.delegate = self - session.activate() - - } - - override func willActivate() { - // This method is called when watch view controller is about to be visible to user - super.willActivate() - - // change some of the UI text so that the user sees that something is happening when they raise their write and we request new data - minutesAgoLabelOutlet.setText("Refreshing...") - - // pull new BG data from xDrip4iOS - requestBGData() - - } - - override func didDeactivate() { - // This method is called when watch view controller is no longer visible - super.didDeactivate() - - // when the app is deactivated or pushed to the background and then we'll change the text colours to gray to indicate (in case the user sees the screen without waking it up) that the app is currently not being updated. As soon as the app is activated, fresh data will be requested and the label colours and values updated accordingly - deltaLabelOutlet.setTextColor(ConstantsWatchApp.deltaLabelColorDeactivated) - - // as the minutesAgo label will get "frozen" when the watch app deactivates, let's change it to show the actual time of the last reading. At least it will correctly show the context until the app reactivates. - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale.current - dateFormatter.dateStyle = .none - dateFormatter.timeStyle = .short - - minutesAgoLabelOutlet.setTextColor(ConstantsWatchApp.minsAgoLabelColorDeactivated) - minutesAgoLabelOutlet.setText(dateFormatter.string(from: currentBGTimestamp)) - - valueLabelOutlet.setTextColor(ConstantsWatchApp.valueLabelColorDeactivated) - } - - - // MARK: - private helper functions - - /// This will update the watch view based upon the current values of the Interface Controller's private variables at the current time - private func updateWatchView() { - - // first we need to make sure that *all* required variables have been updated at least once by receiving a message for each from an active WKSession. This is needed because the messages will arrive asynchronously in a queue and it makes no sense to apply any logic unless they are all updated. - if (urgentLowMarkValueInUserChosenUnit > 0 && lowMarkValueInUserChosenUnit > 0 && highMarkValueInUserChosenUnit > 0 && urgentHighMarkValueInUserChosenUnit > 0 && currentBGValue > 0 && currentBGValueText != "" && deltaTextLocalized != "") { - - // calculate how long ago the last BG value was processed by the iOS app - let minutesAgo = -(Int(currentBGTimestamp.timeIntervalSinceNow) / 60) - - // build a locale-friendly text string using the freshly calculated value and the localized text sent by iOS - //let minutesAgoText = minutesAgo.description + " " + minutesAgoTextLocalized - - minutesAgoLabelOutlet.setText(minutesAgoText) - minutesAgoLabelOutlet.setTextColor(ConstantsWatchApp.minsAgoLabelColor) - - // let's see how long the "mins ago" string is. Some localizations produce a really long string (Dutch, Swedish) that isn't easily abbreviated without losing context. Althouhg unlikely, if this is the case, let's just hide the icon to allow the text to fit without issues - iconImageOutlet.setHidden(minutesAgoText.count > 13 ? true : false) - - deltaLabelOutlet.setText(deltaTextLocalized) - deltaLabelOutlet.setTextColor(ConstantsWatchApp.deltaLabelColor) - - valueLabelOutlet.setText(currentBGValueTextFull.description) - - // make a simple check to ensure that there is no incoherency between the BG and objective values (i.e. some values in mg/dl whilst others are still in mmol/l). This can happen as the message sending from the iOS session is asynchronous. When one value is updated before the others, then it can cause the wrong colour text to be displayed until the next messages arrive 0.5 seconds later and the view is corrected. - let coherencyCheck = (currentBGValue < 30 && urgentLowMarkValueInUserChosenUnit < 10 && lowMarkValueInUserChosenUnit < 10 && highMarkValueInUserChosenUnit < 30 && urgentHighMarkValueInUserChosenUnit < 30) || (currentBGValue > 20 && urgentLowMarkValueInUserChosenUnit > 20 && lowMarkValueInUserChosenUnit > 20 && highMarkValueInUserChosenUnit > 80 && urgentHighMarkValueInUserChosenUnit > 80) - - if minutesAgo > ConstantsWatchApp.minutesAgoUrgentMinutes { - - // if there's a clear problem and iOS hasn't sent any new data in 20-30 minutes - minutesAgoLabelOutlet.setTextColor(ConstantsWatchApp.minsAgoLabelColorUrgent) - - deltaLabelOutlet.setTextColor(ConstantsWatchApp.deltaLabelColorDeactivated) - - valueLabelOutlet.setText("Waiting for data...") - valueLabelOutlet.setTextColor(ConstantsWatchApp.valueLabelColorDeactivated) - - } else if minutesAgo > ConstantsWatchApp.minutesAgoWarningMinutes { - - // if there's a potential problem and iOS hasn't sent any new data in 10-15 minutes - minutesAgoLabelOutlet.setTextColor(ConstantsWatchApp.minsAgoLabelColorWarning) - - deltaLabelOutlet.setTextColor(ConstantsWatchApp.deltaLabelColorDeactivated) - - valueLabelOutlet.setTextColor(ConstantsWatchApp.valueLabelColorDeactivated) - - } else if (currentBGValue >= urgentHighMarkValueInUserChosenUnit || currentBGValue <= urgentLowMarkValueInUserChosenUnit) && coherencyCheck { - - // BG is higher than urgentHigh or lower than urgentLow objectives - valueLabelOutlet.setTextColor(ConstantsWatchApp.glucoseUrgentRangeColor) - - } else if (currentBGValue >= highMarkValueInUserChosenUnit || currentBGValue <= lowMarkValueInUserChosenUnit) && coherencyCheck { - - // BG is between urgentHigh/high and low/urgentLow objectives - valueLabelOutlet.setTextColor(ConstantsWatchApp.glucoseNotUrgentRangeColor) - - } else if coherencyCheck { - - // BG is between high and low objectives so considered "in range" - valueLabelOutlet.setTextColor(ConstantsWatchApp.glucoseInRangeColor) - } - } - } - - /// send a message to the iOS WCSession to request the delegate to immediately resend all current BG and other info - private func requestBGData() { - - let data: [String: Any] = ["action": "refreshBGData" as Any] - - session.sendMessage(data, replyHandler: nil, errorHandler: nil) - - } - -} - - -// MARK: - conform to WCSessionDelegate protocol - -/// This will process all messages received from the active WCSession. -/// This is done asynchronously in individual messages so we need to test which one has arrived before trying to process and assign values -/// All messages are sent as Strings so we will need to cast them into the required types before assigning to the class properties -extension InterfaceController: WCSessionDelegate { - - func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { - } - - func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { - - // uncomment the following for debug console use - // print("received message from iOS App: \(message)") - - if let data = message["currentBGTimeStamp"] as? String, let date = ISO8601DateFormatter().date(from: data) { - if date != currentBGTimestamp { - currentBGTimestamp = date - } - } - - if let data = message["currentBGValue"] as? String, let doubleValue = Double(data) { - currentBGValue = doubleValue - } - - if let data = message["currentBGValueTextFull"] as? String { - currentBGValueTextFull = data - } - - if let data = message["currentBGValueText"] as? String { - currentBGValueText = data - } - - if let data = message["currentBGValueTrend"] as? String { - currentBGValueTrend = data - } - - if let data = message["deltaTextLocalized"] as? String { - deltaTextLocalized = data - } - - if let data = message["minutesAgoTextLocalized"] as? String { - minutesAgoTextLocalized = data - } - - if let data = message["urgentLowMarkValueInUserChosenUnit"] as? String, let doubleValue = Double(data) { - urgentLowMarkValueInUserChosenUnit = doubleValue - } - - if let data = message["lowMarkValueInUserChosenUnit"] as? String, let doubleValue = Double(data) { - lowMarkValueInUserChosenUnit = doubleValue - } - - if let data = message["highMarkValueInUserChosenUnit"] as? String, let doubleValue = Double(data) { - highMarkValueInUserChosenUnit = doubleValue - } - - if let data = message["urgentHighMarkValueInUserChosenUnit"] as? String, let doubleValue = Double(data) { - urgentHighMarkValueInUserChosenUnit = doubleValue - } - - if let data = message["currentBGTimeStamp"] as? String, let date = ISO8601DateFormatter().date(from: data) { - if date != currentBGTimestamp { - currentBGTimestamp = date - } - let minutesAgo = -(Int(currentBGTimestamp.timeIntervalSinceNow) / 60) - minutesAgoText = minutesAgo.description + " " + minutesAgoTextLocalized - } - - // when we've finished, update the view - updateWatchView() - - } - -} diff --git a/Watch App/Assets.xcassets/20.imageset/20.png b/Watch App/Assets.xcassets/20.imageset/20.png deleted file mode 100644 index c8a21862..00000000 Binary files a/Watch App/Assets.xcassets/20.imageset/20.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/30.imageset/30.png b/Watch App/Assets.xcassets/30.imageset/30.png deleted file mode 100644 index eddce191..00000000 Binary files a/Watch App/Assets.xcassets/30.imageset/30.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/30.imageset/Contents.json b/Watch App/Assets.xcassets/30.imageset/Contents.json deleted file mode 100644 index 2118feeb..00000000 --- a/Watch App/Assets.xcassets/30.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "30.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Watch App/Assets.xcassets/40.imageset/40.png b/Watch App/Assets.xcassets/40.imageset/40.png deleted file mode 100644 index a9aaf332..00000000 Binary files a/Watch App/Assets.xcassets/40.imageset/40.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/40.imageset/Contents.json b/Watch App/Assets.xcassets/40.imageset/Contents.json deleted file mode 100644 index 35a59638..00000000 --- a/Watch App/Assets.xcassets/40.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "40.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Watch App/Assets.xcassets/50.imageset/50.png b/Watch App/Assets.xcassets/50.imageset/50.png deleted file mode 100644 index 05ae4375..00000000 Binary files a/Watch App/Assets.xcassets/50.imageset/50.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/50.imageset/Contents.json b/Watch App/Assets.xcassets/50.imageset/Contents.json deleted file mode 100644 index c1c53f25..00000000 --- a/Watch App/Assets.xcassets/50.imageset/Contents.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "images" : [ - { - "filename" : "50.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "auto-scaling" : "auto" - } -} diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/100.png b/Watch App/Assets.xcassets/AppIcon.appiconset/100.png deleted file mode 100644 index 57927ee8..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/100.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/102.png b/Watch App/Assets.xcassets/AppIcon.appiconset/102.png deleted file mode 100644 index aa61cae0..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/102.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/1024.png b/Watch App/Assets.xcassets/AppIcon.appiconset/1024.png deleted file mode 100644 index db41fbe6..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/1024.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/172.png b/Watch App/Assets.xcassets/AppIcon.appiconset/172.png deleted file mode 100644 index ee218535..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/172.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/196.png b/Watch App/Assets.xcassets/AppIcon.appiconset/196.png deleted file mode 100644 index 81d6df50..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/196.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/216.png b/Watch App/Assets.xcassets/AppIcon.appiconset/216.png deleted file mode 100644 index ffbbcc4d..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/216.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/234.png b/Watch App/Assets.xcassets/AppIcon.appiconset/234.png deleted file mode 100644 index c60700e6..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/234.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/48.png b/Watch App/Assets.xcassets/AppIcon.appiconset/48.png deleted file mode 100644 index 339ee3e1..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/48.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/55.png b/Watch App/Assets.xcassets/AppIcon.appiconset/55.png deleted file mode 100644 index 6b59c477..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/55.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/58.png b/Watch App/Assets.xcassets/AppIcon.appiconset/58.png deleted file mode 100644 index c550d53e..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/58.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/66.png b/Watch App/Assets.xcassets/AppIcon.appiconset/66.png deleted file mode 100644 index e0999fae..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/66.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/80.png b/Watch App/Assets.xcassets/AppIcon.appiconset/80.png deleted file mode 100644 index aba2dc11..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/80.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/87.png b/Watch App/Assets.xcassets/AppIcon.appiconset/87.png deleted file mode 100644 index e8f9f9cc..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/87.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/88.png b/Watch App/Assets.xcassets/AppIcon.appiconset/88.png deleted file mode 100644 index 79fb8c42..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/88.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/92.png b/Watch App/Assets.xcassets/AppIcon.appiconset/92.png deleted file mode 100644 index 832927c0..00000000 Binary files a/Watch App/Assets.xcassets/AppIcon.appiconset/92.png and /dev/null differ diff --git a/Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json b/Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 7a9f8c6f..00000000 --- a/Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "images" : [ - { - "filename" : "48.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "24x24", - "subtype" : "38mm" - }, - { - "filename" : "55.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "27.5x27.5", - "subtype" : "42mm" - }, - { - "filename" : "58.png", - "idiom" : "watch", - "role" : "companionSettings", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "87.png", - "idiom" : "watch", - "role" : "companionSettings", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "66.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "33x33", - "subtype" : "45mm" - }, - { - "filename" : "80.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "40x40", - "subtype" : "38mm" - }, - { - "filename" : "88.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "44x44", - "subtype" : "40mm" - }, - { - "filename" : "92.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "46x46", - "subtype" : "41mm" - }, - { - "filename" : "100.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "50x50", - "subtype" : "44mm" - }, - { - "filename" : "102.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "51x51", - "subtype" : "45mm" - }, - { - "filename" : "172.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "86x86", - "subtype" : "38mm" - }, - { - "filename" : "196.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "98x98", - "subtype" : "42mm" - }, - { - "filename" : "216.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "108x108", - "subtype" : "44mm" - }, - { - "filename" : "234.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "117x117", - "subtype" : "45mm" - }, - { - "filename" : "1024.png", - "idiom" : "watch-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Watch App/Base.lproj/Interface.storyboard b/Watch App/Base.lproj/Interface.storyboard deleted file mode 100644 index 7db5e613..00000000 --- a/Watch App/Base.lproj/Interface.storyboard +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Watch App/appstore.png b/Watch App/appstore.png deleted file mode 100644 index db41fbe6..00000000 Binary files a/Watch App/appstore.png and /dev/null differ diff --git a/Watch App/da.lproj/Interface.strings b/Watch App/da.lproj/Interface.strings deleted file mode 100644 index db9a8a40..00000000 --- a/Watch App/da.lproj/Interface.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "WKInterfaceLabel"; text = "---"; ObjectID = "RJK-M1-Bu5"; */ -"RJK-M1-Bu5.text" = "---"; - -/* Class = "WKInterfaceLabel"; text = "Waiting for data..."; ObjectID = "ies-y9-I8N"; */ -"ies-y9-I8N.text" = "Waiting for data..."; - -/* Class = "WKInterfaceLabel"; text = "---"; ObjectID = "oq4-iM-qEv"; */ -"oq4-iM-qEv.text" = "---"; diff --git a/Watch App/el.lproj/Interface.strings b/Watch App/el.lproj/Interface.strings deleted file mode 100644 index db9a8a40..00000000 --- a/Watch App/el.lproj/Interface.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "WKInterfaceLabel"; text = "---"; ObjectID = "RJK-M1-Bu5"; */ -"RJK-M1-Bu5.text" = "---"; - -/* Class = "WKInterfaceLabel"; text = "Waiting for data..."; ObjectID = "ies-y9-I8N"; */ -"ies-y9-I8N.text" = "Waiting for data..."; - -/* Class = "WKInterfaceLabel"; text = "---"; ObjectID = "oq4-iM-qEv"; */ -"oq4-iM-qEv.text" = "---"; diff --git a/Watch App/playstore.png b/Watch App/playstore.png deleted file mode 100644 index a9d20023..00000000 Binary files a/Watch App/playstore.png and /dev/null differ diff --git a/Watch App/ru.lproj/Interface.strings b/Watch App/ru.lproj/Interface.strings deleted file mode 100644 index 0767dc02..00000000 --- a/Watch App/ru.lproj/Interface.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "WKInterfaceLabel"; text = "---"; ObjectID = "RJK-M1-Bu5"; */ -"RJK-M1-Bu5.text" = "---"; - -/* Class = "WKInterfaceLabel"; text = "Waiting for data..."; ObjectID = "ies-y9-I8N"; */ -"ies-y9-I8N.text" = "Ожидание данных..."; - -/* Class = "WKInterfaceLabel"; text = "---"; ObjectID = "oq4-iM-qEv"; */ -"oq4-iM-qEv.text" = "---"; diff --git a/Watch App/tr.lproj/Interface.strings b/Watch App/tr.lproj/Interface.strings deleted file mode 100644 index db9a8a40..00000000 --- a/Watch App/tr.lproj/Interface.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "WKInterfaceLabel"; text = "---"; ObjectID = "RJK-M1-Bu5"; */ -"RJK-M1-Bu5.text" = "---"; - -/* Class = "WKInterfaceLabel"; text = "Waiting for data..."; ObjectID = "ies-y9-I8N"; */ -"ies-y9-I8N.text" = "Waiting for data..."; - -/* Class = "WKInterfaceLabel"; text = "---"; ObjectID = "oq4-iM-qEv"; */ -"oq4-iM-qEv.text" = "---"; diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8c26f6d5..800f023d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -56,8 +56,9 @@ platform :ios do git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), app_identifier: [ "com.#{TEAMID}.xdripswift", - "com.#{TEAMID}.xdripswift.xDrip4iOS-Widget", - "com.#{TEAMID}.xdripswift.watchkitapp" + "com.#{TEAMID}.xdripswift.xDripWidget", + "com.#{TEAMID}.xdripswift.watchkitapp", + "com.#{TEAMID}.xdripswift.watchkitapp.xDripWatchComplication" ] ) @@ -86,16 +87,23 @@ platform :ios do update_code_signing_settings( path: "#{GITHUB_WORKSPACE}/xdrip.xcodeproj", - profile_name: mapping["com.#{TEAMID}.xdripswift.xDrip4iOS-Widget"], + profile_name: mapping["com.#{TEAMID}.xdripswift.xDripWidget"], code_sign_identity: "iPhone Distribution", - targets: ["xDrip4iOS Widget"] + targets: ["xDrip Widget Extension"] ) update_code_signing_settings( path: "#{GITHUB_WORKSPACE}/xdrip.xcodeproj", profile_name: mapping["com.#{TEAMID}.xdripswift.watchkitapp"], code_sign_identity: "iPhone Distribution", - targets: ["Watch App"] + targets: ["xDrip Watch App"] + ) + + update_code_signing_settings( + path: "#{GITHUB_WORKSPACE}/xdrip.xcodeproj", + profile_name: mapping["com.#{TEAMID}.xdripswift.watchkitapp.xDripWatchComplication"], + code_sign_identity: "iPhone Distribution", + targets: ["xDrip Watch Complication Extension"] ) gym( @@ -153,11 +161,15 @@ platform :ios do Spaceship::ConnectAPI::BundleIdCapability::Type::NFC_TAG_READING ]) - configure_bundle_id("xDrip4iOS Widget", "com.#{TEAMID}.xdripswift.xDrip4iOS-Widget", [ + configure_bundle_id("xDrip Widget Extension", "com.#{TEAMID}.xdripswift.xDripWidget", [ Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS ]) - configure_bundle_id("Watch App", "com.#{TEAMID}.xdripswift.watchkitapp", [ + configure_bundle_id("xDrip Watch App", "com.#{TEAMID}.xdripswift.watchkitapp", [ + Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS + ]) + + configure_bundle_id("xDrip Watch Complication Extension", "com.#{TEAMID}.xdripswift.watchkitapp.xDripWatchComplication", [ Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS ]) @@ -181,8 +193,9 @@ platform :ios do git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), app_identifier: [ "com.#{TEAMID}.xdripswift", - "com.#{TEAMID}.xdripswift.xDrip4iOS-Widget", - "com.#{TEAMID}.xdripswift.watchkitapp" + "com.#{TEAMID}.xdripswift.xDripWidget", + "com.#{TEAMID}.xdripswift.watchkitapp", + "com.#{TEAMID}.xdripswift.watchkitapp.xDripWatchComplication" ] ) end diff --git a/fastlane/testflight.md b/fastlane/testflight.md index 231e7399..4bf34af2 100644 --- a/fastlane/testflight.md +++ b/fastlane/testflight.md @@ -133,8 +133,9 @@ Note 2 - Depending on your build history, you may find some of the Identifiers a 1. Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list) on the apple developer site. 1. For each of the following identifier names: * xdripswift - * xdripswift xDrip4iOS-Widget - * Watch App + * xDrip Widget Extension + * xDrip Watch App + * xDrip Watch Complication Extension 1. Click on the identifier's name. 1. On the "App Groups" capabilies, click on the "Configure" button. 1. Select the "Loop App Group" _(yes, "Loop App Group" is correct)_ diff --git a/Watch App/Assets.xcassets/AccentColor.colorset/Contents.json b/xDrip Watch App/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Watch App/Assets.xcassets/AccentColor.colorset/Contents.json rename to xDrip Watch App/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/xDrip Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json b/xDrip Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..0c22f513 --- /dev/null +++ b/xDrip Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Icon-1025.png", + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/xDrip Watch App/Assets.xcassets/AppIcon.appiconset/Icon-1025.png b/xDrip Watch App/Assets.xcassets/AppIcon.appiconset/Icon-1025.png new file mode 100644 index 00000000..2d1aa91b Binary files /dev/null and b/xDrip Watch App/Assets.xcassets/AppIcon.appiconset/Icon-1025.png differ diff --git a/Watch App WatchKit Extension/Assets.xcassets/Contents.json b/xDrip Watch App/Assets.xcassets/Contents.json similarity index 100% rename from Watch App WatchKit Extension/Assets.xcassets/Contents.json rename to xDrip Watch App/Assets.xcassets/Contents.json diff --git a/xDrip Watch App/Constants/ConstantsAppleWatch.swift b/xDrip Watch App/Constants/ConstantsAppleWatch.swift new file mode 100644 index 00000000..37a1c1e0 --- /dev/null +++ b/xDrip Watch App/Constants/ConstantsAppleWatch.swift @@ -0,0 +1,35 @@ +// +// ConstantsAppleWatch.swift +// xDrip Watch App +// +// Created by Paul Plant on 22/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI + +enum ConstantsAppleWatch { + + /// an array holding the different "chart hour to show" options when swiping left/right + static let hoursToShow: [Double] = [2, 3, 4, 6, 9, 12] + + /// the default index of hoursToShow when the app is opened (i.e. for example [2] = 4 hours) + static let hoursToShowDefaultIndex: Int = 2 + + /// less than how many pixels wide should we consider the screen size to the "small" + static let pixelWidthLimitForSmallScreen: Double = 185 + + /// colour for the "requesting data" symbol when active + static let requestingDataIconColorActive = Color(.green) + + /// colour for the "requesting data" symbol when inactive + static let requestingDataIconColorInactive = Color(.white).opacity(0.3) + + /// font size for the "requesting data" symbol + static let requestingDataIconFontSize: CGFloat = 6 + + /// SFSymbol name as a string for the "requesting data" symbol + static let requestingDataIconSFSymbolName: String = "circle.fill" + +} diff --git a/xDrip Watch App/DataModels/WatchStateModel.swift b/xDrip Watch App/DataModels/WatchStateModel.swift new file mode 100644 index 00000000..7dfed556 --- /dev/null +++ b/xDrip Watch App/DataModels/WatchStateModel.swift @@ -0,0 +1,370 @@ +// +// WatchModel.swift +// xDrip Watch App +// +// Created by Paul Plant on 11/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Combine +import Foundation +import SwiftUI +import WatchConnectivity +import WidgetKit + +/// holds, the watch state and allows updates and computed properties/variables to be generated for the different views that use it +/// also used to update the ComplicationSharedUserDefaultsModel in the app group so that the complication can access the data +class WatchStateModel: NSObject, ObservableObject { + + /// the Watch Connectivity session + var session: WCSession + + // set timer to automatically refresh the view + // https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-a-timer-with-swiftui + let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect() + @Published var timerControlDate = Date() + + var bgReadingValues: [Double] = [] + var bgReadingDates: [Date] = [] + + @Published var updatedDatesString: String = "" + + @Published var isMgDl: Bool = true + @Published var slopeOrdinal: Int = 0 + @Published var deltaChangeInMgDl: Double = 0 + @Published var urgentLowLimitInMgDl: Double = 60 + @Published var lowLimitInMgDl: Double = 80 + @Published var highLimitInMgDl: Double = 170 + @Published var urgentHighLimitInMgDl: Double = 250 + @Published var updatedDate: Date = Date() + @Published var activeSensorDescription: String = "" + @Published var sensorAgeInMinutes: Double = 0 + @Published var sensorMaxAgeInMinutes: Double = 14400 + @Published var timeStampOfLastFollowerConnection: Date = Date() + @Published var secondsUntilFollowerDisconnectWarning: Int = 90 + @Published var timeStampOfLastHeartBeat: Date = Date() + @Published var secondsUntilHeartBeatDisconnectWarning: Int = 90 + @Published var isMaster: Bool = true + @Published var followerDataSourceType: FollowerDataSourceType = .nightscout + @Published var followerBackgroundKeepAliveType: FollowerBackgroundKeepAliveType = .normal + @Published var disableComplications: Bool = false + + @Published var lastUpdatedTextString: String = "Requesting data..." + @Published var lastUpdatedTimeString: String = "" + @Published var debugString: String = "Debug..." + @Published var chartHoursIndex: Int = 1 + @Published var requestingDataIconColor: Color = ConstantsAppleWatch.requestingDataIconColorInactive + + init(session: WCSession = .default) { + self.session = session + super.init() + + session.delegate = self + session.activate() + } + + /// the latest BG reading value in the array as a double + /// - Returns: an optional double with the bg value in mg/dL if it exists + func bgValueInMgDl() -> Double? { + return bgReadingValues.isEmpty ? nil : bgReadingValues[0] + } + + /// return the latest BG value in the user's chosen unit as a string + /// - Returns: a string with bgValueInMgDl() converted into the user unit + func bgValueStringInUserChosenUnit() -> String { + if let bgReadingDate = bgReadingDate(), let bgValueInMgDl = bgValueInMgDl(), bgReadingDate > Date().addingTimeInterval(-60 * 20) { + return bgReadingValues.isEmpty ? "---" : bgValueInMgDl.mgdlToMmolAndToString(mgdl: isMgDl) + } else { + return "---" + } + } + + /// the timestamp of the latest BG reading value in the array + /// - Returns: an optional date + func bgReadingDate() -> Date? { + return bgReadingDates.isEmpty ? nil : bgReadingDates[0] + } + + /// returns the localized string of mg/dL or mmol/L + /// - Returns: string representation of mg/dL or mmol/L + func bgUnitString() -> String { + return isMgDl ? Texts_Common.mgdl : Texts_Common.mmol + } + + /// Blood glucose color dependant on the user defined limit values and also on if it is a recent value + /// - Returns: a Color object either red, yellow or green + func bgTextColor() -> Color { + if let bgReadingDate = bgReadingDate(), bgReadingDate > Date().addingTimeInterval(-60 * 7), let bgValueInMgDl = bgValueInMgDl() { + if bgValueInMgDl >= urgentHighLimitInMgDl || bgValueInMgDl <= urgentLowLimitInMgDl { + return Color(.red) + } else if bgValueInMgDl >= highLimitInMgDl || bgValueInMgDl <= lowLimitInMgDl { + return Color(.yellow) + } else { + return Color(.green) + } + } else { + return Color(.gray) + } + } + + /// Color dependant on how long ago the last BG reading was + /// - Returns: a Color either normal (gray) or yellow/red if the reading was several minutes ago and hasn't been updated + func lastUpdatedTimeColor() -> Color { + if let bgReadingDate = bgReadingDate(), bgReadingDate > Date().addingTimeInterval(-60 * 7) { + return Color(.gray) + } else if let bgReadingDate = bgReadingDate(), bgReadingDate > Date().addingTimeInterval(-60 * 12) { + return Color(.yellow) + } else { + return Color(.red) + } + } + + /// returns a string holding the trend arrow + /// - Returns: trend arrow string (i.e. "↑") + func trendArrow() -> String { + if let bgReadingDate = bgReadingDate(), bgReadingDate > Date().addingTimeInterval(-60 * 20) { + switch slopeOrdinal { + case 7: + return "\u{2193}\u{2193}" // ↓↓ + case 6: + return "\u{2193}" // ↓ + case 5: + return "\u{2198}" // ↘ + case 4: + return "\u{2192}" // → + case 3: + return "\u{2197}" // ↗ + case 2: + return "\u{2191}" // ↑ + case 1: + return "\u{2191}\u{2191}" // ↑↑ + default: + return "" + } + } else { + return "" + } + } + + /// convert the optional delta change int (in mg/dL) to a formatted change value in the user chosen unit making sure all zero values are shown as a positive change to follow Nightscout convention + /// - Returns: a string holding the formatted delta change value (i.e. +0.4 or -6) + func deltaChangeStringInUserChosenUnit() -> String { + if let bgReadingDate = bgReadingDate(), bgReadingDate > Date().addingTimeInterval(-60 * 20) { + let valueAsString = deltaChangeInMgDl.mgdlToMmolAndToString(mgdl: isMgDl) + + var deltaSign: String = "" + if (deltaChangeInMgDl > 0) { deltaSign = "+"; } + + // quickly check "value" and prevent "-0mg/dl" or "-0.0mmol/l" being displayed + // show unitized zero deltas as +0 or +0.0 as per Nightscout format + if (isMgDl) { + if (deltaChangeInMgDl > -1) && (deltaChangeInMgDl < 1) { + return "+0" + } else { + return deltaSign + valueAsString + } + } else { + if (deltaChangeInMgDl > -0.1) && (deltaChangeInMgDl < 0.1) { + return "+0.0" + } else { + return deltaSign + valueAsString + } + } + } else { + return "-" + } + } + + /// function to calculate the sensor progress value and return a text color to be used by the view + /// - Returns: progress: the % progress between 0 and 1, textColor: + func activeSensorProgress() -> (progress: Float, textColor: Color) { + if sensorAgeInMinutes > 0 { + let sensorTimeLeftInMinutes = sensorMaxAgeInMinutes - sensorAgeInMinutes + let progress = Float(1 - (sensorTimeLeftInMinutes / sensorMaxAgeInMinutes)) + + // irrespective of all the above, if the current sensor age is over the max age, then just set everything to the expired colour to make it clear + if sensorTimeLeftInMinutes < 0 { + return (1.0, ConstantsHomeView.sensorProgressExpiredSwiftUI) + } else if sensorTimeLeftInMinutes <= ConstantsHomeView.sensorProgressViewUrgentInMinutes { + return (progress, ConstantsHomeView.sensorProgressViewProgressColorUrgentSwiftUI) + } else if sensorTimeLeftInMinutes <= ConstantsHomeView.sensorProgressViewWarningInMinutes { + return (progress, ConstantsHomeView.sensorProgressViewProgressColorWarningSwiftUI) + } else { + return (progress, ConstantsHomeView.sensorProgressNormalTextColorSwiftUI) + } + } else { + return (0, ConstantsHomeView.sensorProgressNormalTextColorSwiftUI) + } + } + + /// check when the last follower connection was and compare that to the actual time + /// - Returns: image and color of the correct follower connection status + func getFollowerConnectionNetworkStatus() -> (image: Image, color: Color) { + if timeStampOfLastFollowerConnection > Date().addingTimeInterval(-Double(secondsUntilFollowerDisconnectWarning)) { + return(Image(systemName: "network"), Color(.green)) + } else { + if followerBackgroundKeepAliveType != .disabled { + return(Image(systemName: "network.slash"), Color(.red)) + } else { + // if keep-alive is disabled, then this will never show a constant server connection so just "disable" + // the icon when not recent. It would be incorrect to show a red error. + return(Image(systemName: "network.slash"), Color(.gray)) + } + } + } + + /// check when the last heartbeat connection was and compare that to the actual time + /// if no heartbeat, just return the standard gray colour for the keep alive type icon + func getFollowerBackgroundKeepAliveColor() -> Color { + if followerBackgroundKeepAliveType == .heartbeat { + if let timeDifferenceInSeconds = Calendar.current.dateComponents([.second], from: timeStampOfLastHeartBeat, to: Date()).second, timeDifferenceInSeconds > secondsUntilHeartBeatDisconnectWarning { + return .red + } else { + return .green + } + } else { + return Color(.gray) + } + } + + /// request a state update from the iOS companion app + func requestWatchStateUpdate() { + guard session.activationState == .activated else { + session.activate() + return + } + // change the text, this must be done in the main thread but only do it if the watch app is reachable + if session.isReachable { + DispatchQueue.main.async { + self.requestingDataIconColor = ConstantsAppleWatch.requestingDataIconColorActive + self.debugString.removeLast(4) + self.debugString += "Fetching" + } + } + + print("Requesting watch state update from iOS") + session.sendMessage(["requestWatchStateUpdate": true], replyHandler: nil) { error in + print("WatchStateModel error: " + error.localizedDescription) + } + } + + /// update the watch state so that the view can be updated + /// - Parameter watchState: this is the new watch state as sent from the iOS companion app + private func processState(_ watchState: WatchState) { + updatedDate = Date() + + bgReadingValues = watchState.bgReadingValues + bgReadingDates = watchState.bgReadingDates + isMgDl = watchState.isMgDl ?? true + slopeOrdinal = watchState.slopeOrdinal ?? 5 + deltaChangeInMgDl = watchState.deltaChangeInMgDl ?? 2 + urgentLowLimitInMgDl = watchState.urgentLowLimitInMgDl ?? 60 + lowLimitInMgDl = watchState.lowLimitInMgDl ?? 80 + highLimitInMgDl = watchState.highLimitInMgDl ?? 180 + urgentHighLimitInMgDl = watchState.urgentHighLimitInMgDl ?? 240 + activeSensorDescription = watchState.activeSensorDescription ?? "" + sensorAgeInMinutes = watchState.sensorAgeInMinutes ?? 0 + sensorMaxAgeInMinutes = watchState.sensorMaxAgeInMinutes ?? 0 + timeStampOfLastFollowerConnection = watchState.timeStampOfLastFollowerConnection ?? .distantPast + secondsUntilFollowerDisconnectWarning = watchState.secondsUntilFollowerDisconnectWarning ?? 70 + timeStampOfLastHeartBeat = watchState.timeStampOfLastHeartBeat ?? .distantPast + secondsUntilHeartBeatDisconnectWarning = watchState.secondsUntilHeartBeatDisconnectWarning ?? 5 + isMaster = watchState.isMaster ?? true + followerDataSourceType = FollowerDataSourceType(rawValue: watchState.followerDataSourceTypeRawValue ?? 0) ?? .nightscout + followerBackgroundKeepAliveType = FollowerBackgroundKeepAliveType(rawValue: watchState.followerBackgroundKeepAliveTypeRawValue ?? 0) ?? .normal + disableComplications = watchState.disableComplications ?? false + + // check if there is any BG data available before updating the data source info strings accordingly + if let bgReadingDate = bgReadingDate() { + lastUpdatedTextString = "Last reading " + lastUpdatedTimeString = bgReadingDate.formatted(date: .omitted, time: .shortened) + } else { + lastUpdatedTextString = "No sensor data" + lastUpdatedTimeString = "" + } + + debugString = generateDebugString() + + // now process the shared user defaults to get data for the WidgetKit complications + updateWatchSharedUserDefaults() + + // change the requesting icon color back after a small delay to prevent it + // flashing on/off too quickly + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.requestingDataIconColor = ConstantsAppleWatch.requestingDataIconColorInactive + } + } + + /// once we've process the state update, then save this data to the shared app group so that the complication can read it + private func updateWatchSharedUserDefaults() { + + guard let sharedUserDefaults = UserDefaults(suiteName: Bundle.main.appGroupSuiteName) else { return } + + let bgReadingDatesAsDouble = bgReadingDates.map { date in + date.timeIntervalSince1970 + } + + let complicationSharedUserDefaultsModel = ComplicationSharedUserDefaultsModel(bgReadingValues: bgReadingValues, bgReadingDatesAsDouble: bgReadingDatesAsDouble, isMgDl: isMgDl, slopeOrdinal: slopeOrdinal, deltaChangeInMgDl: deltaChangeInMgDl, urgentLowLimitInMgDl: urgentLowLimitInMgDl, lowLimitInMgDl: lowLimitInMgDl, highLimitInMgDl: highLimitInMgDl, urgentHighLimitInMgDl: urgentHighLimitInMgDl, disableComplications: disableComplications) + + // 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/complication data + if let stateData = try? JSONEncoder().encode(complicationSharedUserDefaultsModel) { + sharedUserDefaults.set(stateData, forKey: "complicationSharedUserDefaults.\(Bundle.main.mainAppBundleIdentifier)") + } + + // now that the new data is stored in the app group, try to force the complications to reload + WidgetCenter.shared.reloadAllTimelines() + } + + // generate a debugString + private func generateDebugString() -> String { + + var debugString = "Last state: \(Date().formatted(date: .omitted, time: .standard))" + + // check if there is any BG data available before updating the strings accordingly + if let bgReadingDate = bgReadingDate() { + debugString += "\nBG updated: \(bgReadingDate.formatted(date: .omitted, time: .standard))" + } else { + debugString += "\nBG updated: ---" + } + + debugString += "\nBG values: \(bgReadingValues.count)" + + if !isMaster { + debugString += "\nFollower conn.: \(timeStampOfLastFollowerConnection.formatted(date: .omitted, time: .standard))" + + if followerBackgroundKeepAliveType == .heartbeat { + debugString += "\nLast hearbeat: \(timeStampOfLastHeartBeat.formatted(date: .omitted, time: .standard))" + } + } + + debugString += "\nScreen width: \(Int(WKInterfaceDevice.current().screenBounds.size.width))" + debugString += "\niOS app: Idle" + + return debugString + } +} + +extension WatchStateModel: WCSessionDelegate { +#if os(iOS) + public func sessionDidBecomeInactive(_ session: WCSession) {} + public func sessionDidDeactivate(_ session: WCSession) {} +#endif + + func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error _: Error?) { + requestWatchStateUpdate() + } + + func session(_: WCSession, didReceiveMessage _: [String: Any]) {} + + func sessionReachabilityDidChange(_ session: WCSession) { + } + + func session(_: WCSession, didReceiveMessageData messageData: Data) { + if let watchState = try? JSONDecoder().decode(WatchState.self, from: messageData) { + DispatchQueue.main.async { + self.processState(watchState) + } + } + } +} diff --git a/Watch App/Assets.xcassets/Contents.json b/xDrip Watch App/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from Watch App/Assets.xcassets/Contents.json rename to xDrip Watch App/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/xDrip Watch App/Views/MainView.swift b/xDrip Watch App/Views/MainView.swift new file mode 100644 index 00000000..1307226e --- /dev/null +++ b/xDrip Watch App/Views/MainView.swift @@ -0,0 +1,155 @@ +// +// MainView.swift +// xDrip Watch App +// +// Created by Paul Plant on 11/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import SwiftUI + +struct MainView: View { + @EnvironmentObject var watchState: WatchStateModel + + // get the array of different hour ranges from the constants file + // we'll move through this array as the user swipes left/right on the chart + let hoursToShow: [Double] = ConstantsAppleWatch.hoursToShow + + @State private var hoursToShowIndex: Int = ConstantsAppleWatch.hoursToShowDefaultIndex + + @State private var showDebug: Bool = false + + let isSmallScreen = WKInterfaceDevice.current().screenBounds.size.width < ConstantsAppleWatch.pixelWidthLimitForSmallScreen ? true : false + + // MARK: - Body + var body: some View { + + let overrideChartHeight: Double? = isSmallScreen ? ConstantsGlucoseChartSwiftUI.viewHeightWatchAppSmall : nil + + let overrideChartWidth: Double? = isSmallScreen ? ConstantsGlucoseChartSwiftUI.viewWidthWatchAppSmall : nil + + ZStack(alignment: Alignment(horizontal: .center, vertical: .center), content: { + VStack(spacing: 2) { + HeaderView() + .padding([.leading, .trailing], 5) + .padding([.top], -6) + .padding([.bottom], -6) + .onTapGesture(count: 2) { + 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) + .gesture( + DragGesture(minimumDistance: 80, coordinateSpace: .local) + .onEnded({ value in + if (value.startLocation.x > value.location.x) { + if hoursToShow[hoursToShowIndex] != hoursToShow.first { + hoursToShowIndex -= 1 + } + } else { + if hoursToShow[hoursToShowIndex] != hoursToShow.last { + hoursToShowIndex += 1 + } + } + }) + ) + + DataSourceView() + + InfoView() + } + .padding(.bottom, 20) + + if showDebug { + Text(watchState.debugString) + .foregroundStyle(.black) + .font(.system(size: isSmallScreen ? 12 : 14)) + .multilineTextAlignment(.leading) + .padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)) + .background(.teal).opacity(0.85) + .cornerRadius(8) + } + }) + .frame(maxHeight: .infinity) + .onReceive(watchState.timer) { date in + if watchState.updatedDate.timeIntervalSinceNow < -5 { + watchState.timerControlDate = date + watchState.requestWatchStateUpdate() + } + } + .onAppear { + watchState.requestWatchStateUpdate() + } + .onTapGesture(count: 5) { + showDebug = !showDebug + } + } +} + + +// MARK: - Preview +struct ContentView_Previews: PreviewProvider { + + static func bgDateArray() -> [Date] { + let endDate = Date() + let startDate = endDate.addingTimeInterval(-3600 * 12) + var currentDate = startDate + + var dateArray: [Date] = [] + + while currentDate < endDate { + dateArray.append(currentDate) + currentDate = currentDate.addingTimeInterval(60 * 5) + } + + return dateArray + } + + static func bgValueArray() -> [Double] { + + var bgValueArray:[Double] = Array(repeating: 0, count: 144) + var currentValue: Double = 120 + var increaseValues: Bool = true + + for index in bgValueArray.indices { + let randomValue = Double(Int.random(in: -10..<30)) + + if currentValue < 70 { + increaseValues = true + bgValueArray[index] = currentValue + abs(randomValue) + } else if currentValue > 180 { + increaseValues = false + bgValueArray[index] = currentValue - abs(randomValue) + } else { + bgValueArray[index] = currentValue + (increaseValues ? randomValue : -randomValue) + } + currentValue = bgValueArray[index] + } + return bgValueArray + } + + static var previews: some View { + let watchState = WatchStateModel() + + watchState.bgReadingValues = bgValueArray() + watchState.bgReadingDates = bgDateArray() + watchState.isMgDl = false + watchState.slopeOrdinal = 3 + watchState.deltaChangeInMgDl = 2 + watchState.urgentLowLimitInMgDl = 60 + watchState.lowLimitInMgDl = 80 + watchState.highLimitInMgDl = 140 + watchState.urgentHighLimitInMgDl = 180 + watchState.updatedDate = Date().addingTimeInterval(-120) + watchState.activeSensorDescription = "Data Source" + watchState.sensorAgeInMinutes = Double(Int.random(in: 1..<14400)) + watchState.sensorMaxAgeInMinutes = 14400 + watchState.isMaster = false + watchState.followerDataSourceType = .libreLinkUp + watchState.followerBackgroundKeepAliveType = .heartbeat + + return Group { + MainView() + }.environmentObject(watchState) + } +} diff --git a/xDrip Watch App/Views/SubViews/DataSourceView.swift b/xDrip Watch App/Views/SubViews/DataSourceView.swift new file mode 100644 index 00000000..026389ad --- /dev/null +++ b/xDrip Watch App/Views/SubViews/DataSourceView.swift @@ -0,0 +1,150 @@ +// +// DataSourceView.swift +// xDrip Watch App +// +// Created by Paul Plant on 21/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI + +struct DataSourceView: View { + @EnvironmentObject var watchState: WatchStateModel + + let isSmallScreen = WKInterfaceDevice.current().screenBounds.size.width < ConstantsAppleWatch.pixelWidthLimitForSmallScreen ? true : false + + var body: some View { + VStack(spacing: 2) { + + let textSize: CGFloat = isSmallScreen ? 12 : 14 + + if (watchState.activeSensorDescription != "" || watchState.sensorAgeInMinutes > 0) || !watchState.isMaster { + + ProgressView(value: Float(watchState.activeSensorProgress().progress)) + .tint(ConstantsHomeView.sensorProgressViewNormalColorSwiftUI) + .scaleEffect(x: 1, y: 0.3, anchor: .center) + + HStack(alignment: .center) { + if !watchState.isMaster { + HStack(alignment: .center, spacing: isSmallScreen ? 2 : 4) { + watchState.getFollowerConnectionNetworkStatus().image + .font(.system(size: textSize)) + .foregroundStyle(watchState.getFollowerConnectionNetworkStatus().color) + + watchState.followerBackgroundKeepAliveType.keepAliveImage + .font(.system(size: textSize)) + .foregroundStyle(watchState.getFollowerBackgroundKeepAliveColor()) + + Text(watchState.followerDataSourceType.fullDescription) + .font(.system(size: textSize)).fontWeight(.semibold) + .minimumScaleFactor(0.2) + } + } else { + Text(watchState.activeSensorDescription) + .font(.system(size: textSize)).fontWeight(.semibold) + } + + Spacer() + + Image(systemName: ConstantsAppleWatch.requestingDataIconSFSymbolName) + .font(.system(size: ConstantsAppleWatch.requestingDataIconFontSize, weight: .heavy)) + .foregroundStyle(watchState.requestingDataIconColor) + .padding(.bottom, -2) + .padding(.trailing, -3) + + if watchState.sensorAgeInMinutes > 0 { + Text(watchState.sensorAgeInMinutes.minutesToDaysAndHours()) + .font(.system(size: textSize)) + .foregroundStyle(watchState.activeSensorProgress().textColor) + } + } + .padding([.leading, .trailing], isSmallScreen ? 6 : 8) + } else { + ProgressView(value: 0) + .tint(ConstantsHomeView.sensorProgressViewNormalColorSwiftUI) + .scaleEffect(x: 1, y: 0.3, anchor: .center) + + HStack { + Text(" ⚠️ " + Texts_HomeView.noDataSourceConnectedWatch) + .font(.system(size: textSize)).bold() + + Spacer() + + Image(systemName: ConstantsAppleWatch.requestingDataIconSFSymbolName) + .font(.system(size: ConstantsAppleWatch.requestingDataIconFontSize, weight: .heavy)) + .foregroundStyle(watchState.requestingDataIconColor) + .padding(.bottom, -2) + } + .padding([.leading, .trailing], 10) + } + } + } +} + +struct DataSourceView_Previews: PreviewProvider { + + static func bgDateArray() -> [Date] { + let endDate = Date() + let startDate = endDate.addingTimeInterval(-3600 * 12) + var currentDate = startDate + + var dateArray: [Date] = [] + + while currentDate < endDate { + dateArray.append(currentDate) + currentDate = currentDate.addingTimeInterval(60 * 5) + } + + return dateArray + } + + static func bgValueArray() -> [Double] { + + var bgValueArray:[Double] = Array(repeating: 0, count: 144) + var currentValue: Double = 120 + var increaseValues: Bool = true + + for index in bgValueArray.indices { + let randomValue = Double(Int.random(in: -10..<30)) + + if currentValue < 70 { + increaseValues = true + bgValueArray[index] = currentValue + abs(randomValue) + } else if currentValue > 180 { + increaseValues = false + bgValueArray[index] = currentValue - abs(randomValue) + } else { + bgValueArray[index] = currentValue + (increaseValues ? randomValue : -randomValue) + } + currentValue = bgValueArray[index] + } + return bgValueArray + } + + static var previews: some View { + let watchState = WatchStateModel() + + watchState.bgReadingValues = bgValueArray() + watchState.bgReadingDates = bgDateArray() + watchState.isMgDl = true + watchState.slopeOrdinal = 5 + watchState.deltaChangeInMgDl = -2 + watchState.urgentLowLimitInMgDl = 60 + watchState.lowLimitInMgDl = 80 + watchState.highLimitInMgDl = 140 + watchState.urgentHighLimitInMgDl = 180 + watchState.updatedDate = Date().addingTimeInterval(-400) + watchState.activeSensorDescription = "Data Source" + watchState.sensorAgeInMinutes = 0 + watchState.sensorMaxAgeInMinutes = 14400 + watchState.isMaster = false + watchState.followerDataSourceType = .libreLinkUp + watchState.secondsUntilFollowerDisconnectWarning = 60 * 7 + watchState.timeStampOfLastFollowerConnection = Date().addingTimeInterval(-60 * 6) + + return Group { + DataSourceView() + }.environmentObject(watchState) + } +} diff --git a/xDrip Watch App/Views/SubViews/HeaderView.swift b/xDrip Watch App/Views/SubViews/HeaderView.swift new file mode 100644 index 00000000..2164173c --- /dev/null +++ b/xDrip Watch App/Views/SubViews/HeaderView.swift @@ -0,0 +1,102 @@ +// +// HeaderView.swift +// xDrip Watch App +// +// Created by Paul Plant on 21/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI + +struct HeaderView: View { + @EnvironmentObject var watchState: WatchStateModel + + let isSmallScreen = WKInterfaceDevice.current().screenBounds.size.width < ConstantsAppleWatch.pixelWidthLimitForSmallScreen ? true : false + + var body: some View { + HStack(alignment: .lastTextBaseline) { + Text("\(watchState.bgValueStringInUserChosenUnit())\(watchState.trendArrow())") + .font(.system(size: isSmallScreen ? 40 : 50)).fontWeight(.semibold) + .foregroundStyle(watchState.bgTextColor()) + .scaledToFill() + .minimumScaleFactor(0.5) + .lineLimit(1) + + Spacer() + + VStack(alignment: .trailing, spacing: 0) { + Spacer() + Text(watchState.deltaChangeStringInUserChosenUnit()) + .font(.system(size: isSmallScreen ? 24 : 28)).fontWeight(.semibold) + .lineLimit(1) + .padding(.bottom, isSmallScreen ? -5 : -6) + Text(watchState.bgUnitString()) + .font(.system(size: isSmallScreen ? 12 : 14)) + .foregroundStyle(.gray) + .lineLimit(1) + } + } + } +} + +struct HeaderView_Previews: PreviewProvider { + static func bgDateArray() -> [Date] { + let endDate = Date() + let startDate = endDate.addingTimeInterval(-3600 * 12) + var currentDate = startDate + + var dateArray: [Date] = [] + + while currentDate < endDate { + dateArray.append(currentDate) + currentDate = currentDate.addingTimeInterval(60 * 5) + } + + return dateArray + } + + static func bgValueArray() -> [Double] { + var bgValueArray:[Double] = Array(repeating: 0, count: 144) + var currentValue: Double = 120 + var increaseValues: Bool = true + + for index in bgValueArray.indices { + let randomValue = Double(Int.random(in: -10..<30)) + + if currentValue < 70 { + increaseValues = true + bgValueArray[index] = currentValue + abs(randomValue) + } else if currentValue > 180 { + increaseValues = false + bgValueArray[index] = currentValue - abs(randomValue) + } else { + bgValueArray[index] = currentValue + (increaseValues ? randomValue : -randomValue) + } + currentValue = bgValueArray[index] + } + return bgValueArray + } + + static var previews: some View { + let watchState = WatchStateModel() + + watchState.bgReadingValues = bgValueArray() + watchState.bgReadingDates = bgDateArray() + watchState.isMgDl = true + watchState.slopeOrdinal = 5 + watchState.deltaChangeInMgDl = -2 + watchState.urgentLowLimitInMgDl = 60 + watchState.lowLimitInMgDl = 80 + watchState.highLimitInMgDl = 140 + watchState.urgentHighLimitInMgDl = 180 + watchState.updatedDate = Date().addingTimeInterval(-120) + watchState.activeSensorDescription = "Data Source" + watchState.sensorAgeInMinutes = Double(Int.random(in: 1..<14400)) + watchState.sensorMaxAgeInMinutes = 14400 + + return Group { + HeaderView() + }.environmentObject(watchState) + } +} diff --git a/xDrip Watch App/Views/SubViews/InfoView.swift b/xDrip Watch App/Views/SubViews/InfoView.swift new file mode 100644 index 00000000..2c2edff5 --- /dev/null +++ b/xDrip Watch App/Views/SubViews/InfoView.swift @@ -0,0 +1,92 @@ +// +// InfoView.swift +// xDrip Watch App +// +// Created by Paul Plant on 24/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI + +struct InfoView: View { + @EnvironmentObject var watchState: WatchStateModel + + let isSmallScreen = WKInterfaceDevice.current().screenBounds.size.width < ConstantsAppleWatch.pixelWidthLimitForSmallScreen ? true : false + + var body: some View { + + let textSize: CGFloat = isSmallScreen ? 12 : 14 + + HStack(spacing: 2) { + Text(watchState.lastUpdatedTextString) + .font(.system(size: textSize)) + .foregroundStyle(.gray) + + Text(watchState.lastUpdatedTimeString) + .font(.system(size: textSize)) + .foregroundStyle(watchState.lastUpdatedTimeColor()) + } + } +} + +struct InfoView_Previews: PreviewProvider { + static func bgDateArray() -> [Date] { + let endDate = Date() + let startDate = endDate.addingTimeInterval(-3600 * 12) + var currentDate = startDate + + var dateArray: [Date] = [] + + while currentDate < endDate { + dateArray.append(currentDate) + currentDate = currentDate.addingTimeInterval(60 * 5) + } + + return dateArray + } + + static func bgValueArray() -> [Double] { + var bgValueArray:[Double] = Array(repeating: 0, count: 144) + var currentValue: Double = 120 + var increaseValues: Bool = true + + for index in bgValueArray.indices { + let randomValue = Double(Int.random(in: -10..<30)) + + if currentValue < 70 { + increaseValues = true + bgValueArray[index] = currentValue + abs(randomValue) + } else if currentValue > 180 { + increaseValues = false + bgValueArray[index] = currentValue - abs(randomValue) + } else { + bgValueArray[index] = currentValue + (increaseValues ? randomValue : -randomValue) + } + currentValue = bgValueArray[index] + } + return bgValueArray + } + + static var previews: some View { + let watchState = WatchStateModel() + + watchState.bgReadingValues = bgValueArray() + watchState.bgReadingDates = bgDateArray() + watchState.isMgDl = true + watchState.slopeOrdinal = 5 + watchState.deltaChangeInMgDl = -2 + watchState.urgentLowLimitInMgDl = 60 + watchState.lowLimitInMgDl = 80 + watchState.highLimitInMgDl = 140 + watchState.urgentHighLimitInMgDl = 180 + watchState.updatedDate = Date().addingTimeInterval(-120) + watchState.activeSensorDescription = "Data Source" + watchState.sensorAgeInMinutes = Double(Int.random(in: 1..<14400)) + watchState.sensorMaxAgeInMinutes = 14400 + + return Group { + InfoView() + }.environmentObject(watchState) + } +} diff --git a/xDrip4iOS Widget/xDrip4iOS Widget.entitlements b/xDrip Watch App/xDrip Watch App.entitlements similarity index 100% rename from xDrip4iOS Widget/xDrip4iOS Widget.entitlements rename to xDrip Watch App/xDrip Watch App.entitlements diff --git a/xDrip Watch App/xDripWatchApp.swift b/xDrip Watch App/xDripWatchApp.swift new file mode 100644 index 00000000..15a9ae56 --- /dev/null +++ b/xDrip Watch App/xDripWatchApp.swift @@ -0,0 +1,22 @@ +// +// xDripWatchApp.swift +// xDrip Watch App +// +// Created by Paul Plant on 11/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import SwiftUI + +@main +struct xDrip_Watch_AppApp: App { + @StateObject var watchState = WatchStateModel() + + var body: some Scene { + WindowGroup { + NavigationView { + MainView() + }.environmentObject(watchState) + } + } +} diff --git a/xDrip Watch Complication Extension.entitlements b/xDrip Watch Complication Extension.entitlements new file mode 100644 index 00000000..96f2950d --- /dev/null +++ b/xDrip Watch Complication Extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup + + + diff --git a/xDrip Watch Complication/Assets.xcassets/AccentColor.colorset/Contents.json b/xDrip Watch Complication/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/xDrip Watch Complication/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/xDrip Watch Complication/Assets.xcassets/AppIcon.appiconset/Contents.json b/xDrip Watch Complication/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..0c22f513 --- /dev/null +++ b/xDrip Watch Complication/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Icon-1025.png", + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/xDrip Watch Complication/Assets.xcassets/AppIcon.appiconset/Icon-1025.png b/xDrip Watch Complication/Assets.xcassets/AppIcon.appiconset/Icon-1025.png new file mode 100644 index 00000000..2d1aa91b Binary files /dev/null and b/xDrip Watch Complication/Assets.xcassets/AppIcon.appiconset/Icon-1025.png differ diff --git a/Watch App/Assets.xcassets/20.imageset/Contents.json b/xDrip Watch Complication/Assets.xcassets/ComplicationIcon.imageset/Contents.json similarity index 88% rename from Watch App/Assets.xcassets/20.imageset/Contents.json rename to xDrip Watch Complication/Assets.xcassets/ComplicationIcon.imageset/Contents.json index b771de38..b72cde78 100644 --- a/Watch App/Assets.xcassets/20.imageset/Contents.json +++ b/xDrip Watch Complication/Assets.xcassets/ComplicationIcon.imageset/Contents.json @@ -1,11 +1,11 @@ { "images" : [ { - "filename" : "20.png", "idiom" : "universal", "scale" : "1x" }, { + "filename" : "Icon-122.png", "idiom" : "universal", "scale" : "2x" }, diff --git a/xDrip Watch Complication/Assets.xcassets/ComplicationIcon.imageset/Icon-122.png b/xDrip Watch Complication/Assets.xcassets/ComplicationIcon.imageset/Icon-122.png new file mode 100644 index 00000000..425064bf Binary files /dev/null and b/xDrip Watch Complication/Assets.xcassets/ComplicationIcon.imageset/Icon-122.png differ diff --git a/xDrip Watch Complication/Assets.xcassets/Contents.json b/xDrip Watch Complication/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/xDrip Watch Complication/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/xDrip Watch Complication/Assets.xcassets/WidgetBackground.colorset/Contents.json b/xDrip Watch Complication/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/xDrip Watch Complication/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/xDrip Watch Complication/DataModels/ComplicationSharedUserDefaultsModel.swift b/xDrip Watch Complication/DataModels/ComplicationSharedUserDefaultsModel.swift new file mode 100644 index 00000000..c7d8a7f7 --- /dev/null +++ b/xDrip Watch Complication/DataModels/ComplicationSharedUserDefaultsModel.swift @@ -0,0 +1,23 @@ +// +// ComplicationSharedUserDefaultsModel.swift +// xdrip +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation + +/// model of the data we'll store in the shared app group to pass from the watch app to the widgets +struct ComplicationSharedUserDefaultsModel: Codable { + var bgReadingValues: [Double] + var bgReadingDatesAsDouble: [Double] + var isMgDl: Bool + var slopeOrdinal: Int + var deltaChangeInMgDl: Double + var urgentLowLimitInMgDl: Double + var lowLimitInMgDl: Double + var highLimitInMgDl: Double + var urgentHighLimitInMgDl: Double + var disableComplications: Bool +} diff --git a/Watch App WatchKit Extension/Info.plist b/xDrip Watch Complication/Info.plist similarity index 56% rename from Watch App WatchKit Extension/Info.plist rename to xDrip Watch Complication/Info.plist index 27ab2812..b6743ec9 100644 --- a/Watch App WatchKit Extension/Info.plist +++ b/xDrip Watch Complication/Info.plist @@ -2,15 +2,14 @@ + AppGroupIdentifier + $(APP_GROUP_IDENTIFIER) + MainAppBundleIdentifier + $(MAIN_APP_BUNDLE_IDENTIFIER) NSExtension - NSExtensionAttributes - - WKAppBundleIdentifier - $(MAIN_APP_BUNDLE_IDENTIFIER).watchkitapp - NSExtensionPointIdentifier - com.apple.watchkit + com.apple.widgetkit-extension diff --git a/xDrip Watch Complication/Views/AccessoryCircularView.swift b/xDrip Watch Complication/Views/AccessoryCircularView.swift new file mode 100644 index 00000000..b4885329 --- /dev/null +++ b/xDrip Watch Complication/Views/AccessoryCircularView.swift @@ -0,0 +1,43 @@ +// +// AccessoryCircularView.swift +// xDrip Watch Complication Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI + +extension XDripWatchComplication.EntryView { + @ViewBuilder + var accessoryCircularView: some View { + if !entry.widgetState.disableComplications { + Gauge(value: entry.widgetState.bgValueInMgDl ?? 100, in: entry.widgetState.gaugeModel().minValue...entry.widgetState.gaugeModel().maxValue) { + Text("Not shown") + } currentValueLabel: { + Text(entry.widgetState.bgValueStringInUserChosenUnit) + .font(.system(size: 20)).bold() + .minimumScaleFactor(0.2) + .lineLimit(1) + } minimumValueLabel: { + Text(entry.widgetState.gaugeModel().minValue.mgdlToMmolAndToString(mgdl: entry.widgetState.isMgDl)) + .font(.system(size: 8)) + .foregroundStyle(Color(white: 0.7)) + .minimumScaleFactor(0.2) + } maximumValueLabel: { + Text(entry.widgetState.gaugeModel().maxValue.mgdlToMmolAndToString(mgdl: entry.widgetState.isMgDl)) + .font(.system(size: 8)) + .foregroundStyle(Color(white: 0.7)) + .minimumScaleFactor(0.2) + } + .gaugeStyle(.accessoryCircular) + .tint(entry.widgetState.gaugeModel().gaugeGradient) + .widgetBackground(backgroundView: Color.clear) + } else { + Image("ComplicationIcon") + .resizable() + .widgetBackground(backgroundView: Color.clear) + } + } +} diff --git a/xDrip Watch Complication/Views/AccessoryCornerView.swift b/xDrip Watch Complication/Views/AccessoryCornerView.swift new file mode 100644 index 00000000..40b71d81 --- /dev/null +++ b/xDrip Watch Complication/Views/AccessoryCornerView.swift @@ -0,0 +1,51 @@ +// +// AccessoryCornerView.swift +// xDrip Watch Complication Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI + +extension XDripWatchComplication.EntryView { + @ViewBuilder + var accessoryCornerView: some View { + if !entry.widgetState.disableComplications { + Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow())") + .font(.system(size: 20)) + .foregroundColor(entry.widgetState.bgTextColor()) + .minimumScaleFactor(0.2) + .widgetCurvesContent() + .widgetLabel { + Gauge(value: entry.widgetState.bgValueInMgDl ?? 100, in: entry.widgetState.gaugeModel().minValue...entry.widgetState.gaugeModel().maxValue) { + Text("Not shown") + } currentValueLabel: { + Text("Not shown") + } minimumValueLabel: { + Text(entry.widgetState.gaugeModel().minValue.mgdlToMmolAndToString(mgdl: entry.widgetState.isMgDl)) + .font(.system(size: 8)) + .foregroundStyle(Color(white: 0.7)) + .minimumScaleFactor(0.2) + } maximumValueLabel: { + Text(entry.widgetState.gaugeModel().maxValue.mgdlToMmolAndToString(mgdl: entry.widgetState.isMgDl)) + .font(.system(size: 8)) + .foregroundStyle(Color(white: 0.7)) + .minimumScaleFactor(0.2) + } + .tint(entry.widgetState.gaugeModel().gaugeGradient) + .gaugeStyle(LinearCapacityGaugeStyle()) // Doesn't do anything + } + .widgetBackground(backgroundView: Color.clear) + } else { + Text(" ") + .font(.system(size: 20)) + .minimumScaleFactor(0.2) + .widgetCurvesContent() + .widgetLabel("\(ConstantsHomeView.applicationName)") + .widgetBackground(backgroundView: Color.clear) + } + } +} + diff --git a/xDrip Watch Complication/Views/AccessoryInlineView.swift b/xDrip Watch Complication/Views/AccessoryInlineView.swift new file mode 100644 index 00000000..7d4c54c7 --- /dev/null +++ b/xDrip Watch Complication/Views/AccessoryInlineView.swift @@ -0,0 +1,22 @@ +// +// AccessoryInlineView.swift +// xDrip Watch Complication Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI + +extension XDripWatchComplication.EntryView { + @ViewBuilder + var accessoryInlineView: some View { + if !entry.widgetState.disableComplications { + Text("\(entry.widgetState.bgValueStringInUserChosenUnit) \(entry.widgetState.trendArrow()) \(entry.widgetState.deltaChangeStringInUserChosenUnit())") + } else { + Text("\(ConstantsHomeView.applicationName)") + } + } +} + diff --git a/xDrip Watch Complication/Views/AccessoryRectangularView.swift b/xDrip Watch Complication/Views/AccessoryRectangularView.swift new file mode 100644 index 00000000..ba14fe0e --- /dev/null +++ b/xDrip Watch Complication/Views/AccessoryRectangularView.swift @@ -0,0 +1,57 @@ +// +// AccessoryRectangularView.swift +// xDrip Watch Complication Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI + +extension XDripWatchComplication.EntryView { + @ViewBuilder + + var accessoryRectangularView: some View { + ZStack { + VStack(spacing: 0) { + HStack(alignment: .center) { + HStack(alignment: .center, spacing: 4) { + Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow()) ") + .font(.system(size: entry.widgetState.isSmallScreen() ? 20 : 24)).bold() + .foregroundStyle(entry.widgetState.bgTextColor()) + + Text(entry.widgetState.deltaChangeStringInUserChosenUnit()) + .font(.system(size: entry.widgetState.isSmallScreen() ? 20 : 24)).fontWeight(.semibold) + .foregroundStyle(entry.widgetState.deltaChangeTextColor()) + .lineLimit(1) + } + + Spacer() + + Text("\(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")") + .font(.system(size: entry.widgetState.isSmallScreen() ? 14 : 16)) + .foregroundStyle(Color(white: 0.7)) + .minimumScaleFactor(0.2) + } + .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()) + + if entry.widgetState.disableComplications { + HStack(alignment: .center, spacing: 4) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: entry.widgetState.isSmallScreen() ? 12 : 14)) + + Text("Keep-alive disabled") + .font(.system(size: entry.widgetState.isSmallScreen() ? 12 : 14)) + .multilineTextAlignment(.leading) + } + .foregroundStyle(.teal) + .padding(0) + } + } + } + .widgetBackground(backgroundView: Color.clear) + } +} diff --git a/xDrip Watch Complication/XDripWatchComplication+Entry.swift b/xDrip Watch Complication/XDripWatchComplication+Entry.swift new file mode 100644 index 00000000..cfc578da --- /dev/null +++ b/xDrip Watch Complication/XDripWatchComplication+Entry.swift @@ -0,0 +1,179 @@ +// +// XDripWatchComplication+Entry.swift +// xDrip Watch Complication Extension +// +// Created by Paul Plant on 28/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import WidgetKit +import SwiftUI + +extension XDripWatchComplication { + struct Entry: TimelineEntry { + var date: Date = .now + var widgetState: WidgetState + + } +} + +// MARK: - WidgetState + +extension XDripWatchComplication.Entry { + + /// struct to hold the currect values that the widget/complication should show + struct WidgetState { + var bgReadingValues: [Double]? + var bgReadingDates: [Date]? + var isMgDl: Bool + var slopeOrdinal: Int + var deltaChangeInMgDl: Double? + var urgentLowLimitInMgDl: Double + var lowLimitInMgDl: Double + var highLimitInMgDl: Double + var urgentHighLimitInMgDl: Double + + var bgUnitString: String + var bgValueInMgDl: Double? + var bgReadingDate: Date? + var bgValueStringInUserChosenUnit: String + var disableComplications: Bool + + 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, disableComplications: Bool? = false) { + self.bgReadingValues = bgReadingValues + self.bgReadingDates = bgReadingDates + self.isMgDl = isMgDl ?? true + self.slopeOrdinal = slopeOrdinal ?? 0 + self.deltaChangeInMgDl = deltaChangeInMgDl + self.urgentLowLimitInMgDl = urgentLowLimitInMgDl ?? 60 + self.lowLimitInMgDl = lowLimitInMgDl ?? 80 + self.highLimitInMgDl = highLimitInMgDl ?? 180 + self.urgentHighLimitInMgDl = urgentHighLimitInMgDl ?? 250 + + self.bgValueInMgDl = (bgReadingValues?.count ?? 0) > 0 ? bgReadingValues?[0] : nil + self.bgReadingDate = (bgReadingDates?.count ?? 0) > 0 ? bgReadingDates?[0] : nil + self.bgUnitString = self.isMgDl ? Texts_Common.mgdl : Texts_Common.mmol + self.bgValueStringInUserChosenUnit = (bgReadingValues?.count ?? 0) > 0 ? bgReadingValues?[0].mgdlToMmolAndToString(mgdl: self.isMgDl) ?? "" : "" + self.disableComplications = disableComplications ?? false + + } + + /// Blood glucose color dependant on the user defined limit values and based upon the time since the last reading + /// - Returns: a Color either red, yellow or green + func bgTextColor() -> Color { + if let bgReadingDate = bgReadingDate, bgReadingDate > Date().addingTimeInterval(-60 * 7), let bgValueInMgDl = bgValueInMgDl { + if bgValueInMgDl >= urgentHighLimitInMgDl || bgValueInMgDl <= urgentLowLimitInMgDl { + return Color(.red) + } else if bgValueInMgDl >= highLimitInMgDl || bgValueInMgDl <= lowLimitInMgDl { + return Color(.yellow) + } else { + return Color(.green) + } + } else { + return Color(.gray) + } + } + + /// Delta text color dependant on the time since the last reading + /// - Returns: a Color either red, yellow or green + func deltaChangeTextColor() -> Color { + if let bgReadingDate = bgReadingDate, bgReadingDate > Date().addingTimeInterval(-60 * 7) { + return Color(white: 0.8) + } else { + return Color(.gray) + } + } + + /// convert the optional delta change int (in mg/dL) to a formatted change value in the user chosen unit making sure all zero values are shown as a positive change to follow Nightscout convention + /// - Returns: a string holding the formatted delta change value (i.e. +0.4 or -6) + func deltaChangeStringInUserChosenUnit() -> String { + if let deltaChangeInMgDl = deltaChangeInMgDl { + let deltaSign: String = deltaChangeInMgDl > 0 ? "+" : "" + let valueAsString = deltaChangeInMgDl.mgdlToMmolAndToString(mgdl: isMgDl) + + // quickly check "value" and prevent "-0mg/dl" or "-0.0mmol/l" being displayed + // show unitized zero deltas as +0 or +0.0 as per Nightscout format + if (isMgDl) { + return (deltaChangeInMgDl > -1 && deltaChangeInMgDl < 1) ? "+0" : (deltaSign + valueAsString) + } else { + return (deltaChangeInMgDl > -0.1 && deltaChangeInMgDl < 0.1) ? "+0.0" : (deltaSign + valueAsString) + } + } + return "" + } + + + /// returns a string holding the trend arrow + /// - Returns: trend arrow string (i.e. "↑") + func trendArrow() -> String { + switch slopeOrdinal { + case 7: + return "\u{2193}\u{2193}" // ↓↓ + case 6: + return "\u{2193}" // ↓ + case 5: + return "\u{2198}" // ↘ + case 4: + return "\u{2192}" // → + case 3: + return "\u{2197}" // ↗ + case 2: + return "\u{2191}" // ↑ + case 1: + return "\u{2191}\u{2191}" // ↑↑ + default: + return "" + } + } + + /// used to return values and colors used by a SwiftUI gauge view + /// - Returns: minValue/maxValue - used to define the limits of the gauge. gaugeColor/gaugeGradient - the gauge view will use one or the other + func gaugeModel() -> (minValue: Double, maxValue: Double, gaugeColor: Color, gaugeGradient: Gradient) { + + var minValue: Double = lowLimitInMgDl + var maxValue: Double = highLimitInMgDl + var gaugeColor: Color = .green + var gaugeGradient: Gradient = Gradient(colors: [.yellow, .green, .green, .green, .green, .green, .green, .green, .green, .yellow]) + + if let bgValueInMgDl = bgValueInMgDl { + if bgValueInMgDl >= urgentHighLimitInMgDl || bgValueInMgDl <= urgentLowLimitInMgDl { + minValue = 39 + maxValue = 400 + gaugeColor = .red + gaugeGradient = Gradient(colors: [.red, .red, .red, .yellow, .yellow, .green, .yellow, .yellow, .red, .red, .red,]) + } else if bgValueInMgDl >= highLimitInMgDl || bgValueInMgDl <= lowLimitInMgDl { + minValue = urgentLowLimitInMgDl + maxValue = urgentHighLimitInMgDl + gaugeColor = .yellow + gaugeGradient = Gradient(colors: [.red, .yellow, .green, .green, .green, .green, .yellow, .red]) + } + } + return (minValue, maxValue, gaugeColor, gaugeGradient) + } + + func isSmallScreen() -> Bool { + return (WKInterfaceDevice.current().screenBounds.size.width < ConstantsAppleWatch.pixelWidthLimitForSmallScreen) ? true : false + } + + func overrideChartHeight() -> Double { + var height = isSmallScreen() ? ConstantsGlucoseChartSwiftUI.viewHeightWatchAccessoryRectangularSmall : ConstantsGlucoseChartSwiftUI.viewHeightWatchAccessoryRectangular + + height += disableComplications ? -15 : 0 + + return height + } + + func overrideChartWidth() -> Double { + return isSmallScreen() ? ConstantsGlucoseChartSwiftUI.viewWidthWatchAccessoryRectangularSmall : ConstantsGlucoseChartSwiftUI.viewWidthWatchAccessoryRectangular + } + + } +} + +// MARK: - Data + +extension XDripWatchComplication.Entry { + static var placeholder: Self { + .init(date: .now, widgetState: WidgetState(bgReadingValues: [100], bgReadingDates: [Date()], isMgDl: true, slopeOrdinal: 3, deltaChangeInMgDl: 0, urgentLowLimitInMgDl: 60, lowLimitInMgDl: 80, highLimitInMgDl: 140, urgentHighLimitInMgDl: 180, disableComplications: true)) + } +} diff --git a/xDrip Watch Complication/XDripWatchComplication+EntryView.swift b/xDrip Watch Complication/XDripWatchComplication+EntryView.swift new file mode 100644 index 00000000..282b27bd --- /dev/null +++ b/xDrip Watch Complication/XDripWatchComplication+EntryView.swift @@ -0,0 +1,35 @@ +// +// XDripWatchComplication+EntryView.swift +// xDrip Watch Complication Extension +// +// Created by Paul Plant on 28/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import SwiftUI +import Foundation + +extension XDripWatchComplication { + // main complication view body + struct EntryView : View { + // get the widget's family so that we can show the correct view + @Environment(\.widgetFamily) private var widgetFamily + + var entry: Entry + + var body: some View { + switch widgetFamily { + case .accessoryRectangular: + accessoryRectangularView + case .accessoryCircular: + accessoryCircularView + case .accessoryCorner: + accessoryCornerView + case .accessoryInline: + accessoryInlineView + default: + Image("AppIcon") + } + } + } +} diff --git a/xDrip Watch Complication/XDripWatchComplication+Provider.swift b/xDrip Watch Complication/XDripWatchComplication+Provider.swift new file mode 100644 index 00000000..395d068c --- /dev/null +++ b/xDrip Watch Complication/XDripWatchComplication+Provider.swift @@ -0,0 +1,103 @@ +// +// XDripWatchComplication+Provider.swift +// xDrip Watch Complication Extension +// +// Created by Paul Plant on 28/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import SwiftUI +import WidgetKit +import Foundation + +extension XDripWatchComplication { + struct Provider: TimelineProvider { + + func placeholder(in context: Context) -> Entry { + .placeholder + } + + func getSnapshot(in context: Context, completion: @escaping (Entry) -> ()) { + completion(.placeholder) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { + let entry = Entry(date: .now, widgetState: getWidgetStateFromSharedUserDefaults() ?? sampleWidgetStateFromProvider) + + completion(.init(entries: [entry], policy: .never)) + } + } +} + + +// MARK: - Helpers + +extension XDripWatchComplication.Provider { + func getWidgetStateFromSharedUserDefaults() -> XDripWatchComplication.Entry.WidgetState? { + guard let sharedUserDefaults = UserDefaults(suiteName: Bundle.main.appGroupSuiteName) else {return nil} + + guard let encodedLatestReadings = sharedUserDefaults.data(forKey: "complicationSharedUserDefaults.\(Bundle.main.mainAppBundleIdentifier)") else { + return nil + } + + let decoder = JSONDecoder() + + do { + let data = try decoder.decode(ComplicationSharedUserDefaultsModel.self, from: encodedLatestReadings) + + // because dates aren't Codable we stored them as doubles + // we need to convert the bgReadingDatesAsDouble key values to an array of real dates + let bgReadingDates: [Date] = data.bgReadingDatesAsDouble.map { date in + Date(timeIntervalSince1970: date) + } + + return Entry.WidgetState(bgReadingValues: data.bgReadingValues, bgReadingDates: bgReadingDates, isMgDl: data.isMgDl, slopeOrdinal: data.slopeOrdinal, deltaChangeInMgDl: data.deltaChangeInMgDl, urgentLowLimitInMgDl: data.urgentLowLimitInMgDl, lowLimitInMgDl: data.lowLimitInMgDl, highLimitInMgDl: data.highLimitInMgDl, urgentHighLimitInMgDl: data.urgentHighLimitInMgDl, disableComplications: data.disableComplications) + } catch { + print(error.localizedDescription) + } + + return sampleWidgetStateFromProvider + } + + private var sampleWidgetStateFromProvider: XDripWatchComplication.Entry.WidgetState { + + func bgDateArray() -> [Date] { + let endDate = Date() + let startDate = endDate.addingTimeInterval(-3600 * 12) + var currentDate = startDate + + var dateArray: [Date] = [] + + while currentDate < endDate { + dateArray.append(currentDate) + currentDate = currentDate.addingTimeInterval(60 * 5) + } + return dateArray + } + + func bgValueArray() -> [Double] { + + var bgValueArray:[Double] = Array(repeating: 0, count: 144) + var currentValue: Double = 100 + var increaseValues: Bool = true + + for index in bgValueArray.indices { + let randomValue = Double(Int.random(in: -10..<10)) + + if currentValue < 75 { + increaseValues = true + bgValueArray[index] = currentValue + abs(randomValue) + } else if currentValue > 150 { + increaseValues = false + bgValueArray[index] = currentValue - abs(randomValue) + } else { + bgValueArray[index] = currentValue + (increaseValues ? randomValue : -randomValue) + } + currentValue = bgValueArray[index] + } + return bgValueArray + } + + return Entry.WidgetState(bgReadingValues: bgValueArray(), bgReadingDates: bgDateArray(), isMgDl: true, slopeOrdinal: 3, deltaChangeInMgDl: 0, urgentLowLimitInMgDl: ConstantsBGGraphBuilder.defaultUrgentLowMarkInMgdl, lowLimitInMgDl: ConstantsBGGraphBuilder.defaultLowMarkInMgdl, highLimitInMgDl: ConstantsBGGraphBuilder.defaultHighMarkInMgdl, urgentHighLimitInMgDl: ConstantsBGGraphBuilder.defaultUrgentHighMarkInMgdl) + } +} diff --git a/xDrip Watch Complication/XDripWatchComplication.swift b/xDrip Watch Complication/XDripWatchComplication.swift new file mode 100644 index 00000000..ca934759 --- /dev/null +++ b/xDrip Watch Complication/XDripWatchComplication.swift @@ -0,0 +1,30 @@ +// +// xDripWatchComplication.swift +// xDrip Watch Complication +// +// Created by Paul Plant on 23/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import WidgetKit +import SwiftUI + +@main +struct XDripWatchComplication: Widget { + let kind: String = "xDripWatchComplication" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + XDripWatchComplication.EntryView(entry: entry) + } + .configurationDisplayName(ConstantsHomeView.applicationName) + .description("Show the current blood glucose level") + } +} + +@available(watchOS 10.0, *) +#Preview(as: .accessoryRectangular) { + XDripWatchComplication() +} timeline: { + XDripWatchComplication.Entry.placeholder +} diff --git a/xDrip Widget Extension.entitlements b/xDrip Widget Extension.entitlements new file mode 100644 index 00000000..96f2950d --- /dev/null +++ b/xDrip Widget Extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup + + + diff --git a/xDrip Widget/Assets.xcassets/AccentColor.colorset/Contents.json b/xDrip Widget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/xDrip Widget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/xDrip Widget/Assets.xcassets/AppIcon.appiconset/Contents.json b/xDrip Widget/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..3519b159 --- /dev/null +++ b/xDrip Widget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Icon-1025.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/xDrip Widget/Assets.xcassets/AppIcon.appiconset/Icon-1025.png b/xDrip Widget/Assets.xcassets/AppIcon.appiconset/Icon-1025.png new file mode 100644 index 00000000..2d1aa91b Binary files /dev/null and b/xDrip Widget/Assets.xcassets/AppIcon.appiconset/Icon-1025.png differ diff --git a/xDrip Widget/Assets.xcassets/Contents.json b/xDrip Widget/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/xDrip Widget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/xDrip Widget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/xDrip Widget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/xDrip Widget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/xDrip Widget/DataModels/XDripWidgetAttributes.swift b/xDrip Widget/DataModels/XDripWidgetAttributes.swift new file mode 100644 index 00000000..2ea7e058 --- /dev/null +++ b/xDrip Widget/DataModels/XDripWidgetAttributes.swift @@ -0,0 +1,155 @@ +// +// XDripWidgetAttributes.swift +// xDripWidgetExtension +// +// Created by Paul Plant on 30/12/23. +// Copyright © 2023 Johan Degraeve. All rights reserved. +// + +import ActivityKit +import WidgetKit +import SwiftUI + +struct XDripWidgetAttributes: ActivityAttributes { + + public struct ContentState: Codable, Hashable { + + // Store values with a 16 bit precision to save payload bytes + private var bgReadingFloats: [Float16] + // Expose those conveniently as Doubles + var bgReadingValues: [Double] { + bgReadingFloats.map(Double.init) + } + + // To save those precious payload bytes, store only the earliest date as Date + private var firstDate: Date + // ...and all other as seconds from that moment. + // No need for floating points, a second is precise enough for the graph + // UInt16 maximum value is 65535 so that means 18.2 hours. + // This would need to be changed if wishing to present a 24 hour chart. + private var secondsSinceFirstDate: [UInt16] + // Expose the dates conveniently + var bgReadingDates: [Date] { + secondsSinceFirstDate.map { Date(timeInterval: Double($0), since: firstDate) } + } + + var isMgDl: Bool + var slopeOrdinal: Int + var deltaChangeInMgDl: Double? + var urgentLowLimitInMgDl: Double + var lowLimitInMgDl: Double + var highLimitInMgDl: Double + var urgentHighLimitInMgDl: Double + var eventStartDate: Date = Date() + var warnUserToOpenApp: Bool = true + var liveActivitySize: LiveActivitySize + var dataSourceDescription: String + + var bgUnitString: String { + isMgDl ? Texts_Common.mgdl : Texts_Common.mmol + } + /// the latest bg reading + var bgValueInMgDl: Double? { + bgReadingValues[0] + } + /// the latest bg reading date + var bgReadingDate: Date? { + bgReadingDates[0] + } + + var bgValueStringInUserChosenUnit: String { + bgReadingValues[0].mgdlToMmolAndToString(mgdl: isMgDl) + } + + init(bgReadingValues: [Double], bgReadingDates: [Date], isMgDl: Bool, slopeOrdinal: Int, deltaChangeInMgDl: Double?, urgentLowLimitInMgDl: Double, lowLimitInMgDl: Double, highLimitInMgDl: Double, urgentHighLimitInMgDl: Double, liveActivitySize: LiveActivitySize, dataSourceDescription: String? = "") { + + self.bgReadingFloats = bgReadingValues.map(Float16.init) + + let firstDate = bgReadingDates.last ?? .now + self.firstDate = firstDate + self.secondsSinceFirstDate = bgReadingDates.map { UInt16(truncatingIfNeeded: Int($0.timeIntervalSince(firstDate))) } + + self.isMgDl = isMgDl + self.slopeOrdinal = slopeOrdinal + self.deltaChangeInMgDl = deltaChangeInMgDl + self.urgentLowLimitInMgDl = urgentLowLimitInMgDl + self.lowLimitInMgDl = lowLimitInMgDl + self.highLimitInMgDl = highLimitInMgDl + self.urgentHighLimitInMgDl = urgentHighLimitInMgDl + self.liveActivitySize = liveActivitySize + self.dataSourceDescription = dataSourceDescription ?? "" + } + + /// Blood glucose color dependant on the user defined limit values and based upon the time since the last reading + /// - Returns: a Color object either red, yellow or green + func bgTextColor() -> Color { + if let bgReadingDate = bgReadingDate, let bgValueInMgDl = bgValueInMgDl { + if bgReadingDate > Date().addingTimeInterval(-60 * 7) { + if bgValueInMgDl >= urgentHighLimitInMgDl || bgValueInMgDl <= urgentLowLimitInMgDl { + return Color(.red) + } else if bgValueInMgDl >= highLimitInMgDl || bgValueInMgDl <= lowLimitInMgDl { + return Color(.yellow) + } else { + return Color(.green) + } + } else { + return Color.gray + } + } else { + return Color.gray + } + } + + /// Delta text color dependant on the time since the last reading + /// - Returns: a Color either red, yellow or green + func deltaChangeTextColor() -> Color { + if let bgReadingDate = bgReadingDate, bgReadingDate > Date().addingTimeInterval(-60 * 7) { + return Color(white: 0.8) + } else { + return Color(.gray) + } + } + + /// convert the optional delta change int (in mg/dL) to a formatted change value in the user chosen unit making sure all zero values are shown as a positive change to follow Nightscout convention + /// - Returns: a string holding the formatted delta change value (i.e. +0.4 or -6) + func deltaChangeStringInUserChosenUnit() -> String { + if let deltaChangeInMgDl = deltaChangeInMgDl { + let deltaSign: String = deltaChangeInMgDl > 0 ? "+" : "" + let valueAsString = deltaChangeInMgDl.mgdlToMmolAndToString(mgdl: isMgDl) + + // quickly check "value" and prevent "-0mg/dl" or "-0.0mmol/l" being displayed + // show unitized zero deltas as +0 or +0.0 as per Nightscout format + if (isMgDl) { + return (deltaChangeInMgDl > -1 && deltaChangeInMgDl < 1) ? "+0" : (deltaSign + valueAsString) + } else { + return (deltaChangeInMgDl > -0.1 && deltaChangeInMgDl < 0.1) ? "+0.0" : (deltaSign + valueAsString) + } + } else { + return "" + } + } + + /// returns a string holding the trend arrow + /// - Returns: trend arrow string (i.e. "↑") + func trendArrow() -> String { + switch slopeOrdinal { + case 7: + return "\u{2193}\u{2193}" // ↓↓ + case 6: + return "\u{2193}" // ↓ + case 5: + return "\u{2198}" // ↘ + case 4: + return "\u{2192}" // → + case 3: + return "\u{2197}" // ↗ + case 2: + return "\u{2191}" // ↑ + case 1: + return "\u{2191}\u{2191}" // ↑↑ + default: + return "" + } + } + } +} diff --git a/xDrip Widget/Info.plist b/xDrip Widget/Info.plist new file mode 100644 index 00000000..b6743ec9 --- /dev/null +++ b/xDrip Widget/Info.plist @@ -0,0 +1,15 @@ + + + + + AppGroupIdentifier + $(APP_GROUP_IDENTIFIER) + MainAppBundleIdentifier + $(MAIN_APP_BUNDLE_IDENTIFIER) + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/xDrip Widget/Views/AccessoryCircularView.swift b/xDrip Widget/Views/AccessoryCircularView.swift new file mode 100644 index 00000000..750ef691 --- /dev/null +++ b/xDrip Widget/Views/AccessoryCircularView.swift @@ -0,0 +1,42 @@ +// +// AccessoryCircularView.swift +// xDrip Widget Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI +import WidgetKit + +extension XDripWidget.EntryView { + var accessoryCircularView: some View { + ZStack { + AccessoryWidgetBackground() + .cornerRadius(8) + + Gauge(value: entry.widgetState.bgValueInMgDl ?? 100, in: entry.widgetState.gaugeModel().minValue...entry.widgetState.gaugeModel().maxValue) { + Text("Not shown") + } currentValueLabel: { + Text(entry.widgetState.bgValueStringInUserChosenUnit) + .font(.system(size: 20)).bold() + .minimumScaleFactor(0.2) + .lineLimit(1) + } minimumValueLabel: { + Text(entry.widgetState.gaugeModel().minValue.mgdlToMmolAndToString(mgdl: entry.widgetState.isMgDl)) + .font(.system(size: 8)) + .foregroundStyle(Color(white: 0.7)) + .minimumScaleFactor(0.2) + } maximumValueLabel: { + Text(entry.widgetState.gaugeModel().maxValue.mgdlToMmolAndToString(mgdl: entry.widgetState.isMgDl)) + .font(.system(size: 8)) + .foregroundStyle(Color(white: 0.7)) + .minimumScaleFactor(0.2) + } + .gaugeStyle(.accessoryCircular) + .tint(entry.widgetState.gaugeModel().gaugeGradient) + } + .widgetBackground(backgroundView: Color.black) + } +} diff --git a/xDrip Widget/Views/AccessoryRectangularView.swift b/xDrip Widget/Views/AccessoryRectangularView.swift new file mode 100644 index 00000000..0bfcdf7f --- /dev/null +++ b/xDrip Widget/Views/AccessoryRectangularView.swift @@ -0,0 +1,27 @@ +// +// AccessoryRectangularView.swift +// xDrip Widget Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI +import WidgetKit + +extension XDripWidget.EntryView { + var accessoryRectangularView: some View { + ZStack { + AccessoryWidgetBackground() + .cornerRadius(8) + Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow())") + .font(.system(size: 50)).bold() + .minimumScaleFactor(0.2) + .foregroundStyle(Color(white: 1)) + .lineLimit(1) + } + .widgetBackground(backgroundView: Color.black) + } +} + diff --git a/xDrip Widget/Views/SystemLargeView.swift b/xDrip Widget/Views/SystemLargeView.swift new file mode 100644 index 00000000..e77546f7 --- /dev/null +++ b/xDrip Widget/Views/SystemLargeView.swift @@ -0,0 +1,62 @@ +// +// SystemLargeView.swift +// xDrip Widget Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI + +extension XDripWidget.EntryView { + var systemLargeView: some View { + VStack(spacing: 0) { + HStack(alignment: .center) { + Text("\(entry.widgetState.bgValueStringInUserChosenUnit) \(entry.widgetState.trendArrow())") + .font(.largeTitle).fontWeight(.bold) + .foregroundStyle(entry.widgetState.bgTextColor()) + .scaledToFill() + .minimumScaleFactor(0.5) + .lineLimit(1) + + Spacer() + + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(entry.widgetState.deltaChangeStringInUserChosenUnit()) + .font(.title).fontWeight(.semibold) + .foregroundStyle(entry.widgetState.deltaChangeTextColor()) + .lineLimit(1) + Text(entry.widgetState.bgUnitString) + .font(.title) + .foregroundStyle(.gray) + .lineLimit(1) + } + } + .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) + + HStack(alignment: .center) { + if let keepAliveImageString = entry.widgetState.keepAliveImageString { + Image(systemName: keepAliveImageString) + .font(.caption) + .foregroundStyle(Color(white: 0.6)) + .padding(.trailing, -4) + } + + Text(entry.widgetState.dataSourceDescription) + .font(.caption).bold() + .foregroundStyle(Color(white: 0.8)) + + Spacer() + + Text("Last reading at \(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")") + .font(.caption) + .foregroundStyle(Color(white: 0.6)) + } + } + .widgetBackground(backgroundView: Color.black) + } +} + diff --git a/xDrip Widget/Views/SystemMediumView.swift b/xDrip Widget/Views/SystemMediumView.swift new file mode 100644 index 00000000..0dbd9522 --- /dev/null +++ b/xDrip Widget/Views/SystemMediumView.swift @@ -0,0 +1,62 @@ +// +// SystemMediumView.swift +// xDrip Widget Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import SwiftUI + +extension XDripWidget.EntryView { + var systemMediumView: some View { + VStack(spacing: 0) { + HStack(alignment: .center) { + Text("\(entry.widgetState.bgValueStringInUserChosenUnit) \(entry.widgetState.trendArrow())") + .font(.title).fontWeight(.bold) + .foregroundStyle(entry.widgetState.bgTextColor()) + .scaledToFill() + .minimumScaleFactor(0.5) + .lineLimit(1) + + Spacer() + + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(entry.widgetState.deltaChangeStringInUserChosenUnit()) + .font(.title2).fontWeight(.semibold) + .foregroundStyle(entry.widgetState.deltaChangeTextColor()) + .lineLimit(1) + Text(entry.widgetState.bgUnitString) + .font(.title2) + .foregroundStyle(.gray) + .lineLimit(1) + } + } + .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) + + HStack(alignment: .center) { + if let keepAliveImageString = entry.widgetState.keepAliveImageString { + Image(systemName: keepAliveImageString) + .font(.caption) + .foregroundStyle(Color(white: 0.6)) + .padding(.trailing, -4) + } + + Text(entry.widgetState.dataSourceDescription) + .font(.caption).bold() + .foregroundStyle(Color(white: 0.8)) + + Spacer() + + Text("Last reading at \(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")") + .font(.caption) + .foregroundStyle(Color(white: 0.6)) + } + } + .widgetBackground(backgroundView: Color.black) + } +} diff --git a/xDrip Widget/Views/SystemSmallView.swift b/xDrip Widget/Views/SystemSmallView.swift new file mode 100644 index 00000000..0c5d8c26 --- /dev/null +++ b/xDrip Widget/Views/SystemSmallView.swift @@ -0,0 +1,56 @@ +// +// SystemSmallView.swift +// xDrip Widget Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +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) + + Spacer() + + 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) + .font(.caption) + .foregroundStyle(Color(white: 0.6)) + .padding(.trailing, -4) + } + + Text(entry.widgetState.dataSourceDescription) + .font(.system(size: 11)).bold() + .foregroundStyle(Color(white: 0.8)) + + Spacer() + + Text("\(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")") + .font(.system(size: 11)) + .foregroundStyle(Color(white: 0.6)) + } + } + .widgetBackground(backgroundView: Color.black) + } +} diff --git a/xDrip4iOS Widget/xDripClient/XDripClient.swift b/xDrip Widget/XDripClient.swift similarity index 100% rename from xDrip4iOS Widget/xDripClient/XDripClient.swift rename to xDrip Widget/XDripClient.swift diff --git a/xDrip Widget/XDripWidget+Entry.swift b/xDrip Widget/XDripWidget+Entry.swift new file mode 100644 index 00000000..ae4036db --- /dev/null +++ b/xDrip Widget/XDripWidget+Entry.swift @@ -0,0 +1,167 @@ +// +// XDripWidget+Entry.swift +// xDrip Widget Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import WidgetKit +import SwiftUI + +extension XDripWidget { + struct Entry: TimelineEntry { + var date: Date = .now + var widgetState: WidgetState + + } +} + +// MARK: - WidgetState + +extension XDripWidget.Entry { + + /// struct to hold the values that the widgets/complications should show + struct WidgetState { + var bgReadingValues: [Double]? + var bgReadingDates: [Date]? + var isMgDl: Bool + var slopeOrdinal: Int + var deltaChangeInMgDl: Double? + var urgentLowLimitInMgDl: Double + var lowLimitInMgDl: Double + var highLimitInMgDl: Double + var urgentHighLimitInMgDl: Double + var dataSourceDescription: String + var keepAliveImageString: String? + + var bgUnitString: String + var bgValueInMgDl: Double? + 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?) { + self.bgReadingValues = bgReadingValues + self.bgReadingDates = bgReadingDates + self.isMgDl = isMgDl ?? true + self.slopeOrdinal = slopeOrdinal ?? 0 + self.deltaChangeInMgDl = deltaChangeInMgDl + self.urgentLowLimitInMgDl = urgentLowLimitInMgDl ?? 60 + self.lowLimitInMgDl = lowLimitInMgDl ?? 80 + self.highLimitInMgDl = highLimitInMgDl ?? 180 + self.urgentHighLimitInMgDl = urgentHighLimitInMgDl ?? 250 + self.dataSourceDescription = dataSourceDescription ?? "" + self.keepAliveImageString = keepAliveImageString + + + self.bgValueInMgDl = (bgReadingValues?.count ?? 0) > 0 ? bgReadingValues?[0] : nil + self.bgReadingDate = (bgReadingDates?.count ?? 0) > 0 ? bgReadingDates?[0] : nil + self.bgUnitString = self.isMgDl ? Texts_Common.mgdl : Texts_Common.mmol + self.bgValueStringInUserChosenUnit = (bgReadingValues?.count ?? 0) > 0 ? bgReadingValues?[0].mgdlToMmolAndToString(mgdl: self.isMgDl) ?? "" : "" + } + + /// Blood glucose color dependant on the user defined limit values and based upon the time since the last reading + /// - Returns: a Color either red, yellow or green + func bgTextColor() -> Color { + if let bgReadingDate = bgReadingDate, bgReadingDate > Date().addingTimeInterval(-60 * 7), let bgValueInMgDl = bgValueInMgDl { + if bgValueInMgDl >= urgentHighLimitInMgDl || bgValueInMgDl <= urgentLowLimitInMgDl { + return Color(.red) + } else if bgValueInMgDl >= highLimitInMgDl || bgValueInMgDl <= lowLimitInMgDl { + return Color(.yellow) + } else { + return Color(.green) + } + } else { + return Color(.gray) + } + } + + /// Delta text color dependant on the time since the last reading + /// - Returns: a Color either red, yellow or green + func deltaChangeTextColor() -> Color { + if let bgReadingDate = bgReadingDate, bgReadingDate > Date().addingTimeInterval(-60 * 7) { + return Color(white: 0.8) + } else { + return Color(.gray) + } + } + + /// used to return values and colors used by a SwiftUI gauge view + /// - Returns: minValue/maxValue - used to define the limits of the gauge. gaugeColor/gaugeGradient - the gauge view will use one or the other + func gaugeModel() -> (minValue: Double, maxValue: Double, gaugeColor: Color, gaugeGradient: Gradient) { + + var minValue: Double = lowLimitInMgDl + var maxValue: Double = highLimitInMgDl + var gaugeColor: Color = .green + var gaugeGradient: Gradient = Gradient(colors: [.yellow, .green, .green, .green, .green, .green, .green, .green, .green, .yellow]) + + if let bgValueInMgDl = bgValueInMgDl { + if bgValueInMgDl >= urgentHighLimitInMgDl || bgValueInMgDl <= urgentLowLimitInMgDl { + minValue = 39 + maxValue = 400 + gaugeColor = .red + gaugeGradient = Gradient(colors: [.red, .red, .red, .yellow, .yellow, .green, .yellow, .yellow, .red, .red, .red,]) + } else if bgValueInMgDl >= highLimitInMgDl || bgValueInMgDl <= lowLimitInMgDl { + minValue = urgentLowLimitInMgDl + maxValue = urgentHighLimitInMgDl + gaugeColor = .yellow + gaugeGradient = Gradient(colors: [.red, .yellow, .green, .green, .green, .green, .yellow, .red]) + } + } + + return (minValue, maxValue, gaugeColor, gaugeGradient) + } + + /// convert the optional delta change int (in mg/dL) to a formatted change value in the user chosen unit making sure all zero values are shown as a positive change to follow Nightscout convention + /// - Returns: a string holding the formatted delta change value (i.e. +0.4 or -6) + func deltaChangeStringInUserChosenUnit() -> String { + if let deltaChangeInMgDl = deltaChangeInMgDl { + let deltaSign: String = deltaChangeInMgDl > 0 ? "+" : "" + let valueAsString = deltaChangeInMgDl.mgdlToMmolAndToString(mgdl: isMgDl) + + // quickly check "value" and prevent "-0mg/dl" or "-0.0mmol/l" being displayed + // show unitized zero deltas as +0 or +0.0 as per Nightscout format + if (isMgDl) { + return (deltaChangeInMgDl > -1 && deltaChangeInMgDl < 1) ? "+0" : (deltaSign + valueAsString) + } else { + return (deltaChangeInMgDl > -0.1 && deltaChangeInMgDl < 0.1) ? "+0.0" : (deltaSign + valueAsString) + } + } + return "" + } + + + /// returns a string holding the trend arrow + /// - Returns: trend arrow string (i.e. "↑") + func trendArrow() -> String { + switch slopeOrdinal { + case 7: + return "\u{2193}\u{2193}" // ↓↓ + case 6: + return "\u{2193}" // ↓ + case 5: + return "\u{2198}" // ↘ + case 4: + return "\u{2192}" // → + case 3: + return "\u{2197}" // ↗ + case 2: + return "\u{2191}" // ↑ + case 1: + return "\u{2191}\u{2191}" // ↑↑ + default: + return "" + } + } + + } +} + +// MARK: - Data + +extension XDripWidget.Entry { + static var placeholder: Self { + .init(date: .now, widgetState: WidgetState(bgReadingValues: [100], bgReadingDates: [Date()], isMgDl: true, slopeOrdinal: 4, deltaChangeInMgDl: 0, urgentLowLimitInMgDl: 60, lowLimitInMgDl: 80, highLimitInMgDl: 140, urgentHighLimitInMgDl: 180, dataSourceDescription: "Dexcom G6", keepAliveImageString: "circle")) + } +} diff --git a/xDrip Widget/XDripWidget+EntryView.swift b/xDrip Widget/XDripWidget+EntryView.swift new file mode 100644 index 00000000..1877627a --- /dev/null +++ b/xDrip Widget/XDripWidget+EntryView.swift @@ -0,0 +1,38 @@ +// +// XDripWidget+EntryView.swift +// xDrip Widget Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import SwiftUI +import Foundation + +extension XDripWidget { + // main complication view body + struct EntryView : View { + // get the widget's family so that we can show the correct view + @Environment(\.widgetFamily) private var widgetFamily + + var entry: Entry + + var body: some View { + switch widgetFamily { + case .systemSmall: + systemSmallView + case .systemMedium: + systemMediumView + case .systemLarge: + systemLargeView + case .accessoryCircular: + accessoryCircularView + case .accessoryRectangular: + accessoryRectangularView + default: + Text("No Data Available") + } + } + } +} + diff --git a/xDrip Widget/XDripWidget+Provider.swift b/xDrip Widget/XDripWidget+Provider.swift new file mode 100644 index 00000000..4407cbed --- /dev/null +++ b/xDrip Widget/XDripWidget+Provider.swift @@ -0,0 +1,104 @@ +// +// XDripWidget+Provider.swift +// xDrip Widget Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import SwiftUI +import WidgetKit +import Foundation + +extension XDripWidget { + struct Provider: TimelineProvider { + + func placeholder(in context: Context) -> Entry { + .placeholder + } + + func getSnapshot(in context: Context, completion: @escaping (Entry) -> ()) { + completion(.placeholder) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { + let entry = Entry(date: .now, widgetState: getWidgetStateFromSharedUserDefaults() ?? sampleWidgetStateFromProvider) + + completion(.init(entries: [entry], policy: .atEnd)) + } + } +} + + +// MARK: - Helpers + +extension XDripWidget.Provider { + func getWidgetStateFromSharedUserDefaults() -> XDripWidget.Entry.WidgetState? { + + guard let sharedUserDefaults = UserDefaults(suiteName: Bundle.main.appGroupSuiteName) else {return nil} + + guard let encodedLatestReadings = sharedUserDefaults.data(forKey: "widgetSharedUserDefaults.\(Bundle.main.mainAppBundleIdentifier)") else { + return nil + } + + let decoder = JSONDecoder() + + do { + let data = try decoder.decode(WidgetSharedUserDefaultsModel.self, from: encodedLatestReadings) + + // because dates aren't Codable we stored them as doubles + // we need to convert the bgReadingDatesAsDouble key values to an array of real dates + let bgReadingDates: [Date] = data.bgReadingDatesAsDouble.map { date in + Date(timeIntervalSince1970: date) + } + + return Entry.WidgetState(bgReadingValues: data.bgReadingValues, bgReadingDates: bgReadingDates, isMgDl: data.isMgDl, slopeOrdinal: data.slopeOrdinal, deltaChangeInMgDl: data.deltaChangeInMgDl, urgentLowLimitInMgDl: data.urgentLowLimitInMgDl, lowLimitInMgDl: data.lowLimitInMgDl, highLimitInMgDl: data.highLimitInMgDl, urgentHighLimitInMgDl: data.urgentHighLimitInMgDl, dataSourceDescription: data.dataSourceDescription, keepAliveImageString: data.keepAliveImageString) + } catch { + print(error.localizedDescription) + } + + return sampleWidgetStateFromProvider + } + + private var sampleWidgetStateFromProvider: XDripWidget.Entry.WidgetState { + func bgDateArray() -> [Date] { + let endDate = Date() + let startDate = endDate.addingTimeInterval(-3600 * 12) + var currentDate = startDate + + var dateArray: [Date] = [] + + while currentDate < endDate { + dateArray.append(currentDate) + currentDate = currentDate.addingTimeInterval(60 * 5) + } + return dateArray + } + + func bgValueArray() -> [Double] { + + var bgValueArray:[Double] = Array(repeating: 0, count: 144) + var currentValue: Double = 100 + var increaseValues: Bool = true + + for index in bgValueArray.indices { + let randomValue = Double(Int.random(in: -10..<10)) + + if currentValue < 75 { + increaseValues = true + bgValueArray[index] = currentValue + abs(randomValue) + } else if currentValue > 150 { + increaseValues = false + bgValueArray[index] = currentValue - abs(randomValue) + } else { + bgValueArray[index] = currentValue + (increaseValues ? randomValue : -randomValue) + } + currentValue = bgValueArray[index] + } + return bgValueArray + } + + return Entry.WidgetState(bgReadingValues: bgValueArray(), bgReadingDates: bgDateArray(), isMgDl: true, slopeOrdinal: 1, deltaChangeInMgDl: 0, urgentLowLimitInMgDl: ConstantsBGGraphBuilder.defaultUrgentLowMarkInMgdl, lowLimitInMgDl: ConstantsBGGraphBuilder.defaultLowMarkInMgdl, highLimitInMgDl: ConstantsBGGraphBuilder.defaultHighMarkInMgdl, urgentHighLimitInMgDl: ConstantsBGGraphBuilder.defaultUrgentHighMarkInMgdl, dataSourceDescription: "Dexcom G6", keepAliveImageString: nil) + } +} + diff --git a/xDrip Widget/XDripWidget.swift b/xDrip Widget/XDripWidget.swift new file mode 100644 index 00000000..4850597f --- /dev/null +++ b/xDrip Widget/XDripWidget.swift @@ -0,0 +1,46 @@ +// +// XDripWidget.swift +// XDripWidget +// +// Created by Paul Plant on 30/12/23. +// Copyright © 2023 Johan Degraeve. All rights reserved. +// + +import WidgetKit +import SwiftUI + +struct XDripWidget: Widget { + let kind: String = "xDripWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + XDripWidget.EntryView(entry: entry) + .widgetBackground(backgroundView: Color.black) + } + .configurationDisplayName(ConstantsHomeView.applicationName) + .description("Show the current blood glucose level") + .supportedFamilies([ + .systemSmall, + .systemMedium, + .systemLarge, + .accessoryCircular, + .accessoryRectangular + ]) + } +} + +struct XDripWidget_Previews: PreviewProvider { + static var previews: some View { + Group { + XDripWidget.EntryView(entry: .placeholder) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .previewDisplayName("systemSmall") + XDripWidget.EntryView(entry: .placeholder) + .previewContext(WidgetPreviewContext(family: .accessoryCircular)) + .previewDisplayName("accessoryCircular") + XDripWidget.EntryView(entry: .placeholder) + .previewContext(WidgetPreviewContext(family: .accessoryInline)) + .previewDisplayName("accessoryInline") + } + } +} diff --git a/xDrip Widget/XDripWidgetBundle.swift b/xDrip Widget/XDripWidgetBundle.swift new file mode 100644 index 00000000..966a0210 --- /dev/null +++ b/xDrip Widget/XDripWidgetBundle.swift @@ -0,0 +1,19 @@ +// +// xDripWidgetBundle.swift +// XDripWidget +// +// Created by Paul Plant on 30/12/23. +// Copyright © 2023 Johan Degraeve. All rights reserved. +// + +import WidgetKit +import SwiftUI + +@available(iOSApplicationExtension 16.2, *) +@main +struct XDripWidgetBundle: WidgetBundle { + var body: some Widget { + XDripWidget() + XDripWidgetLiveActivity() + } +} diff --git a/xDrip Widget/XDripWidgetLiveActivity.swift b/xDrip Widget/XDripWidgetLiveActivity.swift new file mode 100644 index 00000000..f69da5c3 --- /dev/null +++ b/xDrip Widget/XDripWidgetLiveActivity.swift @@ -0,0 +1,284 @@ +// +// XDripWidgetLiveActivity.swift +// XDripWidget +// +// Created by Paul Plant on 29/12/23. +// Copyright © 2023 Johan Degraeve. All rights reserved. +// + +import ActivityKit +import WidgetKit +import SwiftUI + +@available(iOSApplicationExtension 16.2, *) +struct XDripWidgetLiveActivity: Widget { + + var body: some WidgetConfiguration { + ActivityConfiguration(for: XDripWidgetAttributes.self) { context in + + if context.state.liveActivitySize == .minimal { + + // 1 = minimal widget with no chart + HStack(alignment: .center) { + Text("\(context.state.bgValueStringInUserChosenUnit) \(context.state.trendArrow())") + .font(.system(size: 35)).bold() + .foregroundStyle(context.state.bgTextColor()) + .minimumScaleFactor(0.1) + .lineLimit(1) + + Spacer() + + if context.state.warnUserToOpenApp { + Text("Open app...") + .font(.footnote).bold() + .foregroundStyle(.black) + .multilineTextAlignment(.center) + .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)) + .background(.cyan).opacity(0.9) + .cornerRadius(10) + + Spacer() + } + + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(context.state.deltaChangeStringInUserChosenUnit()) + .font(.title).fontWeight(.semibold) + .foregroundStyle(context.state.deltaChangeTextColor()) + .minimumScaleFactor(0.2) + .lineLimit(1) + + Text(context.state.bgUnitString) + .font(.title) + .foregroundStyle(Color(white: 0.5)) + .minimumScaleFactor(0.2) + .lineLimit(1) + } + } + .activityBackgroundTint(.black) + .padding([.top, .bottom], 0) + .padding([.leading, .trailing], 20) + + } else if context.state.liveActivitySize == .normal { + + // 0 = normal size chart + HStack(spacing: 30) { + VStack(spacing: 0) { + Text("\(context.state.bgValueStringInUserChosenUnit)\(context.state.trendArrow())") + .font(.system(size: 44)).bold() + .foregroundStyle(context.state.bgTextColor()) + .minimumScaleFactor(0.1) + .lineLimit(1) + + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(context.state.deltaChangeStringInUserChosenUnit()) + .font(.system(size: 20)).fontWeight(.semibold) + .foregroundStyle(context.state.deltaChangeTextColor()) + .minimumScaleFactor(0.2) + .lineLimit(1) + + Text(context.state.bgUnitString) + .font(.system(size: 20)) + .foregroundStyle(Color(white: 0.5)) + .minimumScaleFactor(0.2) + .lineLimit(1) + } + } + + 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) + + if context.state.warnUserToOpenApp { + VStack(alignment: .center) { + Spacer() + Text("Open \(ConstantsHomeView.applicationName)") + .font(.footnote).bold() + .foregroundStyle(.black) + .multilineTextAlignment(.center) + .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)) + .background(.cyan).opacity(0.9) + .cornerRadius(10) + Spacer() + } + .padding(8) + } + } + } + .activityBackgroundTint(.black) + .padding(.top, 10) + .padding(.bottom, 10) + + } else { + + // 3 = large chart is final default option + ZStack { + + VStack(spacing: 0) { + HStack(alignment: .center) { + Text("\(context.state.bgValueStringInUserChosenUnit) \(context.state.trendArrow())") + .font(.system(size: 32)).fontWeight(.bold) + .foregroundStyle(context.state.bgTextColor()) + .scaledToFill() + .minimumScaleFactor(0.5) + .lineLimit(1) + + Spacer() + + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(context.state.deltaChangeStringInUserChosenUnit()) + .font(.system(size: 28)).fontWeight(.semibold) + .foregroundStyle(context.state.deltaChangeTextColor()) + .lineLimit(1) + Text(context.state.bgUnitString) + .font(.system(size: 28)) + .foregroundStyle(.gray) + .lineLimit(1) + } + } + .padding(.top, 8) + .padding(.bottom, 2) + .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) + + HStack { + Text(context.state.dataSourceDescription) + .font(.caption).bold() + .foregroundStyle(Color(white: 0.8)) + + Spacer() + + Text("Last reading at \(context.state.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")") + .font(.caption) + .foregroundStyle(Color(white: 0.6)) + } + .padding(.top, 2) + .padding(.bottom, 10) + .padding(.leading, 15) + .padding(.trailing, 15) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(0) + + if context.state.warnUserToOpenApp { + VStack(alignment: .center) { + Text("Please open \(ConstantsHomeView.applicationName)") + .font(.footnote).bold() + .foregroundStyle(.black) + .multilineTextAlignment(.center) + .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)) + .background(.cyan).opacity(0.9) + .cornerRadius(10) + } + } + } + .activityBackgroundTint(.black) + } + + } dynamicIsland: { context in + DynamicIsland { + DynamicIslandExpandedRegion(.leading) { Text("\(context.state.bgValueStringInUserChosenUnit)\(context.state.trendArrow())") + .font(.largeTitle).bold() + .foregroundStyle(context.state.bgTextColor()) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .minimumScaleFactor(0.1) + .lineLimit(1) + } + DynamicIslandExpandedRegion(.trailing) { + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(context.state.deltaChangeStringInUserChosenUnit()) + .font(.title).fontWeight(.semibold) + .foregroundStyle(context.state.deltaChangeTextColor()) + .minimumScaleFactor(0.2) + .lineLimit(1) + + Text(context.state.bgUnitString) + .font(.title) + .foregroundStyle(Color(white: 0.5)) + .minimumScaleFactor(0.2) + .lineLimit(1) + } + .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) + } + } compactLeading: { + Text("\(context.state.bgValueStringInUserChosenUnit)\(context.state.trendArrow())") + .foregroundStyle(context.state.bgTextColor()) + .minimumScaleFactor(0.1) + } compactTrailing: { + Text(context.state.deltaChangeStringInUserChosenUnit()) + .minimumScaleFactor(0.1) + } minimal: { + Text("\(context.state.bgValueStringInUserChosenUnit)") + .foregroundStyle(context.state.bgTextColor()) + .minimumScaleFactor(0.1) + } + .widgetURL(URL(string: "xdripswift")) + .keylineTint(context.state.bgTextColor()) + } + } +} + +@available(iOS 16.2, *) +struct XDripWidgetLiveActivity_Previews: PreviewProvider { + + // generate some random dates for the preview + static func bgDateArray() -> [Date] { + let endDate = Date() + let startDate = endDate.addingTimeInterval(-3600 * 12) + var currentDate = startDate + + var dateArray: [Date] = [] + + while currentDate < endDate { + dateArray.append(currentDate) + currentDate = currentDate.addingTimeInterval(60 * 5) + } + return dateArray + } + + // generate some random bg values for the preview + static func bgValueArray() -> [Double] { + var bgValueArray:[Double] = Array(repeating: 0, count: 144) + var currentValue: Double = 100 + var increaseValues: Bool = true + + for index in bgValueArray.indices { + let randomValue = Double(Int.random(in: -10..<10)) + + if currentValue < 80 { + increaseValues = true + bgValueArray[index] = currentValue + abs(randomValue) + } else if currentValue > 160 { + increaseValues = false + bgValueArray[index] = currentValue - abs(randomValue) + } else { + bgValueArray[index] = currentValue + (increaseValues ? randomValue : -randomValue) + } + currentValue = bgValueArray[index] + } + return bgValueArray + } + + static let attributes = XDripWidgetAttributes() + + static let contentState = XDripWidgetAttributes.ContentState(bgReadingValues: bgValueArray(), bgReadingDates: bgDateArray(), isMgDl: true, slopeOrdinal: 5, deltaChangeInMgDl: -2, urgentLowLimitInMgDl: 70, lowLimitInMgDl: 80, highLimitInMgDl: 140, urgentHighLimitInMgDl: 180, liveActivitySize: .large, dataSourceDescription: "Dexcom G6") + + static var previews: some View { + attributes + .previewContext(contentState, viewKind: .content) + .previewDisplayName("Notification") + attributes + .previewContext(contentState, viewKind: .dynamicIsland(.compact)) + .previewDisplayName("Compact") + attributes + .previewContext(contentState, viewKind: .dynamicIsland(.expanded)) + .previewDisplayName("Expanded") + attributes + .previewContext(contentState, viewKind: .dynamicIsland(.minimal)) + .previewDisplayName("Minimal") + } +} + diff --git a/xDrip-Watch-App-Info.plist b/xDrip-Watch-App-Info.plist new file mode 100644 index 00000000..18311267 --- /dev/null +++ b/xDrip-Watch-App-Info.plist @@ -0,0 +1,10 @@ + + + + + AppGroupIdentifier + $(APP_GROUP_IDENTIFIER) + MainAppBundleIdentifier + $(MAIN_APP_BUNDLE_IDENTIFIER) + + diff --git a/xDrip4iOS Widget/Base.lproj/MainInterface.storyboard b/xDrip4iOS Widget/Base.lproj/MainInterface.storyboard deleted file mode 100644 index b9410627..00000000 --- a/xDrip4iOS Widget/Base.lproj/MainInterface.storyboard +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/xDrip4iOS Widget/Info.plist b/xDrip4iOS Widget/Info.plist deleted file mode 100644 index 5e036edb..00000000 --- a/xDrip4iOS Widget/Info.plist +++ /dev/null @@ -1,33 +0,0 @@ - - - - - AppGroupIdentifier - $(APP_GROUP_IDENTIFIER) - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - $(MAIN_APP_DISPLAY_NAME) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSExtension - - NSExtensionMainStoryboard - MainInterface - NSExtensionPointIdentifier - com.apple.widget-extension - - - diff --git a/xDrip4iOS Widget/Texts/Texts.swift b/xDrip4iOS Widget/Texts/Texts.swift deleted file mode 100644 index 7f82c6da..00000000 --- a/xDrip4iOS Widget/Texts/Texts.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -// all common texts -class Texts { - static private let filename = "Common" - - static let minutes = { - return NSLocalizedString("common_minutes", tableName: filename, bundle: Bundle.main, value: "mins", comment: "literal translation needed") - }() - - static let minute = { - return NSLocalizedString("common_minute", tableName: filename, bundle: Bundle.main, value: "min", comment: "literal translation needed") - }() - - static let HIGH = { - return NSLocalizedString("common_high", tableName: filename, bundle: Bundle.main, value: "HIGH", comment: "the word HIGH, in capitals") - }() - - static let LOW = { - return NSLocalizedString("common_low", tableName: filename, bundle: Bundle.main, value: "LOW", comment: "the word LOW, in capitals") - }() - - static let mgdl: String = { - return NSLocalizedString("common_mgdl", tableName: filename, bundle: Bundle.main, value: "mg/dL", comment: "mg/dL") - }() - - static let mmol: String = { - return NSLocalizedString("common_mmol", tableName: filename, bundle: Bundle.main, value: "mmol/L", comment: "mmol/L") - }() - - static let ago:String = { - return NSLocalizedString("ago", tableName: filename, bundle: Bundle.main, value: "ago", comment: "where it say how old the reading is, 'x minutes ago', literaly translation of 'ago'") - }() - -} diff --git a/xDrip4iOS Widget/Texts/ar.lproj/Common.strings b/xDrip4iOS Widget/Texts/ar.lproj/Common.strings deleted file mode 100644 index 20787281..00000000 --- a/xDrip4iOS Widget/Texts/ar.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "مرتفع"; -"common_low" = "منخفض"; -"common_minutes" = "دقائق"; -"common_minute" = "دقيقة"; -"ago" = "مضت"; diff --git a/xDrip4iOS Widget/Texts/da.lproj/Common.strings b/xDrip4iOS Widget/Texts/da.lproj/Common.strings deleted file mode 100644 index 6dc05e6b..00000000 --- a/xDrip4iOS Widget/Texts/da.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "HIGH"; -"common_low" = "LOW"; -"common_minutes" = "mins"; -"common_minute" = "min"; -"ago" = "ago"; diff --git a/xDrip4iOS Widget/Texts/de.lproj/Common.strings b/xDrip4iOS Widget/Texts/de.lproj/Common.strings deleted file mode 100644 index bbe1485f..00000000 --- a/xDrip4iOS Widget/Texts/de.lproj/Common.strings +++ /dev/null @@ -1,20 +0,0 @@ -/// literal translation needed -"common_minute" = "min"; - -/// the word HIGH, in capitals -"common_high" = "HOCH"; - -/// the word LOW, in capitals -"common_low" = "NIEDRIG"; - -/// literal translation needed -"common_minutes" = "Minuten"; - -/// mmol/l -"common_mmol" = "mmol/L"; - -/// mg/dl -"common_mgdl" = "mg/dL"; - -/// where it say how old the reading is, 'x minutes ago', literaly translation of 'ago' -"ago" = "her"; diff --git a/xDrip4iOS Widget/Texts/el.lproj/Common.strings b/xDrip4iOS Widget/Texts/el.lproj/Common.strings deleted file mode 100644 index 15c1242b..00000000 --- a/xDrip4iOS Widget/Texts/el.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "ΥΨΗΛΗ"; -"common_low" = "ΧΑΜΗΛΗ"; -"common_minutes" = "mins"; -"common_minute" = "min"; -"ago" = "πριν"; diff --git a/xDrip4iOS Widget/Texts/en.lproj/Common.strings b/xDrip4iOS Widget/Texts/en.lproj/Common.strings deleted file mode 100644 index 6dc05e6b..00000000 --- a/xDrip4iOS Widget/Texts/en.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "HIGH"; -"common_low" = "LOW"; -"common_minutes" = "mins"; -"common_minute" = "min"; -"ago" = "ago"; diff --git a/xDrip4iOS Widget/Texts/es-ES.lproj/Common.strings b/xDrip4iOS Widget/Texts/es-ES.lproj/Common.strings deleted file mode 100644 index 0acbe1de..00000000 --- a/xDrip4iOS Widget/Texts/es-ES.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dl"; -"common_mmol" = "mmol/l"; -"common_high" = "ALTA"; -"common_low" = "BAJA"; -"common_minutes" = "mins"; -"common_minute" = "min"; -"ago" = "atrás"; diff --git a/xDrip4iOS Widget/Texts/es-MX.lproj/Common.strings b/xDrip4iOS Widget/Texts/es-MX.lproj/Common.strings deleted file mode 100644 index 0acbe1de..00000000 --- a/xDrip4iOS Widget/Texts/es-MX.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dl"; -"common_mmol" = "mmol/l"; -"common_high" = "ALTA"; -"common_low" = "BAJA"; -"common_minutes" = "mins"; -"common_minute" = "min"; -"ago" = "atrás"; diff --git a/xDrip4iOS Widget/Texts/es.lproj/Common.strings b/xDrip4iOS Widget/Texts/es.lproj/Common.strings deleted file mode 100644 index 6dc05e6b..00000000 --- a/xDrip4iOS Widget/Texts/es.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "HIGH"; -"common_low" = "LOW"; -"common_minutes" = "mins"; -"common_minute" = "min"; -"ago" = "ago"; diff --git a/xDrip4iOS Widget/Texts/fi.lproj/Common.strings b/xDrip4iOS Widget/Texts/fi.lproj/Common.strings deleted file mode 100644 index 23243221..00000000 --- a/xDrip4iOS Widget/Texts/fi.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "KORKEA"; -"common_low" = "MATALA"; -"common_minutes" = "minuuttia"; -"common_minute" = "min"; -"ago" = "sitten"; diff --git a/xDrip4iOS Widget/Texts/fr.lproj/Common.strings b/xDrip4iOS Widget/Texts/fr.lproj/Common.strings deleted file mode 100644 index d174c0a0..00000000 --- a/xDrip4iOS Widget/Texts/fr.lproj/Common.strings +++ /dev/null @@ -1,8 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "HAUT"; -"common_low" = "BAS"; -"common_minutes" = "mins"; -"common_minute" = "min"; -"ago" = "depuis"; - diff --git a/xDrip4iOS Widget/Texts/it.lproj/Common.strings b/xDrip4iOS Widget/Texts/it.lproj/Common.strings deleted file mode 100644 index 7d04c8ec..00000000 --- a/xDrip4iOS Widget/Texts/it.lproj/Common.strings +++ /dev/null @@ -1,25 +0,0 @@ - -///////////////////////////////////////////////////////////////////////////////////////////// -///// Translation needed - remove this header after translation ///// -///////////////////////////////////////////////////////////////////////////////////////////// - -/// literal translation needed -"common_minute" = "min"; - -/// the word HIGH, in capitals -"common_high" = "HIGH"; - -/// the word LOW, in capitals -"common_low" = "LOW"; - -/// literal translation needed -"common_minutes" = "mins"; - -/// mmol/l -"common_mmol" = "mmol/L"; - -/// mg/dl -"common_mgdl" = "mg/dL"; - -/// where it say how old the reading is, 'x minutes ago', literaly translation of 'ago' -"ago" = "ago"; diff --git a/xDrip4iOS Widget/Texts/nl.lproj/Common.strings b/xDrip4iOS Widget/Texts/nl.lproj/Common.strings deleted file mode 100644 index 239bff2f..00000000 --- a/xDrip4iOS Widget/Texts/nl.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_high" = "HOOG"; -"common_low" = "LAAG"; -"common_minute" = "min"; -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_minutes" = "min"; -"ago" = "geleden"; diff --git a/xDrip4iOS Widget/Texts/pl-PL.lproj/Common.strings b/xDrip4iOS Widget/Texts/pl-PL.lproj/Common.strings deleted file mode 100644 index 7d04c8ec..00000000 --- a/xDrip4iOS Widget/Texts/pl-PL.lproj/Common.strings +++ /dev/null @@ -1,25 +0,0 @@ - -///////////////////////////////////////////////////////////////////////////////////////////// -///// Translation needed - remove this header after translation ///// -///////////////////////////////////////////////////////////////////////////////////////////// - -/// literal translation needed -"common_minute" = "min"; - -/// the word HIGH, in capitals -"common_high" = "HIGH"; - -/// the word LOW, in capitals -"common_low" = "LOW"; - -/// literal translation needed -"common_minutes" = "mins"; - -/// mmol/l -"common_mmol" = "mmol/L"; - -/// mg/dl -"common_mgdl" = "mg/dL"; - -/// where it say how old the reading is, 'x minutes ago', literaly translation of 'ago' -"ago" = "ago"; diff --git a/xDrip4iOS Widget/Texts/pt.lproj/Common.strings b/xDrip4iOS Widget/Texts/pt.lproj/Common.strings deleted file mode 100644 index 84c386d4..00000000 --- a/xDrip4iOS Widget/Texts/pt.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "ALTA"; -"common_low" = "BAIXA"; -"common_minutes" = "mins"; -"common_minute" = "min"; -"ago" = "ago"; diff --git a/xDrip4iOS Widget/Texts/ru.lproj/Common.strings b/xDrip4iOS Widget/Texts/ru.lproj/Common.strings deleted file mode 100644 index d53ec235..00000000 --- a/xDrip4iOS Widget/Texts/ru.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "ВЫСОКИЙ"; -"common_low" = "НИЗКИЙ"; -"common_minutes" = "мин"; -"common_minute" = "мин"; -"ago" = "назад"; diff --git a/xDrip4iOS Widget/Texts/sl.lproj/Common.strings b/xDrip4iOS Widget/Texts/sl.lproj/Common.strings deleted file mode 100644 index 7d04c8ec..00000000 --- a/xDrip4iOS Widget/Texts/sl.lproj/Common.strings +++ /dev/null @@ -1,25 +0,0 @@ - -///////////////////////////////////////////////////////////////////////////////////////////// -///// Translation needed - remove this header after translation ///// -///////////////////////////////////////////////////////////////////////////////////////////// - -/// literal translation needed -"common_minute" = "min"; - -/// the word HIGH, in capitals -"common_high" = "HIGH"; - -/// the word LOW, in capitals -"common_low" = "LOW"; - -/// literal translation needed -"common_minutes" = "mins"; - -/// mmol/l -"common_mmol" = "mmol/L"; - -/// mg/dl -"common_mgdl" = "mg/dL"; - -/// where it say how old the reading is, 'x minutes ago', literaly translation of 'ago' -"ago" = "ago"; diff --git a/xDrip4iOS Widget/Texts/sv.lproj/Common.strings b/xDrip4iOS Widget/Texts/sv.lproj/Common.strings deleted file mode 100644 index 54ca6e4a..00000000 --- a/xDrip4iOS Widget/Texts/sv.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "HÖGT"; -"common_low" = "LÅGT"; -"common_minutes" = "min"; -"common_minute" = "min"; -"ago" = "sedan"; diff --git a/xDrip4iOS Widget/Texts/tr.lproj/Common.strings b/xDrip4iOS Widget/Texts/tr.lproj/Common.strings deleted file mode 100644 index 48a06109..00000000 --- a/xDrip4iOS Widget/Texts/tr.lproj/Common.strings +++ /dev/null @@ -1,20 +0,0 @@ -/// literal translation needed -"common_minute" = "min"; - -/// the word HIGH, in capitals -"common_high" = "YÜKSEK"; - -/// the word LOW, in capitals -"common_low" = "DÜŞÜK"; - -/// literal translation needed -"common_minutes" = "Dakika"; - -/// mmol/l -"common_mmol" = "mmol/l"; - -/// mg/dl -"common_mgdl" = "mg/dl"; - -/// where it say how old the reading is, 'x minutes ago', literaly translation of 'ago' -"ago" = "dakika önce"; diff --git a/xDrip4iOS Widget/Texts/uk.lproj/Common.strings b/xDrip4iOS Widget/Texts/uk.lproj/Common.strings deleted file mode 100644 index 6dc05e6b..00000000 --- a/xDrip4iOS Widget/Texts/uk.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "HIGH"; -"common_low" = "LOW"; -"common_minutes" = "mins"; -"common_minute" = "min"; -"ago" = "ago"; diff --git a/xDrip4iOS Widget/Texts/zh.lproj/Common.strings b/xDrip4iOS Widget/Texts/zh.lproj/Common.strings deleted file mode 100644 index d8de330e..00000000 --- a/xDrip4iOS Widget/Texts/zh.lproj/Common.strings +++ /dev/null @@ -1,7 +0,0 @@ -"common_mgdl" = "mg/dL"; -"common_mmol" = "mmol/L"; -"common_high" = "高"; -"common_low" = "低"; -"common_minutes" = "分钟"; -"common_minute" = "分钟"; -"ago" = "之前"; diff --git a/xDrip4iOS Widget/TodayViewController.swift b/xDrip4iOS Widget/TodayViewController.swift deleted file mode 100644 index fc755944..00000000 --- a/xDrip4iOS Widget/TodayViewController.swift +++ /dev/null @@ -1,365 +0,0 @@ -import Foundation -import NotificationCenter -import os - -class TodayViewController: UIViewController, NCWidgetProviding { - - // MARK: - Properties - Outlets and Actions - - - @IBOutlet weak var minutesLabelOutlet: UILabel! - @IBOutlet weak var minutesAgoLabelOutlet: UILabel! - - @IBOutlet weak var diffLabelOutlet: UILabel! - @IBOutlet weak var diffLabelUnitOutlet: UILabel! - - @IBOutlet weak var valueLabelOutlet: UILabel! - - - - // MARK: - private properties - - /// xDripClient - private var xDripClient: XDripClient = XDripClient() - - // MARK: - overriden functions - - override func viewDidLoad() { - - super.viewDidLoad() - - setupView() - - } - - func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) { - // Perform any setup necessary in order to update the view. - - // If an error is encountered, use NCUpdateResult.Failed - // If there's no update required, use NCUpdateResult.NoData - // If there's an update, use NCUpdateResult.NewData - - xDripClient.fetchLast(2, callback: { (error, glucoseArray) in - - if error != nil { - return - } - - guard let glucoseArray = glucoseArray, glucoseArray.count > 0 else { - return - } - - self.updateLabels(latestReadings: glucoseArray) - - }) - - completionHandler(NCUpdateResult.newData) - - } - - // MARK: - private functions - - /// setup colors and so - private func setupView() { - - // set background color to black - self.view.backgroundColor = UIColor.black - - // set minutesLabelOutlet.textColor to white - self.minutesLabelOutlet.textColor = UIColor.white - - // set diffLabelOutlet.textColor to white - self.diffLabelOutlet.textColor = UIColor.white - - self.minutesAgoLabelOutlet.textColor = UIColor.lightGray - - self.diffLabelUnitOutlet.textColor = UIColor.lightGray - - } - - /// - updates the labels - private func updateLabels(latestReadings: [Glucose]) { - - // unwrap shared userdefaults - guard let sharedUserDefaults = xDripClient.shared else {return} - - // unwrap bloodGlucoseUnitIsMgDl in userdefaults - //default value for bool in userdefaults is false, false is for mgdl, true is for mmol - let bloodGlucoseUnitIsMgDl = !sharedUserDefaults.bool(forKey: "bloodGlucoseUnit") - - // get urgentLowMarkValueInUserChosenUnit - let urgentLowMarkValueInUserChosenUnit = getMarkValueInUserChosenUnit(forKey: "urgentLowMarkValue", bloodGlucoseUnitIsMgDl: bloodGlucoseUnitIsMgDl, withDefaultValue: ConstantsBGGraphBuilder.defaultUrgentLowMarkInMgdl) - - // get urgentHighMarkValueInUserChosenUnit - let urgentHighMarkValueInUserChosenUnit = getMarkValueInUserChosenUnit(forKey: "urgentHighMarkValue", bloodGlucoseUnitIsMgDl: bloodGlucoseUnitIsMgDl, withDefaultValue: ConstantsBGGraphBuilder.defaultUrgentHighMarkInMgdl) - - // get highMarkValueInUserChoenUnit - let highMarkValueInUserChosenUnit = getMarkValueInUserChosenUnit(forKey: "highMarkValue", bloodGlucoseUnitIsMgDl: bloodGlucoseUnitIsMgDl, withDefaultValue: ConstantsBGGraphBuilder.defaultHighMarkInMgdl) - - // get lowMarkValueInUserChosenUnit - let lowMarkValueInUserChosenUnit = getMarkValueInUserChosenUnit(forKey: "lowMarkValue", bloodGlucoseUnitIsMgDl: bloodGlucoseUnitIsMgDl, withDefaultValue: ConstantsBGGraphBuilder.defaultLowMarkInMgdl) - - // if there's no readings, then give empty fields - guard latestReadings.count > 0 else { - - valueLabelOutlet.textColor = UIColor.darkGray - minutesLabelOutlet.text = "" - minutesAgoLabelOutlet.text = "" - diffLabelOutlet.text = "" - diffLabelUnitOutlet.text = "" - - let attributeString: NSMutableAttributedString = NSMutableAttributedString(string: "---") - attributeString.addAttribute(.strikethroughStyle, value: 0, range: NSMakeRange(0, attributeString.length)) - - valueLabelOutlet.attributedText = attributeString - - return - } - - // assign last reading - let lastReading = latestReadings[0] - - // assign last but one reading - let lastButOneReading = latestReadings.count > 1 ? latestReadings[1]:nil - - // start creating text for valueLabelOutlet, first the calculated value - var calculatedValueAsString = unitizedString(calculatedValue: Double(lastReading.glucose), unitIsMgDl: bloodGlucoseUnitIsMgDl) - - // if latestReading is older than 11 minutes, then it should be strikethrough - if lastReading.timestamp < Date(timeIntervalSinceNow: -60 * 11) { - - let attributeString: NSMutableAttributedString = NSMutableAttributedString(string: calculatedValueAsString) - attributeString.addAttribute(.strikethroughStyle, value: 2, range: NSMakeRange(0, attributeString.length)) - - valueLabelOutlet.attributedText = attributeString - - } else { - - // if lastButOneReading is available and is less than maxSlopeInMinutes earlier than lastReading, then show slopeArrow - if let lastButOneReading = lastButOneReading { - - if lastReading.timestamp.timeIntervalSince(lastButOneReading.timestamp) <= Double(ConstantsBGGraphBuilder.maxSlopeInMinutes * 60) { - - // don't show delta if there are not enough values or the values are more than 20 mintes apart - calculatedValueAsString = calculatedValueAsString + " " + slopeArrow(slopeOrdinal: lastReading.trend) - - } - - } - - // no strikethrough needed, but attributedText may still be set to strikethrough from previous period during which there was no recent reading. - let attributeString: NSMutableAttributedString = NSMutableAttributedString(string: calculatedValueAsString) - attributeString.addAttribute(.strikethroughStyle, value: 0, range: NSMakeRange(0, attributeString.length)) - - valueLabelOutlet.attributedText = attributeString - - } - - // if data is stale (over 11 minutes old), show it as gray colour to indicate that it isn't current - // if not, then set color, depending on value lower than low mark or higher than high mark - // set both HIGH and LOW BG values to red as previous yellow for hig is now not so obvious due to in-range colour of green. - if lastReading.timestamp < Date(timeIntervalSinceNow: -60 * 11) { - - valueLabelOutlet.textColor = UIColor.lightGray - - } else if lastReading.glucose >= UInt16(urgentHighMarkValueInUserChosenUnit.mmolToMgdl(mgdl: bloodGlucoseUnitIsMgDl)) || lastReading.glucose <= UInt16(urgentLowMarkValueInUserChosenUnit.mmolToMgdl(mgdl: bloodGlucoseUnitIsMgDl)) { - - // BG is higher than urgentHigh or lower than urgentLow objectives - valueLabelOutlet.textColor = UIColor.red - - } else if lastReading.glucose >= UInt16(highMarkValueInUserChosenUnit.mmolToMgdl(mgdl: bloodGlucoseUnitIsMgDl)) || lastReading.glucose <= UInt16(lowMarkValueInUserChosenUnit.mmolToMgdl(mgdl: bloodGlucoseUnitIsMgDl)) { - - // BG is between urgentHigh/high and low/urgentLow objectives - valueLabelOutlet.textColor = UIColor.yellow - - } else { - - // BG is between high and low objectives so considered "in range" - valueLabelOutlet.textColor = UIColor.green - - } - - // get minutes ago and create text for minutes ago label - let minutesAgo = -Int(lastReading.timestamp.timeIntervalSinceNow) / 60 - let minutesAgoText = minutesAgo.description // + " " + (minutesAgo == 1 ? Texts.minute:Texts.minutes) + " " + Texts.ago - - minutesLabelOutlet.text = minutesAgoText - - // configure the localized text in the "mins ago" label - let minutesAgoMinAgoText = (minutesAgo == 1 ? Texts.minute : Texts.minutes) + " " + Texts.ago - - minutesAgoLabelOutlet.text = minutesAgoMinAgoText - - minutesLabelOutlet.text = minutesAgoText - - // create delta value text (without the units) - diffLabelOutlet.text = unitizedDeltaString(bgReading: lastReading, previousBgReading: lastButOneReading, mgdl: bloodGlucoseUnitIsMgDl) - - // set the delta unit label text - let diffLabelUnitText = bloodGlucoseUnitIsMgDl ? Texts.mgdl : Texts.mmol - diffLabelUnitOutlet.text = diffLabelUnitText - - } - - /// creates string with bg value in correct unit or "HIGH" or "LOW", or other like ??? - private func unitizedString(calculatedValue: Double, unitIsMgDl:Bool) -> String { - var returnValue:String - if (calculatedValue >= 400) { - returnValue = Texts.HIGH - } else if (calculatedValue >= 40) { - returnValue = calculatedValue.mgdlToMmolAndToString(mgdl: unitIsMgDl) - } else if (calculatedValue > 12) { - returnValue = Texts.LOW - } else { - switch(calculatedValue) { - case 0: - returnValue = "??0" - break - case 1: - returnValue = "?SN" - break - case 2: - returnValue = "??2" - break - case 3: - returnValue = "?NA" - break - case 5: - returnValue = "?NC" - break - case 6: - returnValue = "?CD" - break - case 9: - returnValue = "?AD" - break - case 12: - returnValue = "?RF" - break - default: - returnValue = "???" - break - } - } - return returnValue - } - - private func slopeArrow(slopeOrdinal: UInt8) -> String { - - switch slopeOrdinal { - - case 7: - return "\u{2193}\u{2193}" - - case 6: - return "\u{2193}" - - case 5: - return "\u{2198}" - - case 4: - return "\u{2192}" - - case 3: - return "\u{2197}" - - case 2: - return "\u{2191}" - - case 1: - return "\u{2191}\u{2191}" - - default: - return "" - - } - - } - - /// creates string with difference from previous reading and also unit - private func unitizedDeltaString(bgReading:Glucose, previousBgReading:Glucose?, mgdl:Bool) -> String { - - guard let previousBgReading = previousBgReading else { - return "???" - } - - if bgReading.timestamp.timeIntervalSince(previousBgReading.timestamp) > Double(ConstantsBGGraphBuilder.maxSlopeInMinutes * 60) { - // don't show delta if there are not enough values or the values are more than 20 mintes apart - return "???"; - } - - // delta value recalculated aligned with time difference between previous and this reading - let value = currentSlope(thisBgReading: bgReading, previousBgReading: previousBgReading) * bgReading.timestamp.timeIntervalSince(previousBgReading.timestamp) * 1000; - - if(abs(value) > 100){ - // a delta > 100 will not happen with real BG values -> problematic sensor data - return "ERR"; - } - - let valueAsString = value.mgdlToMmolAndToString(mgdl: mgdl) - - var deltaSign:String = "" - if (value > 0) { deltaSign = "+"; } - - // quickly check "value" and prevent "-0mg/dl" or "-0.0mmol/l" being displayed - if (mgdl) { - if (value > -1) && (value < 1) { - return "0"// + " " + Texts.mgdl; - } else { - return deltaSign + valueAsString //+ " " + Texts.mgdl; - } - } else { - if (value > -0.1) && (value < 0.1) { - return "0.0" //+ " " + Texts.mmol; - } else { - return deltaSign + valueAsString //+ " " + Texts.mmol; - } - } - } - - private func currentSlope(thisBgReading:Glucose, previousBgReading:Glucose?) -> Double { - - if let previousBgReading = previousBgReading { - let (slope,_) = calculateSlope(thisBgReading: thisBgReading, previousBgReading: previousBgReading); - return slope - } else { - return 0.0 - } - - } - - private func calculateSlope(thisBgReading:Glucose, previousBgReading:Glucose) -> (Double, Bool) { - - if thisBgReading.timestamp == previousBgReading.timestamp - || - thisBgReading.timestamp.toMillisecondsAsDouble() - previousBgReading.timestamp.toMillisecondsAsDouble() > Double(ConstantsBGGraphBuilder.maxSlopeInMinutes * 60 * 1000) { - return (0,true) - } - - return ( ( Double(previousBgReading.glucose) - Double(thisBgReading.glucose) ) / (previousBgReading.timestamp.toMillisecondsAsDouble() - thisBgReading.timestamp.toMillisecondsAsDouble()), false) - - } - - private func getMarkValueInUserChosenUnit(forKey key: String, bloodGlucoseUnitIsMgDl: Bool, withDefaultValue defaultValue: Double) -> Double { - - // unwrap shared userdefaults - guard let sharedUserDefaults = xDripClient.shared else {return defaultValue} - - // unwrap urgentLowMarkValueInUserChosenUnit, if key not found then markValue will be 0.0 - var markValue = sharedUserDefaults.double(forKey: key) - - // if 0 set to defaultvalue - if markValue == 0.0 { - markValue = defaultValue - } - - // check if conversion to mmol is needed - if !bloodGlucoseUnitIsMgDl { - markValue = markValue.mgdlToMmol() - } - - return markValue - - } - -} diff --git a/xDrip4iOS Widget/da.lproj/MainInterface.strings b/xDrip4iOS Widget/da.lproj/MainInterface.strings deleted file mode 100644 index d3ac4f06..00000000 --- a/xDrip4iOS Widget/da.lproj/MainInterface.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "UILabel"; text = "Label"; ObjectID = "Usa-lp-vmV"; */ -"Usa-lp-vmV.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "chW-dl-9hs"; */ -"chW-dl-9hs.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "otr-CI-Hqi"; */ -"otr-CI-Hqi.text" = "Label"; diff --git a/xDrip4iOS Widget/el.lproj/MainInterface.strings b/xDrip4iOS Widget/el.lproj/MainInterface.strings deleted file mode 100644 index d3ac4f06..00000000 --- a/xDrip4iOS Widget/el.lproj/MainInterface.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "UILabel"; text = "Label"; ObjectID = "Usa-lp-vmV"; */ -"Usa-lp-vmV.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "chW-dl-9hs"; */ -"chW-dl-9hs.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "otr-CI-Hqi"; */ -"otr-CI-Hqi.text" = "Label"; diff --git a/xDrip4iOS Widget/es.lproj/MainInterface.strings b/xDrip4iOS Widget/es.lproj/MainInterface.strings deleted file mode 100644 index d3ac4f06..00000000 --- a/xDrip4iOS Widget/es.lproj/MainInterface.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "UILabel"; text = "Label"; ObjectID = "Usa-lp-vmV"; */ -"Usa-lp-vmV.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "chW-dl-9hs"; */ -"chW-dl-9hs.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "otr-CI-Hqi"; */ -"otr-CI-Hqi.text" = "Label"; diff --git a/xDrip4iOS Widget/fi.lproj/MainInterface.strings b/xDrip4iOS Widget/fi.lproj/MainInterface.strings deleted file mode 100644 index d3ac4f06..00000000 --- a/xDrip4iOS Widget/fi.lproj/MainInterface.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "UILabel"; text = "Label"; ObjectID = "Usa-lp-vmV"; */ -"Usa-lp-vmV.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "chW-dl-9hs"; */ -"chW-dl-9hs.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "otr-CI-Hqi"; */ -"otr-CI-Hqi.text" = "Label"; diff --git a/xDrip4iOS Widget/ru.lproj/MainInterface.strings b/xDrip4iOS Widget/ru.lproj/MainInterface.strings deleted file mode 100644 index d3ac4f06..00000000 --- a/xDrip4iOS Widget/ru.lproj/MainInterface.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "UILabel"; text = "Label"; ObjectID = "Usa-lp-vmV"; */ -"Usa-lp-vmV.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "chW-dl-9hs"; */ -"chW-dl-9hs.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "otr-CI-Hqi"; */ -"otr-CI-Hqi.text" = "Label"; diff --git a/xDrip4iOS Widget/tr.lproj/MainInterface.strings b/xDrip4iOS Widget/tr.lproj/MainInterface.strings deleted file mode 100644 index d3ac4f06..00000000 --- a/xDrip4iOS Widget/tr.lproj/MainInterface.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "UILabel"; text = "Label"; ObjectID = "Usa-lp-vmV"; */ -"Usa-lp-vmV.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "chW-dl-9hs"; */ -"chW-dl-9hs.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "otr-CI-Hqi"; */ -"otr-CI-Hqi.text" = "Label"; diff --git a/xDrip4iOS Widget/xDripClient/ConstantsBGGraphBuilder.swift b/xDrip4iOS Widget/xDripClient/ConstantsBGGraphBuilder.swift deleted file mode 100644 index 0a691a62..00000000 --- a/xDrip4iOS Widget/xDripClient/ConstantsBGGraphBuilder.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -enum ConstantsBGGraphBuilder { - static let maxSlopeInMinutes = 21 - static let defaultUrgentHighMarkInMgdl = 230.0 - static let defaultHighMarkInMgdl = 180.0 - static let defaultTargetMarkInMgdl = 120.0 - static let defaultLowMarkInMgdl = 70.0 - static let defaultUrgentLowMarkInMgdl = 50.0 -} diff --git a/xDrip4iOS Widget/xDripClient/ConstantsBloodGlucose.swift b/xDrip4iOS Widget/xDripClient/ConstantsBloodGlucose.swift deleted file mode 100644 index fdba7137..00000000 --- a/xDrip4iOS Widget/xDripClient/ConstantsBloodGlucose.swift +++ /dev/null @@ -1,6 +0,0 @@ -enum ConstantsBloodGlucose { - static let mmollToMgdl = 18.01801801801802 - static let mgDlToMmoll = 0.0555 - static let libreMultiplier = 117.64705 -} - diff --git a/xDrip4iOS Widget/xDripClient/Date.swift b/xDrip4iOS Widget/xDripClient/Date.swift deleted file mode 100644 index f82b73ec..00000000 --- a/xDrip4iOS Widget/xDripClient/Date.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -extension Date { - - func toMillisecondsAsDouble() -> Double { - return Double(self.timeIntervalSince1970 * 1000) - } - -} diff --git a/xDrip4iOS Widget/xDripClient/Double.swift b/xDrip4iOS Widget/xDripClient/Double.swift deleted file mode 100644 index d9e896f8..00000000 --- a/xDrip4iOS Widget/xDripClient/Double.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -extension Double { - - /// converts mmol to mgdl if parametermgdl = false and, converts value to string, round. Number of digits after decimal seperator depends on the unit. For mg/dl 0 digits after decimal seperator, for mmol, 1 digit after decimal seperator - /// - /// this function is actually a combination of mmolToMgdl if mgdl = true and bgValuetoString - func mgdlToMmolAndToString(mgdl:Bool) -> String { - if mgdl { - return String(format:"%.0f", self) - } else { - return String(format:"%.1f", self.mgdlToMmol()) - } - } - - /// converts mgdl to mmol - func mgdlToMmol() -> Double { - return self * ConstantsBloodGlucose.mgDlToMmoll - } - - /// converts mmol to mgdl if parameter mgdl = false. If mgdl = true then just returns self - func mmolToMgdl(mgdl:Bool) -> Double { - if mgdl { - return self - } else { - return self.mmolToMgdl() - } - } - - /// converts mmol to mgdl - func mmolToMgdl() -> Double { - return self * ConstantsBloodGlucose.mmollToMgdl - } - -} diff --git a/xDrip4iOS Widget/xDripClient/Glucose+GlucoseKit.swift b/xDrip4iOS Widget/xDripClient/Glucose+GlucoseKit.swift deleted file mode 100644 index 9cececa5..00000000 --- a/xDrip4iOS Widget/xDripClient/Glucose+GlucoseKit.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -public struct Glucose { - public let glucose: UInt16 - public let trend: UInt8 - public let timestamp: Date - public let collector: String? -} - diff --git a/xDrip4iOS Widget/xDripClient/Trace.swift b/xDrip4iOS Widget/xDripClient/Trace.swift deleted file mode 100644 index 00eff8d0..00000000 --- a/xDrip4iOS Widget/xDripClient/Trace.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import os - -/// log only used for debuglogging -fileprivate var log:OSLog = { - let log:OSLog = OSLog(subsystem: "TodayView", category: "TodayView") - return log -}() - -/// will only be used during development -func debuglogging(_ logtext:String) { - os_log("%{public}@", log: log, type: .debug, logtext) -} diff --git a/xdrip-Bridging-Header-swift_2K9IH5TUSZLKY-clang_1I9XNFA44R9PL.pch b/xdrip-Bridging-Header-swift_2K9IH5TUSZLKY-clang_1I9XNFA44R9PL.pch new file mode 100644 index 00000000..744d6de0 Binary files /dev/null and b/xdrip-Bridging-Header-swift_2K9IH5TUSZLKY-clang_1I9XNFA44R9PL.pch differ diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index 4bd9cee5..64f9b5c7 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -11,33 +11,133 @@ 47046EA72A6E8F7B00A6F736 /* BgReadings.strings in Resources */ = {isa = PBXBuildFile; fileRef = 47046EA92A6E8F7B00A6F736 /* BgReadings.strings */; }; 470824D2297484B500C52317 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = 470824D1297484B500C52317 /* SwiftCharts */; }; 470824D3297484B500C52317 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 470824D1297484B500C52317 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 470B618A270C448000561E56 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 470B6188270C448000561E56 /* Interface.storyboard */; }; - 470B618C270C448100561E56 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 470B618B270C448100561E56 /* Assets.xcassets */; }; - 470B6197270C448100561E56 /* InterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470B6196270C448100561E56 /* InterfaceController.swift */; }; - 470B6199270C448100561E56 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470B6198270C448100561E56 /* ExtensionDelegate.swift */; }; - 470B619B270C448100561E56 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470B619A270C448100561E56 /* ComplicationController.swift */; }; - 470B619D270C448200561E56 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 470B619C270C448200561E56 /* Assets.xcassets */; }; - 470B61A1270C448200561E56 /* xDrip4iO5.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 470B6186270C448000561E56 /* xDrip4iO5.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 470CE1FC246802EB00D5CB74 /* BluetoothPeripheralsView.strings in Resources */ = {isa = PBXBuildFile; fileRef = 470CE1FE246802EB00D5CB74 /* BluetoothPeripheralsView.strings */; }; + 4713B0142BBAB12D009F2838 /* ConstantsAppleWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DE41B82B87B2680041DA19 /* ConstantsAppleWatch.swift */; }; 47150A4027F6211C00DB2994 /* SettingsViewTreatmentsSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47150A3F27F6211C00DB2994 /* SettingsViewTreatmentsSettingsViewModel.swift */; }; + 4716A4EF2B406C3D00419052 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4716A4EE2B406C3D00419052 /* WidgetKit.framework */; }; + 4716A4F12B406C3D00419052 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4716A4F02B406C3D00419052 /* SwiftUI.framework */; }; + 4716A4F42B406C3D00419052 /* XDripWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A4F32B406C3D00419052 /* XDripWidgetBundle.swift */; }; + 4716A4F62B406C3D00419052 /* XDripWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A4F52B406C3D00419052 /* XDripWidgetLiveActivity.swift */; }; + 4716A4F82B406C3D00419052 /* XDripWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A4F72B406C3D00419052 /* XDripWidget.swift */; }; + 4716A4FA2B406C3F00419052 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4716A4F92B406C3F00419052 /* Assets.xcassets */; }; + 4716A4FE2B406C3F00419052 /* xDrip Widget Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4716A4ED2B406C3D00419052 /* xDrip Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 4716A5052B40709E00419052 /* XDripWidgetAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A5042B40709E00419052 /* XDripWidgetAttributes.swift */; }; + 4716A5072B4082ED00419052 /* XDripWidgetAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A5042B40709E00419052 /* XDripWidgetAttributes.swift */; }; + 4716A50D2B416EE100419052 /* ConstantsGlucoseChartSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A50C2B416EE100419052 /* ConstantsGlucoseChartSwiftUI.swift */; }; + 4716A5142B41CAD000419052 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A5132B41CAD000419052 /* LiveActivityManager.swift */; }; + 471C9BFF2B932952005E1326 /* LibreLinkUpModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DB06E82A715FD900267BE3 /* LibreLinkUpModels.swift */; }; + 471C9C002B932957005E1326 /* LibreLinkUpModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DB06E82A715FD900267BE3 /* LibreLinkUpModels.swift */; }; + 471C9C082B94F0F0005E1326 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D602448E695001C9E5A /* Bundle.swift */; }; 47228B152996BDD2008725DB /* BgReadingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47228B142996BDD2008725DB /* BgReadingsView.swift */; }; + 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 */; }; + 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 */; }; + 4746066B2B96185C00AC9214 /* AccessoryCircularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746066A2B96185C00AC9214 /* AccessoryCircularView.swift */; }; + 4746066D2B9618B800AC9214 /* AccessoryCornerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746066C2B9618B800AC9214 /* AccessoryCornerView.swift */; }; + 4746066F2B9618FD00AC9214 /* AccessoryInlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746066E2B9618FD00AC9214 /* AccessoryInlineView.swift */; }; + 474606732B962B9500AC9214 /* XDripWidget+Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474606722B962B9500AC9214 /* XDripWidget+Entry.swift */; }; + 474606752B962C4D00AC9214 /* XDripWidget+EntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474606742B962C4D00AC9214 /* XDripWidget+EntryView.swift */; }; + 474606772B962CCD00AC9214 /* XDripWidget+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474606762B962CCD00AC9214 /* XDripWidget+Provider.swift */; }; + 4746067A2B962F4C00AC9214 /* SystemSmallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474606792B962F4C00AC9214 /* SystemSmallView.swift */; }; + 4746067C2B962F8500AC9214 /* SystemMediumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746067B2B962F8500AC9214 /* SystemMediumView.swift */; }; + 4746067E2B962FBD00AC9214 /* SystemLargeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746067D2B962FBD00AC9214 /* SystemLargeView.swift */; }; + 474606802B96308A00AC9214 /* WidgetSharedUserDefaultsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746067F2B96308A00AC9214 /* WidgetSharedUserDefaultsModel.swift */; }; + 474606822B96358E00AC9214 /* WidgetSharedUserDefaultsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746067F2B96308A00AC9214 /* WidgetSharedUserDefaultsModel.swift */; }; + 474606852B9637CA00AC9214 /* LiveActivitySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D08D5D2B54390B00B0BEA7 /* LiveActivitySize.swift */; }; + 474606862B9637CB00AC9214 /* LiveActivitySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D08D5D2B54390B00B0BEA7 /* LiveActivitySize.swift */; }; + 474606872B9637F100AC9214 /* TextsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8BDD451221DEAB1006EAB84 /* TextsSettingsView.swift */; }; + 474606882B9637F600AC9214 /* TextsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8BDD451221DEAB1006EAB84 /* TextsSettingsView.swift */; }; + 474606892B9637F600AC9214 /* TextsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8BDD451221DEAB1006EAB84 /* TextsSettingsView.swift */; }; + 4746068A2B96380200AC9214 /* ConstantsLibreLinkUp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DB06E62A715EC500267BE3 /* ConstantsLibreLinkUp.swift */; }; + 4746068B2B96380200AC9214 /* ConstantsLibreLinkUp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DB06E62A715EC500267BE3 /* ConstantsLibreLinkUp.swift */; }; + 4746068C2B96380200AC9214 /* ConstantsLibreLinkUp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DB06E62A715EC500267BE3 /* ConstantsLibreLinkUp.swift */; }; + 4746068E2B963EA100AC9214 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746068D2B963EA100AC9214 /* View.swift */; }; + 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 */; }; 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 */; }; 4752B4062635878E0081D551 /* SettingsViewStatisticsSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4752B4052635878E0081D551 /* SettingsViewStatisticsSettingsViewModel.swift */; }; + 476CAF792B9F6A7200743168 /* ComplicationSharedUserDefaultsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474606632B95E48D00AC9214 /* ComplicationSharedUserDefaultsModel.swift */; }; 476FE8FF2B2F1D1700537E0A /* ConstantsFollower.swift in Sources */ = {isa = PBXBuildFile; fileRef = 476FE8FE2B2F1D1700537E0A /* ConstantsFollower.swift */; }; 4779BCEE2974306300515714 /* ActionClosurable in Frameworks */ = {isa = PBXBuildFile; productRef = 4779BCED2974306300515714 /* ActionClosurable */; }; 4779BCF12974307700515714 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 4779BCF02974307700515714 /* CryptoSwift */; }; 4779BCF42974308F00515714 /* PieCharts in Frameworks */ = {isa = PBXBuildFile; productRef = 4779BCF32974308F00515714 /* PieCharts */; }; + 477B2C7E2B432775002F64A4 /* LiveActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477B2C7D2B432775002F64A4 /* LiveActivityType.swift */; }; 477F45E6285B993200AC8475 /* GlucoseMiniChartManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477F45E5285B993100AC8475 /* GlucoseMiniChartManager.swift */; }; + 478A923E2B8B64DE0084C394 /* ConstantsHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A1586622EDB8BF007F5B5D /* ConstantsHomeView.swift */; }; + 478A92422B8CCF8F0084C394 /* ConstantsLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B7313F2B83DC0200B0A450 /* ConstantsLiveActivity.swift */; }; + 478A92552B8FA1D80084C394 /* ConstantsBGGraphBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A1585022EDB597007F5B5D /* ConstantsBGGraphBuilder.swift */; }; + 478A92562B8FA1D80084C394 /* ConstantsBloodGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A1585222EDB602007F5B5D /* ConstantsBloodGlucose.swift */; }; + 478A92572B8FA1E30084C394 /* ConstantsBGGraphBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A1585022EDB597007F5B5D /* ConstantsBGGraphBuilder.swift */; }; + 478A92582B8FA1F20084C394 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8EA6C8121B723BC0082976B /* Date.swift */; }; + 478A92592B8FA1F20084C394 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8025E4D21ED450300ECF0C0 /* Double.swift */; }; + 478A925A2B8FA27D0084C394 /* TextsCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8BDD44F221CAA64006EAB84 /* TextsCommon.swift */; }; + 478A925D2B8FA6D30084C394 /* ConstantsUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8FDFEAC260DE1B90047597D /* ConstantsUI.swift */; }; + 478A925F2B8FB5290084C394 /* XDripWatchComplication+Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 478A925E2B8FB5290084C394 /* XDripWatchComplication+Entry.swift */; }; + 478A92612B8FB53B0084C394 /* XDripWatchComplication+EntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 478A92602B8FB53B0084C394 /* XDripWatchComplication+EntryView.swift */; }; + 478A92632B8FB5490084C394 /* XDripWatchComplication+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 478A92622B8FB5490084C394 /* XDripWatchComplication+Provider.swift */; }; + 478A92642B8FBEBF0084C394 /* TextsHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81FA009228F53680028C70F /* TextsHomeView.swift */; }; + 478A92652B90AB040084C394 /* GlucoseChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C210EF2B52A05B00005711 /* GlucoseChartView.swift */; }; + 478A92662B90AB230084C394 /* GlucoseChartType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C210ED2B5298EB00005711 /* GlucoseChartType.swift */; }; + 478A92682B90ABB30084C394 /* ConstantsGlucoseChartSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A50C2B416EE100419052 /* ConstantsGlucoseChartSwiftUI.swift */; }; + 479359862B88B95A007D3CEE /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4716A4EE2B406C3D00419052 /* WidgetKit.framework */; }; + 479359872B88B95A007D3CEE /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4716A4F02B406C3D00419052 /* SwiftUI.framework */; }; + 4793598A2B88B95A007D3CEE /* XDripWatchComplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 479359892B88B95A007D3CEE /* XDripWatchComplication.swift */; }; + 4793598C2B88B95B007D3CEE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4793598B2B88B95B007D3CEE /* Assets.xcassets */; }; + 479359902B88B95B007D3CEE /* xDrip Watch Complication Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 479359852B88B95A007D3CEE /* xDrip Watch Complication Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 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 */; }; + 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 */; }; + 47A6ABE92B790CC70047A4BA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 47A6ABE82B790CC70047A4BA /* Preview Assets.xcassets */; }; + 47A6ABEF2B7949B80047A4BA /* WatchStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A6ABEE2B7949B80047A4BA /* WatchStateModel.swift */; }; + 47A6ABFF2B795DA30047A4BA /* GlucoseChartType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C210ED2B5298EB00005711 /* GlucoseChartType.swift */; }; + 47A6AC292B7D3DAB0047A4BA /* GlucoseChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C210EF2B52A05B00005711 /* GlucoseChartView.swift */; }; + 47A6AC2A2B7D3DE30047A4BA /* GlucoseChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C210EF2B52A05B00005711 /* GlucoseChartView.swift */; }; + 47A6AC2B2B7D3DE30047A4BA /* GlucoseChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C210EF2B52A05B00005711 /* GlucoseChartView.swift */; }; + 47A6AC2C2B7D3E170047A4BA /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8025E4D21ED450300ECF0C0 /* Double.swift */; }; + 47A6AC2D2B7D3E170047A4BA /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8EA6C8121B723BC0082976B /* Date.swift */; }; + 47A6AC2E2B7D3E170047A4BA /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8025E4D21ED450300ECF0C0 /* Double.swift */; }; + 47A6AC2F2B7D3E170047A4BA /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8EA6C8121B723BC0082976B /* Date.swift */; }; + 47A6AC302B7D3E250047A4BA /* ConstantsHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A1586622EDB8BF007F5B5D /* ConstantsHomeView.swift */; }; + 47A6AC312B7D3E250047A4BA /* ConstantsBloodGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A1585222EDB602007F5B5D /* ConstantsBloodGlucose.swift */; }; + 47A6AC322B7D3E250047A4BA /* ConstantsUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8FDFEAC260DE1B90047597D /* ConstantsUI.swift */; }; + 47A6AC332B7D3E260047A4BA /* ConstantsHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A1586622EDB8BF007F5B5D /* ConstantsHomeView.swift */; }; + 47A6AC342B7D3E260047A4BA /* ConstantsBloodGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A1585222EDB602007F5B5D /* ConstantsBloodGlucose.swift */; }; + 47A6AC352B7D3E260047A4BA /* ConstantsUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8FDFEAC260DE1B90047597D /* ConstantsUI.swift */; }; + 47A6AC362B7D3E640047A4BA /* GlucoseChartType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C210ED2B5298EB00005711 /* GlucoseChartType.swift */; }; + 47A6AC372B7D3E650047A4BA /* GlucoseChartType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C210ED2B5298EB00005711 /* GlucoseChartType.swift */; }; + 47A6AC382B7D3E770047A4BA /* TextsHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81FA009228F53680028C70F /* TextsHomeView.swift */; }; + 47A6AC392B7D3E770047A4BA /* TextsHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81FA009228F53680028C70F /* TextsHomeView.swift */; }; + 47A6AC3B2B7D3F9B0047A4BA /* ConstantsGlucoseChartSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A50C2B416EE100419052 /* ConstantsGlucoseChartSwiftUI.swift */; }; + 47A6AC412B7D42EC0047A4BA /* TextsCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8BDD44F221CAA64006EAB84 /* TextsCommon.swift */; }; + 47A6AC422B7D42ED0047A4BA /* TextsCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8BDD44F221CAA64006EAB84 /* TextsCommon.swift */; }; + 47A6AC432B7D430D0047A4BA /* HomeView.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8B48A9E22B2FA7B009BCC01 /* HomeView.strings */; }; + 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 */; }; 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 */; }; 47B60F3726F389E2003198D3 /* LandscapeChartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B60F3626F389E2003198D3 /* LandscapeChartViewController.swift */; }; + 47B731412B83DD3C00B0A450 /* ConstantsLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B7313F2B83DC0200B0A450 /* ConstantsLiveActivity.swift */; }; 47B7FC722B00CF4B004C872B /* FollowerBackgroundKeepAliveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B7FC712B00CF4B004C872B /* FollowerBackgroundKeepAliveType.swift */; }; + 47CA61E42B965E7100C2A597 /* AccessoryCircularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CA61E32B965E7100C2A597 /* AccessoryCircularView.swift */; }; + 47CA61E62B966A9700C2A597 /* AccessoryRectangularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CA61E52B966A9700C2A597 /* AccessoryRectangularView.swift */; }; + 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 */; }; 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 */; }; + 47D08D5F2B54390B00B0BEA7 /* LiveActivitySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D08D5D2B54390B00B0BEA7 /* LiveActivitySize.swift */; }; 47D2DB3B2B14F6D000C8EE6B /* ScreenLockDimmingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D2DB3A2B14F6D000C8EE6B /* ScreenLockDimmingType.swift */; }; 47D9BC952A78498500AB85B2 /* BgReadingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D9BC942A78498500AB85B2 /* BgReadingsDetailView.swift */; }; 47DB06C22A6FC02200267BE3 /* SettingsViewDataSourceSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DB06C12A6FC02200267BE3 /* SettingsViewDataSourceSettingsViewModel.swift */; }; @@ -46,7 +146,14 @@ 47DB06E32A7137B000267BE3 /* LibreLinkUpFollowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DB06E22A7137B000267BE3 /* LibreLinkUpFollowManager.swift */; }; 47DB06E72A715EC500267BE3 /* ConstantsLibreLinkUp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DB06E62A715EC500267BE3 /* ConstantsLibreLinkUp.swift */; }; 47DB06E92A715FD900267BE3 /* LibreLinkUpModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DB06E82A715FD900267BE3 /* LibreLinkUpModels.swift */; }; - 47F8E95A2710255D00B8B02B /* ConstantsWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F8E9592710255C00B8B02B /* ConstantsWatchApp.swift */; }; + 47DE41AA2B860DF00041DA19 /* WatchConnectivity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47DE41A92B860DF00041DA19 /* WatchConnectivity.framework */; }; + 47DE41AD2B863D370041DA19 /* WatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DE41AC2B863D370041DA19 /* WatchState.swift */; }; + 47DE41AE2B863D370041DA19 /* WatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DE41AC2B863D370041DA19 /* WatchState.swift */; }; + 47DE41AF2B864EE50041DA19 /* xDrip Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 47A6ABDF2B790CC60047A4BA /* xDrip Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 47DE41B32B8672F90041DA19 /* DataSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DE41B22B8672F90041DA19 /* DataSourceView.swift */; }; + 47DE41B52B8693CB0041DA19 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DE41B42B8693CB0041DA19 /* HeaderView.swift */; }; + 47DE41B92B87B2680041DA19 /* ConstantsAppleWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DE41B82B87B2680041DA19 /* ConstantsAppleWatch.swift */; }; + 47E91BBA2B9A43F20063181B /* FollowerBackgroundKeepAliveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B7FC712B00CF4B004C872B /* FollowerBackgroundKeepAliveType.swift */; }; 47FB28082636B04200042FFB /* StatisticsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FB28072636B04200042FFB /* StatisticsManager.swift */; }; CE1B2FE025D0264B00F642F5 /* LaunchScreen.strings in Resources */ = {isa = PBXBuildFile; fileRef = CE1B2FD125D0264900F642F5 /* LaunchScreen.strings */; }; CE1B2FE125D0264B00F642F5 /* Main.strings in Resources */ = {isa = PBXBuildFile; fileRef = CE1B2FD425D0264900F642F5 /* Main.strings */; }; @@ -133,7 +240,6 @@ F81F370325C1583400520946 /* WatlaaView.strings in Resources */ = {isa = PBXBuildFile; fileRef = F81F370525C1583400520946 /* WatlaaView.strings */; }; F81F370825C1584A00520946 /* LibreStates.strings in Resources */ = {isa = PBXBuildFile; fileRef = F81F370A25C1584A00520946 /* LibreStates.strings */; }; F81F3C4225D1D91300520946 /* CoreNFC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F81F3C4125D1D91300520946 /* CoreNFC.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; - F81F3C4325D1DA1300520946 /* CoreNFC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F81F3C4125D1D91300520946 /* CoreNFC.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; F81F9FF822861E6D0028C70F /* KeyValueObserverTimeKeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81F9FF722861E6D0028C70F /* KeyValueObserverTimeKeeper.swift */; }; F81F9FFC2288C7530028C70F /* NewAlertSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81F9FFB2288C7530028C70F /* NewAlertSettingsViewController.swift */; }; F81FA0002289E4990028C70F /* AlertSettingsViewControllerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81F9FFF2289E4990028C70F /* AlertSettingsViewControllerData.swift */; }; @@ -311,13 +417,6 @@ F867E2612252ADAB000FD265 /* Calibration+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F867E25D2252ADAB000FD265 /* Calibration+CoreDataProperties.swift */; }; F8691888239CEEFA0065B607 /* BluetoothPeripheralViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8691887239CEEFA0065B607 /* BluetoothPeripheralViewModel.swift */; }; F869188C23A044340065B607 /* TextsM5StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F869188B23A044340065B607 /* TextsM5StackView.swift */; }; - F870D3D325126A49008967B0 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F870D3D225126A49008967B0 /* NotificationCenter.framework */; }; - F870D3D625126A49008967B0 /* TodayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F870D3D525126A49008967B0 /* TodayViewController.swift */; }; - F870D3D925126A49008967B0 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F870D3D725126A49008967B0 /* MainInterface.storyboard */; }; - F870D3DD25126A49008967B0 /* xDrip4iOS Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F870D3D125126A49008967B0 /* xDrip4iOS Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - F870D3EA25129C43008967B0 /* Glucose+GlucoseKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F870D3E925129C43008967B0 /* Glucose+GlucoseKit.swift */; }; - F870D3EC25129FC2008967B0 /* XDripClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F870D3EB25129FC2008967B0 /* XDripClient.swift */; }; - F870D3EE2513B786008967B0 /* Trace.swift in Sources */ = {isa = PBXBuildFile; fileRef = F870D3ED2513B786008967B0 /* Trace.swift */; }; F8797CEA255B43960033956B /* GlucoseData+Smoothable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8797CE9255B43960033956B /* GlucoseData+Smoothable.swift */; }; F889CB6F236D84AC00A81068 /* M5StackView.strings in Resources */ = {isa = PBXBuildFile; fileRef = F889CB71236D84AC00A81068 /* M5StackView.strings */; }; F88EC27A260120C000DF0EAF /* ConstantsAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88EC279260120C000DF0EAF /* ConstantsAlerts.swift */; }; @@ -434,7 +533,7 @@ F8B3A847227F090E004BA588 /* SettingsViewNightScoutSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A83B227F090D004BA588 /* SettingsViewNightScoutSettingsViewModel.swift */; }; F8B3A848227F090E004BA588 /* SettingsViewHealthKitSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A83C227F090D004BA588 /* SettingsViewHealthKitSettingsViewModel.swift */; }; F8B3A849227F090E004BA588 /* SettingsViewSpeakSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A83D227F090D004BA588 /* SettingsViewSpeakSettingsViewModel.swift */; }; - F8B3A84A227F090E004BA588 /* SettingsViewGeneralSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A83E227F090D004BA588 /* SettingsViewGeneralSettingsViewModel.swift */; }; + F8B3A84A227F090E004BA588 /* SettingsViewNotificationsSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A83E227F090D004BA588 /* SettingsViewNotificationsSettingsViewModel.swift */; }; F8B3A84C227F090E004BA588 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A841227F090D004BA588 /* SettingsViewController.swift */; }; F8B3A850227F26F8004BA588 /* AlertTypesSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A84F227F26F8004BA588 /* AlertTypesSettingsViewController.swift */; }; F8B3A853227F2743004BA588 /* AlertsSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A852227F2743004BA588 /* AlertsSettingsViewController.swift */; }; @@ -487,8 +586,8 @@ F8DF766D23ED9B0900063910 /* DexcomG5BluetoothPeripheralViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF766C23ED9B0900063910 /* DexcomG5BluetoothPeripheralViewModel.swift */; }; F8E0475E28CC8E330049D8C9 /* GlucoseData+LoopShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E0475D28CC8E330049D8C9 /* GlucoseData+LoopShare.swift */; }; F8E3A2A323D4E7E200E5E98A /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F8E3A2A223D4E7E200E5E98A /* Default-568h@2x.png */; }; - F8E3A2A923D906C200E5E98A /* WatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E3A2A823D906C200E5E98A /* WatchManager.swift */; }; - F8E3A2AB23DA520B00E5E98A /* ConstantsWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E3A2AA23DA520B00E5E98A /* ConstantsWatch.swift */; }; + F8E3A2A923D906C200E5E98A /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E3A2A823D906C200E5E98A /* CalendarManager.swift */; }; + F8E3A2AB23DA520B00E5E98A /* ConstantsCalendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E3A2AA23DA520B00E5E98A /* ConstantsCalendar.swift */; }; F8E3C3AB21FE17B700907A04 /* StringProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E3C3AA21FE17B700907A04 /* StringProtocol.swift */; }; F8E3C3AD21FE551C00907A04 /* DexcomCalibrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E3C3AC21FE551C00907A04 /* DexcomCalibrator.swift */; }; F8E51D5D2448D8B5001C9E5A /* LoopManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D5C2448D8B5001C9E5A /* LoopManager.swift */; }; @@ -498,12 +597,6 @@ F8E51D65244BA790001C9E5A /* WatlaaBluetoothPeripheralViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D64244BA790001C9E5A /* WatlaaBluetoothPeripheralViewModel.swift */; }; F8E51D67244BAE0E001C9E5A /* WatlaaBluetoothTransmitterMaster+CGMTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D66244BAE0E001C9E5A /* WatlaaBluetoothTransmitterMaster+CGMTransmitter.swift */; }; F8E51D6924549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D6824549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift */; }; - F8E53FAB2517733700052CE5 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E53FAA2517733700052CE5 /* Double.swift */; }; - F8E53FAF2517739700052CE5 /* ConstantsBloodGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E53FAE2517739700052CE5 /* ConstantsBloodGlucose.swift */; }; - F8E53FB92517F79400052CE5 /* ConstantsBGGraphBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E53FB82517F79400052CE5 /* ConstantsBGGraphBuilder.swift */; }; - F8E53FBD2517F96800052CE5 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E53FBC2517F96800052CE5 /* Date.swift */; }; - F8E53FCE251D35FB00052CE5 /* Common.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8E53FD0251D35FB00052CE5 /* Common.strings */; }; - F8E53FE9251F7B8800052CE5 /* Texts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E53FE8251F7B8800052CE5 /* Texts.swift */; }; F8E5404C2522624800052CE5 /* ConstantsHousekeeping.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5404B2522624800052CE5 /* ConstantsHousekeeping.swift */; }; F8E6C78C24CDDB83007C1199 /* SnoozeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6C78B24CDDB83007C1199 /* SnoozeViewController.swift */; }; F8E6C79024CEC22A007C1199 /* TextsSnooze.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6C78F24CEC22A007C1199 /* TextsSnooze.swift */; }; @@ -601,19 +694,26 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 470B619F270C448200561E56 /* PBXContainerItemProxy */ = { + 4716A4FC2B406C3F00419052 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F8AC425221ADEBD60078C348 /* Project object */; proxyType = 1; - remoteGlobalIDString = 470B6185270C448000561E56; - remoteInfo = "Watch App"; + remoteGlobalIDString = 4716A4EC2B406C3D00419052; + remoteInfo = xDripWidgetExtension; }; - F870D3DB25126A49008967B0 /* PBXContainerItemProxy */ = { + 4793598E2B88B95B007D3CEE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F8AC425221ADEBD60078C348 /* Project object */; proxyType = 1; - remoteGlobalIDString = F870D3D025126A49008967B0; - remoteInfo = "xDrip4iOS Widget"; + remoteGlobalIDString = 479359842B88B95A007D3CEE; + remoteInfo = "xDrip Watch ComplicationExtension"; + }; + 47DE41B02B864EE50041DA19 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F8AC425221ADEBD60078C348 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 47A6ABDE2B790CC60047A4BA; + remoteInfo = "xDrip Watch App"; }; /* End PBXContainerItemProxy section */ @@ -635,18 +735,29 @@ dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; dstSubfolderSpec = 16; files = ( - 470B61A1270C448200561E56 /* xDrip4iO5.app in Embed Watch Content */, + 47DE41AF2B864EE50041DA19 /* xDrip Watch App.app in Embed Watch Content */, ); name = "Embed Watch Content"; runOnlyForDeploymentPostprocessing = 0; }; + 479359912B88B95B007D3CEE /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 479359902B88B95B007D3CEE /* xDrip Watch Complication Extension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; F870D3DE25126A49008967B0 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - F870D3DD25126A49008967B0 /* xDrip4iOS Widget.appex in Embed Foundation Extensions */, + 4716A4FE2B406C3F00419052 /* xDrip Widget Extension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -654,7 +765,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 198D44C3260A3A3300A2B4A2 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Common.strings; sourceTree = ""; }; 198D44C4260A3A3300A2B4A2 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Alerts.strings; sourceTree = ""; }; 198D44C5260A3A3300A2B4A2 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/AlertTypesSettingsView.strings; sourceTree = ""; }; 198D44C6260A3A3300A2B4A2 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/BluetoothPeripheralsView.strings; sourceTree = ""; }; @@ -676,10 +786,8 @@ 198D44D7260A3A3400A2B4A2 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Snooze.strings; sourceTree = ""; }; 198D44D8260A3A3400A2B4A2 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/LibreNFC.strings; sourceTree = ""; }; 198D44E7260A822000A2B4A2 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Main.strings; sourceTree = ""; }; - 2867F5C725BC209400AA1E98 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/MainInterface.strings; sourceTree = ""; }; 2867F5C825BC209400AA1E98 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/LaunchScreen.strings; sourceTree = ""; }; 2867F5C925BC209500AA1E98 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Main.strings; sourceTree = ""; }; - 2867F5CA25BC209500AA1E98 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Common.strings; sourceTree = ""; }; 2867F5CB25BC209500AA1E98 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Alerts.strings; sourceTree = ""; }; 2867F5CC25BC209500AA1E98 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/AlertTypesSettingsView.strings; sourceTree = ""; }; 2867F5CD25BC209500AA1E98 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/BluetoothPeripheralsView.strings; sourceTree = ""; }; @@ -697,11 +805,8 @@ 2867F5D925BC209B00AA1E98 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/LibreErrors.strings; sourceTree = ""; }; 2867F5DA25BC209C00AA1E98 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Snooze.strings; sourceTree = ""; }; 2867F5DB25BC209C00AA1E98 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/LibreNFC.strings; sourceTree = ""; }; - 4166BFB528C3501400199980 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainInterface.strings; sourceTree = ""; }; - 4166BFB628C3501400199980 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Interface.strings; sourceTree = ""; }; 4166BFB728C3501500199980 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/LaunchScreen.strings; sourceTree = ""; }; 4166BFB828C3501500199980 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Main.strings; sourceTree = ""; }; - 4166BFB928C3501500199980 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Common.strings; sourceTree = ""; }; 4166BFBA28C3501500199980 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Treatments.strings; sourceTree = ""; }; 4166BFBB28C3501500199980 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Alerts.strings; sourceTree = ""; }; 4166BFBC28C3501500199980 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/AlertTypesSettingsView.strings; sourceTree = ""; }; @@ -725,30 +830,58 @@ 47046EA32A6E8BA700A6F736 /* TextsBgReadings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsBgReadings.swift; sourceTree = ""; }; 47046EA82A6E8F7B00A6F736 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BgReadings.strings; sourceTree = ""; }; 47046EAA2A6E8F8200A6F736 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/BgReadings.strings; sourceTree = ""; }; - 470B6186270C448000561E56 /* xDrip4iO5.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = xDrip4iO5.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 470B6189270C448000561E56 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; - 470B618B270C448100561E56 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 470B6196270C448100561E56 /* InterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceController.swift; sourceTree = ""; }; - 470B6198270C448100561E56 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = ""; }; - 470B619A270C448100561E56 /* ComplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = ""; }; - 470B619C270C448200561E56 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 470B619E270C448200561E56 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 470CE1FF246802F400D5CB74 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BluetoothPeripheralsView.strings; sourceTree = ""; }; 47150A3F27F6211C00DB2994 /* SettingsViewTreatmentsSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTreatmentsSettingsViewModel.swift; sourceTree = ""; }; + 4716A4ED2B406C3D00419052 /* xDrip Widget Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "xDrip Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4716A4EE2B406C3D00419052 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 4716A4F02B406C3D00419052 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 4716A4F32B406C3D00419052 /* XDripWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDripWidgetBundle.swift; sourceTree = ""; }; + 4716A4F52B406C3D00419052 /* XDripWidgetLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDripWidgetLiveActivity.swift; sourceTree = ""; }; + 4716A4F72B406C3D00419052 /* XDripWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDripWidget.swift; sourceTree = ""; }; + 4716A4F92B406C3F00419052 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4716A4FB2B406C3F00419052 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4716A5042B40709E00419052 /* XDripWidgetAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDripWidgetAttributes.swift; sourceTree = ""; }; + 4716A50C2B416EE100419052 /* ConstantsGlucoseChartSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsGlucoseChartSwiftUI.swift; sourceTree = ""; }; + 4716A5132B41CAD000419052 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = ""; }; + 471C9C012B9333D5005E1326 /* xDrip Watch Complication Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "xDrip Watch Complication Extension.entitlements"; sourceTree = ""; }; + 471C9C022B933461005E1326 /* xDrip Widget Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "xDrip Widget Extension.entitlements"; sourceTree = ""; }; + 471C9C052B94DFAE005E1326 /* xDrip-Watch-App-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "xDrip-Watch-App-Info.plist"; sourceTree = SOURCE_ROOT; }; + 471C9C062B94E2B3005E1326 /* xDrip Watch App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "xDrip Watch App.entitlements"; sourceTree = ""; }; 47228B142996BDD2008725DB /* BgReadingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BgReadingsView.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 4746066C2B9618B800AC9214 /* AccessoryCornerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryCornerView.swift; sourceTree = ""; }; + 4746066E2B9618FD00AC9214 /* AccessoryInlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryInlineView.swift; sourceTree = ""; }; + 474606722B962B9500AC9214 /* XDripWidget+Entry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XDripWidget+Entry.swift"; sourceTree = ""; }; + 474606742B962C4D00AC9214 /* XDripWidget+EntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XDripWidget+EntryView.swift"; sourceTree = ""; }; + 474606762B962CCD00AC9214 /* XDripWidget+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XDripWidget+Provider.swift"; sourceTree = ""; }; + 474606792B962F4C00AC9214 /* SystemSmallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemSmallView.swift; sourceTree = ""; }; + 4746067B2B962F8500AC9214 /* SystemMediumView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemMediumView.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; 4752B4052635878E0081D551 /* SettingsViewStatisticsSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewStatisticsSettingsViewModel.swift; sourceTree = ""; }; 475DED96244AF92A00F78473 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Alerts.strings; sourceTree = ""; }; 476FE8FE2B2F1D1700537E0A /* ConstantsFollower.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConstantsFollower.swift; sourceTree = ""; }; + 477B2C7D2B432775002F64A4 /* LiveActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityType.swift; sourceTree = ""; }; 477F45E5285B993100AC8475 /* GlucoseMiniChartManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseMiniChartManager.swift; sourceTree = ""; }; + 478A925E2B8FB5290084C394 /* XDripWatchComplication+Entry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XDripWatchComplication+Entry.swift"; sourceTree = ""; }; + 478A92602B8FB53B0084C394 /* XDripWatchComplication+EntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XDripWatchComplication+EntryView.swift"; sourceTree = ""; }; + 478A92622B8FB5490084C394 /* XDripWatchComplication+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XDripWatchComplication+Provider.swift"; sourceTree = ""; }; + 479359852B88B95A007D3CEE /* xDrip Watch Complication Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "xDrip Watch Complication Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 479359892B88B95A007D3CEE /* XDripWatchComplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDripWatchComplication.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; - 4798BAC927BA766A002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/MainInterface.strings; sourceTree = ""; }; - 4798BACA27BA766A002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Interface.strings; sourceTree = ""; }; 4798BACB27BA7688002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Alerts.strings; sourceTree = ""; }; 4798BACC27BA7691002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/AlertTypesSettingsView.strings; sourceTree = ""; }; 4798BACD27BA7887002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/BluetoothPeripheralsView.strings; sourceTree = ""; }; @@ -769,12 +902,24 @@ 4798BADC27BA7965002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/LibreNFC.strings; sourceTree = ""; }; 4798BADD27BA7996002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/M5StackView.strings; sourceTree = ""; }; 4798BADE27BA79B8002583BC /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/LaunchScreen.strings; sourceTree = ""; }; + 47A6ABDF2B790CC60047A4BA /* xDrip Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "xDrip Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 47A6ABE12B790CC60047A4BA /* xDripWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = xDripWatchApp.swift; sourceTree = ""; }; + 47A6ABE32B790CC60047A4BA /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; 47B60F3626F389E2003198D3 /* LandscapeChartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapeChartViewController.swift; sourceTree = ""; }; + 47B7313F2B83DC0200B0A450 /* ConstantsLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsLiveActivity.swift; sourceTree = ""; }; 47B7FC712B00CF4B004C872B /* FollowerBackgroundKeepAliveType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerBackgroundKeepAliveType.swift; sourceTree = ""; }; + 47C210ED2B5298EB00005711 /* GlucoseChartType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseChartType.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; 47D9BC942A78498500AB85B2 /* BgReadingsDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BgReadingsDetailView.swift; sourceTree = ""; }; 47DB06C02A6FB3CC00267BE3 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/BgReadings.strings; sourceTree = ""; }; @@ -784,7 +929,11 @@ 47DB06E22A7137B000267BE3 /* LibreLinkUpFollowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreLinkUpFollowManager.swift; sourceTree = ""; }; 47DB06E62A715EC500267BE3 /* ConstantsLibreLinkUp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsLibreLinkUp.swift; sourceTree = ""; }; 47DB06E82A715FD900267BE3 /* LibreLinkUpModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreLinkUpModels.swift; sourceTree = ""; }; - 47F8E9592710255C00B8B02B /* ConstantsWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsWatchApp.swift; sourceTree = ""; }; + 47DE41A92B860DF00041DA19 /* WatchConnectivity.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WatchConnectivity.framework; path = System/Library/Frameworks/WatchConnectivity.framework; sourceTree = SDKROOT; }; + 47DE41AC2B863D370041DA19 /* WatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchState.swift; sourceTree = ""; }; + 47DE41B22B8672F90041DA19 /* DataSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceView.swift; sourceTree = ""; }; + 47DE41B42B8693CB0041DA19 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; + 47DE41B82B87B2680041DA19 /* ConstantsAppleWatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsAppleWatch.swift; sourceTree = ""; }; 47FB28072636B04200042FFB /* StatisticsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsManager.swift; sourceTree = ""; }; 666E283826F7E54C00ACE4DF /* xDrip.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = xDrip.xcconfig; path = xdrip/xDrip.xcconfig; sourceTree = ""; }; 666E283926F7E54C00ACE4DF /* Version.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Version.xcconfig; path = xdrip/Version.xcconfig; sourceTree = ""; }; @@ -806,7 +955,6 @@ CE1B2FDD25D0264B00F642F5 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/M5StackView.strings; sourceTree = ""; }; CE1B2FDE25D0264B00F642F5 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Snooze.strings; sourceTree = ""; }; CE1B2FDF25D0264B00F642F5 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/BluetoothPeripheralView.strings; sourceTree = ""; }; - CE1B2FE425D026B400F642F5 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Common.strings; sourceTree = ""; }; D400F8022778BD8000B57648 /* TextsTreatmentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsTreatmentsView.swift; sourceTree = ""; }; D4028CBF2774A50600341476 /* TreatmentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentsViewController.swift; sourceTree = ""; }; D40C3DA3277542C400111B73 /* TreatmentEntry+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TreatmentEntry+CoreDataClass.swift"; sourceTree = ""; }; @@ -956,10 +1104,8 @@ F81F39E925C616C000520946 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/LibreStates.strings; sourceTree = ""; }; F81F39EA25C616C500520946 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Snooze.strings; sourceTree = ""; }; F81F39EB25C616C900520946 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/LibreNFC.strings; sourceTree = ""; }; - F81F3A5F25C9E5A800520946 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainInterface.strings; sourceTree = ""; }; F81F3A6025C9E5A800520946 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/LaunchScreen.strings; sourceTree = ""; }; F81F3A6125C9E5A800520946 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Main.strings; sourceTree = ""; }; - F81F3A6225C9E5A800520946 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Common.strings; sourceTree = ""; }; F81F3A6325C9E5A800520946 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Alerts.strings; sourceTree = ""; }; F81F3A6425C9E5A800520946 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/AlertTypesSettingsView.strings; sourceTree = ""; }; F81F3A6525C9E5A800520946 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/BluetoothPeripheralsView.strings; sourceTree = ""; }; @@ -1253,15 +1399,7 @@ F86A3C78247718C700EE7E46 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/SpeakReading.strings; sourceTree = ""; }; F86A3C79247718C800EE7E46 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/LaunchScreen.strings; sourceTree = ""; }; F86A3C7A247718C800EE7E46 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Main.strings; sourceTree = ""; }; - F870D3D125126A49008967B0 /* xDrip4iOS Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "xDrip4iOS Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; F870D3D225126A49008967B0 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; - F870D3D525126A49008967B0 /* TodayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayViewController.swift; sourceTree = ""; }; - F870D3D825126A49008967B0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; - F870D3DA25126A49008967B0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F870D3E22512701E008967B0 /* xDrip4iOS Widget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "xDrip4iOS Widget.entitlements"; sourceTree = ""; }; - F870D3E925129C43008967B0 /* Glucose+GlucoseKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Glucose+GlucoseKit.swift"; sourceTree = ""; }; - F870D3EB25129FC2008967B0 /* XDripClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XDripClient.swift; sourceTree = ""; }; - F870D3ED2513B786008967B0 /* Trace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trace.swift; sourceTree = ""; }; F8797CE9255B43960033956B /* GlucoseData+Smoothable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseData+Smoothable.swift"; sourceTree = ""; }; F87F5EFD2560686C00FFA395 /* xdrip v15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "xdrip v15.xcdatamodel"; sourceTree = ""; }; F889CB70236D84AC00A81068 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/M5StackView.strings; sourceTree = ""; }; @@ -1271,11 +1409,6 @@ F88EC12325F6CFB200DF0EAF /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/SettingsViews.strings; sourceTree = ""; }; F88EC12425F6CFC200DF0EAF /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/WatlaaView.strings; sourceTree = ""; }; F88EC12525F6CFC500DF0EAF /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/WatlaaView.strings; sourceTree = ""; }; - F88EC17325FABAAE00DF0EAF /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Common.strings; sourceTree = ""; }; - F88EC17425FABAB000DF0EAF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Common.strings; sourceTree = ""; }; - F88EC17525FABAB300DF0EAF /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Common.strings"; sourceTree = ""; }; - F88EC17625FABAB700DF0EAF /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Common.strings; sourceTree = ""; }; - F88EC17725FABAB800DF0EAF /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Common.strings; sourceTree = ""; }; F88EC279260120C000DF0EAF /* ConstantsAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsAlerts.swift; sourceTree = ""; }; F890E079247687AE008FB2EC /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; F897AAF82200F2D200CDDD10 /* CBPeripheralState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBPeripheralState.swift; sourceTree = ""; }; @@ -1396,7 +1529,7 @@ F8B3A83B227F090D004BA588 /* SettingsViewNightScoutSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewNightScoutSettingsViewModel.swift; sourceTree = ""; }; F8B3A83C227F090D004BA588 /* SettingsViewHealthKitSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewHealthKitSettingsViewModel.swift; sourceTree = ""; }; F8B3A83D227F090D004BA588 /* SettingsViewSpeakSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewSpeakSettingsViewModel.swift; sourceTree = ""; }; - F8B3A83E227F090D004BA588 /* SettingsViewGeneralSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewGeneralSettingsViewModel.swift; sourceTree = ""; }; + F8B3A83E227F090D004BA588 /* SettingsViewNotificationsSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewNotificationsSettingsViewModel.swift; sourceTree = ""; }; F8B3A841227F090D004BA588 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; F8B3A84F227F26F8004BA588 /* AlertTypesSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertTypesSettingsViewController.swift; sourceTree = ""; }; F8B3A852227F2743004BA588 /* AlertsSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertsSettingsViewController.swift; sourceTree = ""; }; @@ -1479,7 +1612,6 @@ F8CB59CD27444D6300BA199E /* DexcomSessionStopResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSessionStopResponse.swift; sourceTree = ""; }; F8CB59D2274D94AE00BA199E /* DexcomSessionStartTxMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSessionStartTxMessage.swift; sourceTree = ""; }; F8D0587B24BCB570008C8734 /* SettingsViewHomeScreenSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewHomeScreenSettingsViewModel.swift; sourceTree = ""; }; - F8D094EB2846BDD50087FFEA /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Common.strings; sourceTree = ""; }; F8D094EC2846BDD50087FFEA /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Treatments.strings; sourceTree = ""; }; F8D094ED2846BDD50087FFEA /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Alerts.strings; sourceTree = ""; }; F8D094EE2846BDD50087FFEA /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/AlertTypesSettingsView.strings; sourceTree = ""; }; @@ -1509,15 +1641,12 @@ F8DF766C23ED9B0900063910 /* DexcomG5BluetoothPeripheralViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomG5BluetoothPeripheralViewModel.swift; sourceTree = ""; }; F8E0475D28CC8E330049D8C9 /* GlucoseData+LoopShare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GlucoseData+LoopShare.swift"; sourceTree = ""; }; F8E3A2A223D4E7E200E5E98A /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; - F8E3A2A823D906C200E5E98A /* WatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchManager.swift; sourceTree = ""; }; - F8E3A2AA23DA520B00E5E98A /* ConstantsWatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsWatch.swift; sourceTree = ""; }; + F8E3A2A823D906C200E5E98A /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = ""; }; + F8E3A2AA23DA520B00E5E98A /* ConstantsCalendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsCalendar.swift; sourceTree = ""; }; F8E3C3AA21FE17B700907A04 /* StringProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringProtocol.swift; sourceTree = ""; }; F8E3C3AC21FE551C00907A04 /* DexcomCalibrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomCalibrator.swift; sourceTree = ""; }; - F8E4DCD82805F7FA007CF822 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/MainInterface.strings; sourceTree = ""; }; - F8E4DCD92805F7FA007CF822 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Interface.strings; sourceTree = ""; }; F8E4DCDA2805F7FA007CF822 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/LaunchScreen.strings; sourceTree = ""; }; F8E4DCDB2805F7FA007CF822 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Main.strings; sourceTree = ""; }; - F8E4DCDC2805F7FA007CF822 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Common.strings; sourceTree = ""; }; F8E4DCDD2805F7FB007CF822 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Treatments.strings; sourceTree = ""; }; F8E4DCDE2805F7FB007CF822 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Alerts.strings; sourceTree = ""; }; F8E4DCDF2805F7FB007CF822 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/AlertTypesSettingsView.strings; sourceTree = ""; }; @@ -1545,16 +1674,6 @@ F8E51D64244BA790001C9E5A /* WatlaaBluetoothPeripheralViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatlaaBluetoothPeripheralViewModel.swift; sourceTree = ""; }; F8E51D66244BAE0E001C9E5A /* WatlaaBluetoothTransmitterMaster+CGMTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WatlaaBluetoothTransmitterMaster+CGMTransmitter.swift"; sourceTree = ""; }; F8E51D6824549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTraceSettingsViewModel.swift; sourceTree = ""; }; - F8E53FAA2517733700052CE5 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; - F8E53FAE2517739700052CE5 /* ConstantsBloodGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsBloodGlucose.swift; sourceTree = ""; }; - F8E53FB82517F79400052CE5 /* ConstantsBGGraphBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsBGGraphBuilder.swift; sourceTree = ""; }; - F8E53FBC2517F96800052CE5 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; - F8E53FCF251D35FB00052CE5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Common.strings; sourceTree = ""; }; - F8E53FD1251D360200052CE5 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Common.strings; sourceTree = ""; }; - F8E53FD2251D360300052CE5 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/Common.strings; sourceTree = ""; }; - F8E53FD3251D361600052CE5 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Common.strings; sourceTree = ""; }; - F8E53FD7251D363B00052CE5 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Common.strings; sourceTree = ""; }; - F8E53FE8251F7B8800052CE5 /* Texts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Texts.swift; sourceTree = ""; }; F8E5404B2522624800052CE5 /* ConstantsHousekeeping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsHousekeeping.swift; sourceTree = ""; }; F8E6C78B24CDDB83007C1199 /* SnoozeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozeViewController.swift; sourceTree = ""; }; F8E6C78F24CEC22A007C1199 /* TextsSnooze.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsSnooze.swift; sourceTree = ""; }; @@ -1656,19 +1775,28 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 2F95F647D357FD5C1665313D /* Frameworks */ = { + 4716A4EA2B406C3D00419052 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4716A4F12B406C3D00419052 /* SwiftUI.framework in Frameworks */, + 4716A4EF2B406C3D00419052 /* WidgetKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - F870D3CE25126A49008967B0 /* Frameworks */ = { + 479359822B88B95A007D3CEE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 479359872B88B95A007D3CEE /* SwiftUI.framework in Frameworks */, + 479359862B88B95A007D3CEE /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 47A6ABDC2B790CC60047A4BA /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F81F3C4325D1DA1300520946 /* CoreNFC.framework in Frameworks */, - F870D3D325126A49008967B0 /* NotificationCenter.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1680,6 +1808,7 @@ 470824D2297484B500C52317 /* SwiftCharts in Frameworks */, 4779BCEE2974306300515714 /* ActionClosurable in Frameworks */, F821CF9722AE589E005C1E43 /* HealthKit.framework in Frameworks */, + 47DE41AA2B860DF00041DA19 /* WatchConnectivity.framework in Frameworks */, 4779BCF42974308F00515714 /* PieCharts in Frameworks */, 4779BCF12974307700515714 /* CryptoSwift in Frameworks */, ); @@ -1688,26 +1817,143 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 470B6187270C448000561E56 /* Watch App */ = { + 4716A4F22B406C3D00419052 /* xDrip Widget */ = { isa = PBXGroup; children = ( - 470B618B270C448100561E56 /* Assets.xcassets */, - 470B6188270C448000561E56 /* Interface.storyboard */, + 4716A4FB2B406C3F00419052 /* Info.plist */, + 4716A4F72B406C3D00419052 /* XDripWidget.swift */, + 474606722B962B9500AC9214 /* XDripWidget+Entry.swift */, + 474606742B962C4D00AC9214 /* XDripWidget+EntryView.swift */, + 474606762B962CCD00AC9214 /* XDripWidget+Provider.swift */, + 4716A4F32B406C3D00419052 /* XDripWidgetBundle.swift */, + 4716A4F52B406C3D00419052 /* XDripWidgetLiveActivity.swift */, + 4716A4F92B406C3F00419052 /* Assets.xcassets */, + 4716A5032B40704000419052 /* DataModels */, + 474606782B962F3300AC9214 /* Views */, ); - path = "Watch App"; + path = "xDrip Widget"; sourceTree = ""; }; - 470B6195270C448100561E56 /* Watch App WatchKit Extension */ = { + 4716A5032B40704000419052 /* DataModels */ = { isa = PBXGroup; children = ( - 470B619E270C448200561E56 /* Info.plist */, - 470B619A270C448100561E56 /* ComplicationController.swift */, - 47F8E9592710255C00B8B02B /* ConstantsWatchApp.swift */, - 470B6198270C448100561E56 /* ExtensionDelegate.swift */, - 470B6196270C448100561E56 /* InterfaceController.swift */, - 470B619C270C448200561E56 /* Assets.xcassets */, + 4716A5042B40709E00419052 /* XDripWidgetAttributes.swift */, ); - path = "Watch App WatchKit Extension"; + path = DataModels; + sourceTree = ""; + }; + 4716A5122B41CA9C00419052 /* LiveActivity */ = { + isa = PBXGroup; + children = ( + 4716A5132B41CAD000419052 /* LiveActivityManager.swift */, + 47D08D5D2B54390B00B0BEA7 /* LiveActivitySize.swift */, + 477B2C7D2B432775002F64A4 /* LiveActivityType.swift */, + ); + path = LiveActivity; + sourceTree = ""; + }; + 472596032B76300800459D12 /* Watch */ = { + isa = PBXGroup; + children = ( + 472596042B76301F00459D12 /* WatchManager.swift */, + 47DE41AC2B863D370041DA19 /* WatchState.swift */, + ); + path = Watch; + sourceTree = ""; + }; + 474606662B95F12A00AC9214 /* DataModels */ = { + isa = PBXGroup; + children = ( + 474606632B95E48D00AC9214 /* ComplicationSharedUserDefaultsModel.swift */, + ); + path = DataModels; + sourceTree = ""; + }; + 474606672B96168A00AC9214 /* Views */ = { + isa = PBXGroup; + children = ( + 4746066A2B96185C00AC9214 /* AccessoryCircularView.swift */, + 4746066C2B9618B800AC9214 /* AccessoryCornerView.swift */, + 4746066E2B9618FD00AC9214 /* AccessoryInlineView.swift */, + 474606682B9616AA00AC9214 /* AccessoryRectangularView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 474606782B962F3300AC9214 /* Views */ = { + isa = PBXGroup; + children = ( + 4746067D2B962FBD00AC9214 /* SystemLargeView.swift */, + 4746067B2B962F8500AC9214 /* SystemMediumView.swift */, + 474606792B962F4C00AC9214 /* SystemSmallView.swift */, + 47CA61E32B965E7100C2A597 /* AccessoryCircularView.swift */, + 47CA61E52B966A9700C2A597 /* AccessoryRectangularView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 474606812B96357200AC9214 /* Widgets */ = { + isa = PBXGroup; + children = ( + 4746067F2B96308A00AC9214 /* WidgetSharedUserDefaultsModel.swift */, + ); + path = Widgets; + sourceTree = ""; + }; + 478A92542B8F95930084C394 /* SubViews */ = { + isa = PBXGroup; + children = ( + 47DE41B22B8672F90041DA19 /* DataSourceView.swift */, + 47DE41B42B8693CB0041DA19 /* HeaderView.swift */, + 479359992B8A2A4E007D3CEE /* InfoView.swift */, + ); + path = SubViews; + sourceTree = ""; + }; + 479359882B88B95A007D3CEE /* xDrip Watch Complication */ = { + isa = PBXGroup; + children = ( + 4793598D2B88B95B007D3CEE /* Info.plist */, + 479359892B88B95A007D3CEE /* XDripWatchComplication.swift */, + 478A925E2B8FB5290084C394 /* XDripWatchComplication+Entry.swift */, + 478A92602B8FB53B0084C394 /* XDripWatchComplication+EntryView.swift */, + 478A92622B8FB5490084C394 /* XDripWatchComplication+Provider.swift */, + 4793598B2B88B95B007D3CEE /* Assets.xcassets */, + 474606662B95F12A00AC9214 /* DataModels */, + 474606672B96168A00AC9214 /* Views */, + ); + path = "xDrip Watch Complication"; + sourceTree = ""; + }; + 479359952B88BA4E007D3CEE /* DataModels */ = { + isa = PBXGroup; + children = ( + 47A6ABEE2B7949B80047A4BA /* WatchStateModel.swift */, + ); + path = DataModels; + sourceTree = ""; + }; + 47A6ABE02B790CC60047A4BA /* xDrip Watch App */ = { + isa = PBXGroup; + children = ( + 471C9C062B94E2B3005E1326 /* xDrip Watch App.entitlements */, + 471C9C052B94DFAE005E1326 /* xDrip-Watch-App-Info.plist */, + 47A6ABE12B790CC60047A4BA /* xDripWatchApp.swift */, + 47A6ABE52B790CC70047A4BA /* Assets.xcassets */, + 47DE41B62B87B24C0041DA19 /* Constants */, + 479359952B88BA4E007D3CEE /* DataModels */, + 47A6ABE72B790CC70047A4BA /* Preview Content */, + 47DE41B72B87B2510041DA19 /* Views */, + ); + path = "xDrip Watch App"; + sourceTree = ""; + }; + 47A6ABE72B790CC70047A4BA /* Preview Content */ = { + isa = PBXGroup; + children = ( + 47A6ABE82B790CC70047A4BA /* Preview Assets.xcassets */, + ); + path = "Preview Content"; sourceTree = ""; }; 47DB06CC2A7013EF00267BE3 /* Followers */ = { @@ -1730,6 +1976,23 @@ path = LibreLinkUp; sourceTree = ""; }; + 47DE41B62B87B24C0041DA19 /* Constants */ = { + isa = PBXGroup; + children = ( + 47DE41B82B87B2680041DA19 /* ConstantsAppleWatch.swift */, + ); + path = Constants; + sourceTree = ""; + }; + 47DE41B72B87B2510041DA19 /* Views */ = { + isa = PBXGroup; + children = ( + 47A6ABE32B790CC60047A4BA /* MainView.swift */, + 478A92542B8F95930084C394 /* SubViews */, + ); + path = Views; + sourceTree = ""; + }; 47FB28052636AFE700042FFB /* Statistics */ = { isa = PBXGroup; children = ( @@ -1742,9 +2005,12 @@ 48C0E851274A3BB6D42C6F20 /* Frameworks */ = { isa = PBXGroup; children = ( + 47DE41A92B860DF00041DA19 /* WatchConnectivity.framework */, F81F3C4125D1D91300520946 /* CoreNFC.framework */, F821CF9622AE589E005C1E43 /* HealthKit.framework */, F870D3D225126A49008967B0 /* NotificationCenter.framework */, + 4716A4EE2B406C3D00419052 /* WidgetKit.framework */, + 4716A4F02B406C3D00419052 /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -1817,6 +2083,7 @@ F890E079247687AE008FB2EC /* URL.swift */, F8BDD4232218790E006EAB84 /* UserDefaults.swift */, F80859282364D61B00F3829D /* UserDefaults+charts.swift */, + 4746068D2B963EA100AC9214 /* View.swift */, ); path = Extensions; sourceTree = ""; @@ -2166,18 +2433,21 @@ F821CF48229BF43A005C1E43 /* Alerts */, F821CF54229BF43A005C1E43 /* Application */, F8297F42238DC4AC00D74D66 /* BluetoothPeripheral */, + F8E3A2A723D906B600E5E98A /* Calendar */, F8BECB03235CE5970060DAE1 /* Charts */, F821CF4D229BF43A005C1E43 /* CoreData */, F81D6D4622BD5F43005EFAE2 /* DexcomShare */, 47DB06CC2A7013EF00267BE3 /* Followers */, F821CF9122ADB064005C1E43 /* HealthKit */, 47DB06DB2A7136E900267BE3 /* LibreLinkUp */, + 4716A5122B41CA9C00419052 /* LiveActivity */, F8E51D5B2448D8A3001C9E5A /* Loop */, F821CF4F229BF43A005C1E43 /* NightScout */, F64039AE281C3F8D0051EFFE /* QuickActions */, F821CF9922AEF2DF005C1E43 /* Speak */, 47FB28052636AFE700042FFB /* Statistics */, - F8E3A2A723D906B600E5E98A /* Watch */, + 472596032B76300800459D12 /* Watch */, + 474606812B96357200AC9214 /* Widgets */, ); path = Managers; sourceTree = ""; @@ -2249,6 +2519,7 @@ children = ( 47D9BC942A78498500AB85B2 /* BgReadingsDetailView.swift */, 47228B142996BDD2008725DB /* BgReadingsView.swift */, + 47C210EF2B52A05B00005711 /* GlucoseChartView.swift */, F866974E2867A97000025441 /* Settings */, ); path = SwiftUIViews; @@ -2262,42 +2533,6 @@ path = Settings; sourceTree = ""; }; - F870D3D425126A49008967B0 /* xDrip4iOS Widget */ = { - isa = PBXGroup; - children = ( - F870D3E22512701E008967B0 /* xDrip4iOS Widget.entitlements */, - F870D3DA25126A49008967B0 /* Info.plist */, - F870D3D525126A49008967B0 /* TodayViewController.swift */, - F870D3D725126A49008967B0 /* MainInterface.storyboard */, - F870D3EF25149EA0008967B0 /* Texts */, - F870D3E825129C0B008967B0 /* xDripClient */, - ); - path = "xDrip4iOS Widget"; - sourceTree = ""; - }; - F870D3E825129C0B008967B0 /* xDripClient */ = { - isa = PBXGroup; - children = ( - F870D3EB25129FC2008967B0 /* XDripClient.swift */, - F870D3E925129C43008967B0 /* Glucose+GlucoseKit.swift */, - F870D3ED2513B786008967B0 /* Trace.swift */, - F8E53FAA2517733700052CE5 /* Double.swift */, - F8E53FAE2517739700052CE5 /* ConstantsBloodGlucose.swift */, - F8E53FB82517F79400052CE5 /* ConstantsBGGraphBuilder.swift */, - F8E53FBC2517F96800052CE5 /* Date.swift */, - ); - path = xDripClient; - sourceTree = ""; - }; - F870D3EF25149EA0008967B0 /* Texts */ = { - isa = PBXGroup; - children = ( - F8E53FE8251F7B8800052CE5 /* Texts.swift */, - F8E53FD0251D35FB00052CE5 /* Common.strings */, - ); - path = Texts; - sourceTree = ""; - }; F8A2BBFB25D9D36C001D1E78 /* Atom */ = { isa = PBXGroup; children = ( @@ -2359,16 +2594,18 @@ F8AC425121ADEBD60078C348 = { isa = PBXGroup; children = ( + 471C9C022B933461005E1326 /* xDrip Widget Extension.entitlements */, + 471C9C012B9333D5005E1326 /* xDrip Watch Complication Extension.entitlements */, F8E3A2A223D4E7E200E5E98A /* Default-568h@2x.png */, 666E283926F7E54C00ACE4DF /* Version.xcconfig */, 666E283826F7E54C00ACE4DF /* xDrip.xcconfig */, + 4716A4F22B406C3D00419052 /* xDrip Widget */, + 47A6ABE02B790CC60047A4BA /* xDrip Watch App */, + 479359882B88B95A007D3CEE /* xDrip Watch Complication */, 48C0E851274A3BB6D42C6F20 /* Frameworks */, F8AC425B21ADEBD60078C348 /* Products */, F85DC29B21CFCEB800B9F74A /* Recovered References */, - 470B6187270C448000561E56 /* Watch App */, - 470B6195270C448100561E56 /* Watch App WatchKit Extension */, F8AC425C21ADEBD60078C348 /* xdrip */, - F870D3D425126A49008967B0 /* xDrip4iOS Widget */, ); sourceTree = ""; }; @@ -2376,8 +2613,9 @@ isa = PBXGroup; children = ( F8AC425A21ADEBD60078C348 /* xdrip.app */, - F870D3D125126A49008967B0 /* xDrip4iOS Widget.appex */, - 470B6186270C448000561E56 /* xDrip4iO5.app */, + 4716A4ED2B406C3D00419052 /* xDrip Widget Extension.appex */, + 47A6ABDF2B790CC60047A4BA /* xDrip Watch App.app */, + 479359852B88B95A007D3CEE /* xDrip Watch Complication Extension.appex */, ); name = Products; sourceTree = ""; @@ -2644,7 +2882,7 @@ 47DB06C12A6FC02200267BE3 /* SettingsViewDataSourceSettingsViewModel.swift */, F8A1584C22ECA445007F5B5D /* SettingsViewDevelopmentSettingsViewModel.swift */, F8B3A839227F090D004BA588 /* SettingsViewDexcomSettingsViewModel.swift */, - F8B3A83E227F090D004BA588 /* SettingsViewGeneralSettingsViewModel.swift */, + F8B3A83E227F090D004BA588 /* SettingsViewNotificationsSettingsViewModel.swift */, F8B3A83C227F090D004BA588 /* SettingsViewHealthKitSettingsViewModel.swift */, 47AB72F227105EF4005E7CAB /* SettingsViewHelpSettingModel.swift */, F8D0587B24BCB570008C8734 /* SettingsViewHomeScreenSettingsViewModel.swift */, @@ -2739,6 +2977,7 @@ 47ADD2DE27FAF8630025E2F4 /* ChartPointsScatterDownTrianglesLayer.swift */, 47ADD2E027FB05EB0025E2F4 /* ChartPointsScatterDownTrianglesWithDropdownLineLayer.swift */, F8BECB04235CE5D80060DAE1 /* GlucoseChartManager.swift */, + 47C210ED2B5298EB00005711 /* GlucoseChartType.swift */, 477F45E5285B993100AC8475 /* GlucoseMiniChartManager.swift */, ); path = Charts; @@ -2805,12 +3044,12 @@ path = DexcomG5; sourceTree = ""; }; - F8E3A2A723D906B600E5E98A /* Watch */ = { + F8E3A2A723D906B600E5E98A /* Calendar */ = { isa = PBXGroup; children = ( - F8E3A2A823D906C200E5E98A /* WatchManager.swift */, + F8E3A2A823D906C200E5E98A /* CalendarManager.swift */, ); - path = Watch; + path = Calendar; sourceTree = ""; }; F8E51D5B2448D8A3001C9E5A /* Loop */ = { @@ -2826,25 +3065,29 @@ F8EA6C7D21B70DEA0082976B /* Constants */ = { isa = PBXGroup; children = ( - F8EE3EA12B68332200B27B96 /* ConstantsHeartBeat.swift */, - 476FE8FE2B2F1D1700537E0A /* ConstantsFollower.swift */, F88EC279260120C000DF0EAF /* ConstantsAlerts.swift */, F8A1585022EDB597007F5B5D /* ConstantsBGGraphBuilder.swift */, F8A1585222EDB602007F5B5D /* ConstantsBloodGlucose.swift */, F856CE5A22EDC8E50083E436 /* ConstantsBluetoothPairing.swift */, + F8E3A2AA23DA520B00E5E98A /* ConstantsCalendar.swift */, F8A1585422EDB706007F5B5D /* ConstantsCalibrationAlgorithms.swift */, F8A1585622EDB754007F5B5D /* ConstantsCoreData.swift */, F8A1585822EDB7C6007F5B5D /* ConstantsDefaultAlertLevels.swift */, F8A1586422EDB89D007F5B5D /* ConstantsDefaultAlertTypeSettings.swift */, F8A1585A22EDB7EA007F5B5D /* ConstantsDexcomG5.swift */, + F8F71D792B7E9DFB005076E8 /* ConstantsDexcomG7.swift */, F8A1587222EDC893007F5B5D /* ConstantsDexcomShare.swift */, + 476FE8FE2B2F1D1700537E0A /* ConstantsFollower.swift */, F80859262364355F00F3829D /* ConstantsGlucoseChart.swift */, + 4716A50C2B416EE100419052 /* ConstantsGlucoseChartSwiftUI.swift */, F8B955B6258D5E2000C06016 /* ConstantsHealthKit.swift */, + F8EE3EA12B68332200B27B96 /* ConstantsHeartBeat.swift */, F8A1586622EDB8BF007F5B5D /* ConstantsHomeView.swift */, F8E5404B2522624800052CE5 /* ConstantsHousekeeping.swift */, F8252866243E50FE0067AF77 /* ConstantsLibre.swift */, 47DB06E62A715EC500267BE3 /* ConstantsLibreLinkUp.swift */, F85FB768255DE14600D1C39E /* ConstantsLibreSmoothing.swift */, + 47B7313F2B83DC0200B0A450 /* ConstantsLiveActivity.swift */, F8A1585E22EDB81E007F5B5D /* ConstantsLog.swift */, F8A389C723203E3E0010F405 /* ConstantsM5Stack.swift */, F8A1586A22EDB967007F5B5D /* ConstantsMaster.swift */, @@ -2858,8 +3101,6 @@ F8A1586E22EDC7EE007F5B5D /* ConstantsSuspensionPrevention.swift */, F8AF36142455C6F700B5977B /* ConstantsTrace.swift */, F8FDFEAC260DE1B90047597D /* ConstantsUI.swift */, - F8E3A2AA23DA520B00E5E98A /* ConstantsWatch.swift */, - F8F71D792B7E9DFB005076E8 /* ConstantsDexcomG7.swift */, F2F31E592B9F287000F4FF4C /* FontWeightType.swift */, ); name = Constants; @@ -2871,6 +3112,7 @@ children = ( F81F9FF722861E6D0028C70F /* KeyValueObserverTimeKeeper.swift */, F821CF8022A5C814005C1E43 /* RepeatingTimer.swift */, + 47D2DB3A2B14F6D000C8EE6B /* ScreenLockDimmingType.swift */, F8C5EBE622F38F0E00563B5F /* Trace.swift */, F8EA6CA821BBE3010082976B /* UniqueId.swift */, F8FDFEA6260DE1A70047597D /* CustomColoredDisclosureIndicator */, @@ -2879,7 +3121,6 @@ F821CF69229FC22D005C1E43 /* Network */, F8F7B8E9259A7A9400C47B04 /* SavitzkyGolayFilter */, F8B3A7DD226E48C1004BA588 /* SoundPlayer */, - 47D2DB3A2B14F6D000C8EE6B /* ScreenLockDimmingType.swift */, F227BF1D2B9E536400CEEAAD /* NumberImageView.swift */, ); path = Utilities; @@ -3360,40 +3601,59 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 470B6185270C448000561E56 /* Watch App */ = { + 4716A4EC2B406C3D00419052 /* xDrip Widget Extension */ = { isa = PBXNativeTarget; - buildConfigurationList = 470B61A9270C448200561E56 /* Build configuration list for PBXNativeTarget "Watch App" */; + buildConfigurationList = 4716A5012B406C3F00419052 /* Build configuration list for PBXNativeTarget "xDrip Widget Extension" */; buildPhases = ( - 470B6184270C448000561E56 /* Resources */, - 2F95F647D357FD5C1665313D /* Frameworks */, - 470B618D270C448100561E56 /* Sources */, + 4716A4E92B406C3D00419052 /* Sources */, + 4716A4EA2B406C3D00419052 /* Frameworks */, + 4716A4EB2B406C3D00419052 /* Resources */, ); buildRules = ( ); dependencies = ( ); - name = "Watch App"; - productName = "Watch App"; - productReference = 470B6186270C448000561E56 /* xDrip4iO5.app */; - productType = "com.apple.product-type.application"; - }; - F870D3D025126A49008967B0 /* xDrip4iOS Widget */ = { - isa = PBXNativeTarget; - buildConfigurationList = F870D3E125126A49008967B0 /* Build configuration list for PBXNativeTarget "xDrip4iOS Widget" */; - buildPhases = ( - F870D3CD25126A49008967B0 /* Sources */, - F870D3CE25126A49008967B0 /* Frameworks */, - F870D3CF25126A49008967B0 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "xDrip4iOS Widget"; - productName = "xDrip4iOS Widget"; - productReference = F870D3D125126A49008967B0 /* xDrip4iOS Widget.appex */; + name = "xDrip Widget Extension"; + productName = xDripWidgetExtension; + productReference = 4716A4ED2B406C3D00419052 /* xDrip Widget Extension.appex */; productType = "com.apple.product-type.app-extension"; }; + 479359842B88B95A007D3CEE /* xDrip Watch Complication Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 479359942B88B95B007D3CEE /* Build configuration list for PBXNativeTarget "xDrip Watch Complication Extension" */; + buildPhases = ( + 479359812B88B95A007D3CEE /* Sources */, + 479359822B88B95A007D3CEE /* Frameworks */, + 479359832B88B95A007D3CEE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "xDrip Watch Complication Extension"; + productName = "xDrip Watch ComplicationExtension"; + productReference = 479359852B88B95A007D3CEE /* xDrip Watch Complication Extension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 47A6ABDE2B790CC60047A4BA /* xDrip Watch App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 47A6ABEC2B790CC70047A4BA /* Build configuration list for PBXNativeTarget "xDrip Watch App" */; + buildPhases = ( + 47A6ABDB2B790CC60047A4BA /* Sources */, + 47A6ABDC2B790CC60047A4BA /* Frameworks */, + 47A6ABDD2B790CC60047A4BA /* Resources */, + 479359912B88B95B007D3CEE /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 4793598F2B88B95B007D3CEE /* PBXTargetDependency */, + ); + name = "xDrip Watch App"; + productName = "xDrip Watch App"; + productReference = 47A6ABDF2B790CC60047A4BA /* xDrip Watch App.app */; + productType = "com.apple.product-type.application"; + }; F8AC425921ADEBD60078C348 /* xdrip */ = { isa = PBXNativeTarget; buildConfigurationList = F8AC426E21ADEBD70078C348 /* Build configuration list for PBXNativeTarget "xdrip" */; @@ -3408,8 +3668,8 @@ buildRules = ( ); dependencies = ( - F870D3DC25126A49008967B0 /* PBXTargetDependency */, - 470B61A0270C448200561E56 /* PBXTargetDependency */, + 4716A4FD2B406C3F00419052 /* PBXTargetDependency */, + 47DE41B12B864EE50041DA19 /* PBXTargetDependency */, ); name = xdrip; packageProductDependencies = ( @@ -3429,16 +3689,19 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - DefaultBuildSystemTypeForWorkspace = Original; - LastSwiftUpdateCheck = 1310; + DefaultBuildSystemTypeForWorkspace = Latest; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1500; ORGANIZATIONNAME = "Johan Degraeve"; TargetAttributes = { - 470B6185270C448000561E56 = { - CreatedOnToolsVersion = 13.0; + 4716A4EC2B406C3D00419052 = { + CreatedOnToolsVersion = 15.1; }; - F870D3D025126A49008967B0 = { - CreatedOnToolsVersion = 11.7; + 479359842B88B95A007D3CEE = { + CreatedOnToolsVersion = 15.2; + }; + 47A6ABDE2B790CC60047A4BA = { + CreatedOnToolsVersion = 15.2; }; F8AC425921ADEBD60078C348 = { CreatedOnToolsVersion = 10.1; @@ -3491,29 +3754,40 @@ projectRoot = ""; targets = ( F8AC425921ADEBD60078C348 /* xdrip */, - F870D3D025126A49008967B0 /* xDrip4iOS Widget */, - 470B6185270C448000561E56 /* Watch App */, + 4716A4EC2B406C3D00419052 /* xDrip Widget Extension */, + 47A6ABDE2B790CC60047A4BA /* xDrip Watch App */, + 479359842B88B95A007D3CEE /* xDrip Watch Complication Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 470B6184270C448000561E56 /* Resources */ = { + 4716A4EB2B406C3D00419052 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 470B618C270C448100561E56 /* Assets.xcassets in Resources */, - 470B619D270C448200561E56 /* Assets.xcassets in Resources */, - 470B618A270C448000561E56 /* Interface.storyboard in Resources */, + 47A6AC432B7D430D0047A4BA /* HomeView.strings in Resources */, + 4716A4FA2B406C3F00419052 /* Assets.xcassets in Resources */, + 47A6AC452B7D43100047A4BA /* Common.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - F870D3CF25126A49008967B0 /* Resources */ = { + 479359832B88B95A007D3CEE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - F870D3D925126A49008967B0 /* MainInterface.storyboard in Resources */, - F8E53FCE251D35FB00052CE5 /* Common.strings in Resources */, + 4793598C2B88B95B007D3CEE /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 47A6ABDD2B790CC60047A4BA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 47A6AC442B7D430D0047A4BA /* HomeView.strings in Resources */, + 47A6ABE92B790CC70047A4BA /* Preview Assets.xcassets in Resources */, + 47A6ABE62B790CC70047A4BA /* Assets.xcassets in Resources */, + 47A6AC462B7D43110047A4BA /* Common.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3679,30 +3953,110 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 470B618D270C448100561E56 /* Sources */ = { + 4716A4E92B406C3D00419052 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 470B6199270C448100561E56 /* ExtensionDelegate.swift in Sources */, - 470B6197270C448100561E56 /* InterfaceController.swift in Sources */, - 470B619B270C448100561E56 /* ComplicationController.swift in Sources */, - 47F8E95A2710255D00B8B02B /* ConstantsWatchApp.swift in Sources */, + 47A6AC2A2B7D3DE30047A4BA /* GlucoseChartView.swift in Sources */, + 47A6AC2D2B7D3E170047A4BA /* Date.swift in Sources */, + 471C9C002B932957005E1326 /* LibreLinkUpModels.swift in Sources */, + 47CA61E42B965E7100C2A597 /* AccessoryCircularView.swift in Sources */, + 471C9C082B94F0F0005E1326 /* Bundle.swift in Sources */, + 4746067E2B962FBD00AC9214 /* SystemLargeView.swift in Sources */, + 4716A5052B40709E00419052 /* XDripWidgetAttributes.swift in Sources */, + 47A6AC302B7D3E250047A4BA /* ConstantsHomeView.swift in Sources */, + 474606732B962B9500AC9214 /* XDripWidget+Entry.swift in Sources */, + 47A6AC382B7D3E770047A4BA /* TextsHomeView.swift in Sources */, + 4746067A2B962F4C00AC9214 /* SystemSmallView.swift in Sources */, + 474606772B962CCD00AC9214 /* XDripWidget+Provider.swift in Sources */, + 4746067C2B962F8500AC9214 /* SystemMediumView.swift in Sources */, + 4716A4F62B406C3D00419052 /* XDripWidgetLiveActivity.swift in Sources */, + 4716A4F42B406C3D00419052 /* XDripWidgetBundle.swift in Sources */, + 47D08D5F2B54390B00B0BEA7 /* LiveActivitySize.swift in Sources */, + 474606752B962C4D00AC9214 /* XDripWidget+EntryView.swift in Sources */, + 4716A4F82B406C3D00419052 /* XDripWidget.swift in Sources */, + 4716A50D2B416EE100419052 /* ConstantsGlucoseChartSwiftUI.swift in Sources */, + 47A6AC312B7D3E250047A4BA /* ConstantsBloodGlucose.swift in Sources */, + 47CA61E62B966A9700C2A597 /* AccessoryRectangularView.swift in Sources */, + 4746068A2B96380200AC9214 /* ConstantsLibreLinkUp.swift in Sources */, + 47A6AC2C2B7D3E170047A4BA /* Double.swift in Sources */, + 47A6AC412B7D42EC0047A4BA /* TextsCommon.swift in Sources */, + 478A92572B8FA1E30084C394 /* ConstantsBGGraphBuilder.swift in Sources */, + 4746068F2B963EA100AC9214 /* View.swift in Sources */, + 47A6AC322B7D3E250047A4BA /* ConstantsUI.swift in Sources */, + 474606802B96308A00AC9214 /* WidgetSharedUserDefaultsModel.swift in Sources */, + 47A6AC362B7D3E640047A4BA /* GlucoseChartType.swift in Sources */, + 478A92422B8CCF8F0084C394 /* ConstantsLiveActivity.swift in Sources */, + 474606872B9637F100AC9214 /* TextsSettingsView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - F870D3CD25126A49008967B0 /* Sources */ = { + 479359812B88B95A007D3CEE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F870D3D625126A49008967B0 /* TodayViewController.swift in Sources */, - F8E53FBD2517F96800052CE5 /* Date.swift in Sources */, - F8E53FAF2517739700052CE5 /* ConstantsBloodGlucose.swift in Sources */, - F870D3EE2513B786008967B0 /* Trace.swift in Sources */, - F870D3EC25129FC2008967B0 /* XDripClient.swift in Sources */, - F870D3EA25129C43008967B0 /* Glucose+GlucoseKit.swift in Sources */, - F8E53FAB2517733700052CE5 /* Double.swift in Sources */, - F8E53FE9251F7B8800052CE5 /* Texts.swift in Sources */, - F8E53FB92517F79400052CE5 /* ConstantsBGGraphBuilder.swift in Sources */, + 474606912B963EA100AC9214 /* View.swift in Sources */, + 4746066D2B9618B800AC9214 /* AccessoryCornerView.swift in Sources */, + 478A92652B90AB040084C394 /* GlucoseChartView.swift in Sources */, + 4793598A2B88B95A007D3CEE /* XDripWatchComplication.swift in Sources */, + 478A92682B90ABB30084C394 /* ConstantsGlucoseChartSwiftUI.swift in Sources */, + 478A92562B8FA1D80084C394 /* ConstantsBloodGlucose.swift in Sources */, + 4746066B2B96185C00AC9214 /* AccessoryCircularView.swift in Sources */, + 478A92612B8FB53B0084C394 /* XDripWatchComplication+EntryView.swift in Sources */, + 478A92642B8FBEBF0084C394 /* TextsHomeView.swift in Sources */, + 474606652B95E48D00AC9214 /* ComplicationSharedUserDefaultsModel.swift in Sources */, + 478A92662B90AB230084C394 /* GlucoseChartType.swift in Sources */, + 478A92632B8FB5490084C394 /* XDripWatchComplication+Provider.swift in Sources */, + 478A92592B8FA1F20084C394 /* Double.swift in Sources */, + 474606862B9637CB00AC9214 /* LiveActivitySize.swift in Sources */, + 4746066F2B9618FD00AC9214 /* AccessoryInlineView.swift in Sources */, + 474606892B9637F600AC9214 /* TextsSettingsView.swift in Sources */, + 471C9BFF2B932952005E1326 /* LibreLinkUpModels.swift in Sources */, + 478A923E2B8B64DE0084C394 /* ConstantsHomeView.swift in Sources */, + 4796C6072B9516FD00DE2210 /* Bundle.swift in Sources */, + 47E91BBA2B9A43F20063181B /* FollowerBackgroundKeepAliveType.swift in Sources */, + 478A92582B8FA1F20084C394 /* Date.swift in Sources */, + 478A92552B8FA1D80084C394 /* ConstantsBGGraphBuilder.swift in Sources */, + 4713B0142BBAB12D009F2838 /* ConstantsAppleWatch.swift in Sources */, + 478A925F2B8FB5290084C394 /* XDripWatchComplication+Entry.swift in Sources */, + 478A925A2B8FA27D0084C394 /* TextsCommon.swift in Sources */, + 4746068C2B96380200AC9214 /* ConstantsLibreLinkUp.swift in Sources */, + 478A925D2B8FA6D30084C394 /* ConstantsUI.swift in Sources */, + 474606692B9616AA00AC9214 /* AccessoryRectangularView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 47A6ABDB2B790CC60047A4BA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 47A6AC2B2B7D3DE30047A4BA /* GlucoseChartView.swift in Sources */, + 47DE41B52B8693CB0041DA19 /* HeaderView.swift in Sources */, + 47A6AC342B7D3E260047A4BA /* ConstantsBloodGlucose.swift in Sources */, + 474606852B9637CA00AC9214 /* LiveActivitySize.swift in Sources */, + 47A6AC2E2B7D3E170047A4BA /* Double.swift in Sources */, + 47A6AC392B7D3E770047A4BA /* TextsHomeView.swift in Sources */, + 47CA61E72B97948000C2A597 /* FollowerDataSourceType.swift in Sources */, + 47A6AC332B7D3E260047A4BA /* ConstantsHomeView.swift in Sources */, + 474606642B95E48D00AC9214 /* ComplicationSharedUserDefaultsModel.swift in Sources */, + 474606902B963EA100AC9214 /* View.swift in Sources */, + 47A6AC372B7D3E650047A4BA /* GlucoseChartType.swift in Sources */, + 47A6ABE42B790CC60047A4BA /* MainView.swift in Sources */, + 474606882B9637F600AC9214 /* TextsSettingsView.swift in Sources */, + 4793599A2B8A2A4E007D3CEE /* InfoView.swift in Sources */, + 47A6ABEF2B7949B80047A4BA /* WatchStateModel.swift in Sources */, + 47A6ABE22B790CC60047A4BA /* xDripWatchApp.swift in Sources */, + 47DE41B32B8672F90041DA19 /* DataSourceView.swift in Sources */, + 4796C6062B9516FC00DE2210 /* Bundle.swift in Sources */, + 47A6AC2F2B7D3E170047A4BA /* Date.swift in Sources */, + 47CA61E92B97A6BD00C2A597 /* FollowerBackgroundKeepAliveType.swift in Sources */, + 47DE41AE2B863D370041DA19 /* WatchState.swift in Sources */, + 47A6AC422B7D42ED0047A4BA /* TextsCommon.swift in Sources */, + 47A6AC3B2B7D3F9B0047A4BA /* ConstantsGlucoseChartSwiftUI.swift in Sources */, + 47DE41B92B87B2680041DA19 /* ConstantsAppleWatch.swift in Sources */, + 47CA61E82B9796D200C2A597 /* ConstantsFollower.swift in Sources */, + 4746068B2B96380200AC9214 /* ConstantsLibreLinkUp.swift in Sources */, + 47A6AC352B7D3E260047A4BA /* ConstantsUI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3725,7 +4079,7 @@ F81FA006228E09D40028C70F /* TextsCalibration.swift in Sources */, F816E0F724367137009EE65B /* GNSEntry+CoreDataClass.swift in Sources */, F8F9721923A5915900C3F17D /* CGMGNSEntryTransmitter.swift in Sources */, - F8B3A84A227F090E004BA588 /* SettingsViewGeneralSettingsViewModel.swift in Sources */, + F8B3A84A227F090E004BA588 /* SettingsViewNotificationsSettingsViewModel.swift in Sources */, F83098FE23AD3F84005741DF /* UITabBarController.swift in Sources */, F816E11A243923B2009EE65B /* Droplet+CoreDataClass.swift in Sources */, F80D916D24F82A17006840B5 /* CGMLibre2TransmitterDelegate.swift in Sources */, @@ -3749,6 +4103,7 @@ F85542322B7573D20058CE09 /* OmniPodHeartBeat+CoreDataClass.swift in Sources */, F816E1002436734C009EE65B /* CGMGNSEntryTransmitterDelegate.swift in Sources */, F82842322752CBE00097E0C9 /* DexcomSessionStopTxMessage.swift in Sources */, + 4716A5142B41CAD000419052 /* LiveActivityManager.swift in Sources */, F8C97853242AA70D00A09483 /* MiaoMiao+CoreDataClass.swift in Sources */, F8F9720D23A5915900C3F17D /* ResetMessage.swift in Sources */, F8EE3EA22B68332200B27B96 /* ConstantsHeartBeat.swift in Sources */, @@ -3764,6 +4119,7 @@ F8797CEA255B43960033956B /* GlucoseData+Smoothable.swift in Sources */, F821CF9022AB1068005C1E43 /* DatePickerViewData.swift in Sources */, F8F9722623A5915900C3F17D /* BluconTransmitterOpCode.swift in Sources */, + 47B731412B83DD3C00B0A450 /* ConstantsLiveActivity.swift in Sources */, F8025E4E21ED450300ECF0C0 /* Double.swift in Sources */, F8F9723423A5915900C3F17D /* BluetoothTransmitterDelegate.swift in Sources */, F8F9724923A69DAE00C3F17D /* M5StickC.swift in Sources */, @@ -3802,6 +4158,7 @@ F8F9721B23A5915900C3F17D /* CGMMiaoMiaoTransmitter.swift in Sources */, F8F9721A23A5915900C3F17D /* CGMBubbleTransmitter.swift in Sources */, 4752B400263570DA0081D551 /* ConstantsStatistics.swift in Sources */, + 47A6ABFF2B795DA30047A4BA /* GlucoseChartType.swift in Sources */, F227BF192B9DF76D00CEEAAD /* SettingsViewContactTrickSettingsViewModel.swift in Sources */, F8A2BC3B25DB0D6D001D1E78 /* BluetoothPeripheralManager+CGMBubbleTransmitterDelegate.swift in Sources */, F8A2BC3A25DB0D6D001D1E78 /* BluetoothPeripheralManager+CGMGNSEntryTransmitterDelegate.swift in Sources */, @@ -3845,6 +4202,7 @@ F8F971B623A5914D00C3F17D /* M5Stack+BluetoothPeripheral.swift in Sources */, F830992323C291EE005741DF /* Watlaa+BluetoothPeripheral.swift in Sources */, F821CF57229BF43A005C1E43 /* SnoozeParameters.swift in Sources */, + 47DE41AD2B863D370041DA19 /* WatchState.swift in Sources */, F830992823C32A13005741DF /* TextsWatlaaView.swift in Sources */, F8A2BC4525DB2679001D1E78 /* AtomResponseType.swift in Sources */, F8B3A79722635A25004BA588 /* AlertEntry+CoreDataProperties.swift in Sources */, @@ -3870,6 +4228,7 @@ F8A1586F22EDC7EE007F5B5D /* ConstantsSuspensionPrevention.swift in Sources */, F8B3A82D227F07D6004BA588 /* SettingsNavigationController.swift in Sources */, F8EE3EA62B6833FA00B27B96 /* DexcomG7HeartBeatBluetoothPeripheralViewModel.swift in Sources */, + 476CAF792B9F6A7200743168 /* ComplicationSharedUserDefaultsModel.swift in Sources */, D482BD942776153F003C4FB2 /* TreatmentsNavigationController.swift in Sources */, F80ED2EC236F68F90005C035 /* SettingsViewM5StackBluetoothSettingsViewModel.swift in Sources */, F8BECB05235CE5D80060DAE1 /* GlucoseChartManager.swift in Sources */, @@ -3949,13 +4308,15 @@ F8A1586722EDB8BF007F5B5D /* ConstantsHomeView.swift in Sources */, F8A1585922EDB7C6007F5B5D /* ConstantsDefaultAlertLevels.swift in Sources */, F8A54AAD22D6859200934E7A /* SlopeParameters.swift in Sources */, + 472596052B76301F00459D12 /* WatchManager.swift in Sources */, F830993023C928E0005741DF /* WatlaaBluetoothTransmitterDelegate.swift in Sources */, F8025C0A21D94FD700ECF0C0 /* CBManagerState.swift in Sources */, 47ADD2DF27FAF8630025E2F4 /* ChartPointsScatterDownTrianglesLayer.swift in Sources */, F8F9722723A5915900C3F17D /* CGMBluconTransmitter.swift in Sources */, - F8E3A2A923D906C200E5E98A /* WatchManager.swift in Sources */, + F8E3A2A923D906C200E5E98A /* CalendarManager.swift in Sources */, F8B3A80A227A3D11004BA588 /* TextsAlertTypeSettings.swift in Sources */, F85FF39125288870004E6FF1 /* HouseKeeper.swift in Sources */, + 47D08D5E2B54390B00B0BEA7 /* LiveActivitySize.swift in Sources */, F8CB59C42739D1CD00BA199E /* DexcomBackfillTxMessage.swift in Sources */, F8F9721823A5915900C3F17D /* CGMBlueReaderTransmitter.swift in Sources */, F8252867243E50FE0067AF77 /* ConstantsLibre.swift in Sources */, @@ -3963,6 +4324,7 @@ F8CB59D3274D94AF00BA199E /* DexcomSessionStartTxMessage.swift in Sources */, F80ED2ED236F68F90005C035 /* SettingsViewM5StackGeneralSettingsViewModel.swift in Sources */, F816E10A2437E7B8009EE65B /* CGMBlueReaderTransmitterDelegate.swift in Sources */, + 47D08D5B2B5437F500B0BEA7 /* ConstantsGlucoseChartSwiftUI.swift in Sources */, F227BF1E2B9E536400CEEAAD /* NumberImageView.swift in Sources */, F8B3A850227F26F8004BA588 /* AlertTypesSettingsViewController.swift in Sources */, D484BC292774F783008490E9 /* TreatmentsInsertViewController.swift in Sources */, @@ -3973,6 +4335,7 @@ F816E0F02433C31B009EE65B /* Blucon+CoreDataProperties.swift in Sources */, F8A2BC3925DB0D6D001D1E78 /* BluetoothPeripheralManager+CGMLibre2TransmitterDelegate.swift in Sources */, F8A5EEAE25791F370085E660 /* Libre2BLEUtilities.swift in Sources */, + 477B2C7E2B432775002F64A4 /* LiveActivityType.swift in Sources */, F86697502867AA4A00025441 /* LoopDelayScheduleView.swift in Sources */, F8E3C3AB21FE17B700907A04 /* StringProtocol.swift in Sources */, F8F9720723A5915900C3F17D /* AuthChallengeTxMessage.swift in Sources */, @@ -3981,7 +4344,7 @@ F8E51D6924549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift in Sources */, F85DC2F321CFE3D400B9F74A /* Calibration+CoreDataClass.swift in Sources */, F8AF361B245D93EE00B5977B /* Int16.swift in Sources */, - F8E3A2AB23DA520B00E5E98A /* ConstantsWatch.swift in Sources */, + F8E3A2AB23DA520B00E5E98A /* ConstantsCalendar.swift in Sources */, F8F9721523A5915900C3F17D /* PairRequestTxMessage.swift in Sources */, F8BECB12235CEA9B0060DAE1 /* TimeInterval.swift in Sources */, F8EE3E9E2B6831A200B27B96 /* DexcomG7HeartBeat+BluetoothPeripheral.swift in Sources */, @@ -4028,6 +4391,7 @@ F8A2BC1425DB0B61001D1E78 /* CGMAtomTransmitter.swift in Sources */, F8AC426021ADEBD60078C348 /* RootViewController.swift in Sources */, F8B3A78B225D473D004BA588 /* UIAlertController.swift in Sources */, + 47A6AC292B7D3DAB0047A4BA /* GlucoseChartView.swift in Sources */, F816E118243921FB009EE65B /* CGMBDropletTransmitterDelegate.swift in Sources */, F8F9720E23A5915900C3F17D /* AuthRequestRxMessage.swift in Sources */, F8A2BC3F25DB0D89001D1E78 /* BluetoothPeripheralManager+CGMAtomTransmitterDelegate.swift in Sources */, @@ -4058,6 +4422,7 @@ F8F9721123A5915900C3F17D /* KeepAliveTxMessage.swift in Sources */, F64039B0281C3F9D0051EFFE /* QuickActionsManager.swift in Sources */, F8B3A81E227DEC92004BA588 /* BgReadingsAccessor.swift in Sources */, + 4746068E2B963EA100AC9214 /* View.swift in Sources */, D41F32942827332000861B3D /* DataExporter.swift in Sources */, 4733B9402AD17D15001D609D /* FollowerDelegate.swift in Sources */, F8EE3EAF2B6834FD00B27B96 /* Libre2HeartBeat+CoreDataProperties.swift in Sources */, @@ -4084,6 +4449,7 @@ F808D2D2240329E80084B5DB /* Bubble+BluetoothPeripheral.swift in Sources */, F8025C1321DA683400ECF0C0 /* Data.swift in Sources */, F80859272364355F00F3829D /* ConstantsGlucoseChart.swift in Sources */, + 4716A5072B4082ED00419052 /* XDripWidgetAttributes.swift in Sources */, F816E10324367389009EE65B /* GNSEntryBluetoothPeripheralViewModel.swift in Sources */, D4E499AD277B4CE7000F8CBA /* DateOnly.swift in Sources */, F8E51D5D2448D8B5001C9E5A /* LoopManager.swift in Sources */, @@ -4101,6 +4467,7 @@ F8E51D5F2448E2E8001C9E5A /* ConstantsShareWithLoop.swift in Sources */, F816E0ED2432A55F009EE65B /* BluconBluetoothPeripheralViewModel.swift in Sources */, F8F9722923A5915900C3F17D /* CGMTransmitter.swift in Sources */, + 474606822B96358E00AC9214 /* WidgetSharedUserDefaultsModel.swift in Sources */, 47150A4027F6211C00DB2994 /* SettingsViewTreatmentsSettingsViewModel.swift in Sources */, F8A389ED23342EB10010F405 /* ConstantsNightScout.swift in Sources */, F85FF3D1252F9FF9004E6FF1 /* SnoozeParameters+CoreDataClass.swift in Sources */, @@ -4126,15 +4493,20 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 470B61A0270C448200561E56 /* PBXTargetDependency */ = { + 4716A4FD2B406C3F00419052 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 470B6185270C448000561E56 /* Watch App */; - targetProxy = 470B619F270C448200561E56 /* PBXContainerItemProxy */; + target = 4716A4EC2B406C3D00419052 /* xDrip Widget Extension */; + targetProxy = 4716A4FC2B406C3F00419052 /* PBXContainerItemProxy */; }; - F870D3DC25126A49008967B0 /* PBXTargetDependency */ = { + 4793598F2B88B95B007D3CEE /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = F870D3D025126A49008967B0 /* xDrip4iOS Widget */; - targetProxy = F870D3DB25126A49008967B0 /* PBXContainerItemProxy */; + target = 479359842B88B95A007D3CEE /* xDrip Watch Complication Extension */; + targetProxy = 4793598E2B88B95B007D3CEE /* PBXContainerItemProxy */; + }; + 47DE41B12B864EE50041DA19 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 47A6ABDE2B790CC60047A4BA /* xDrip Watch App */; + targetProxy = 47DE41B02B864EE50041DA19 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -4149,17 +4521,6 @@ name = BgReadings.strings; sourceTree = ""; }; - 470B6188270C448000561E56 /* Interface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 470B6189270C448000561E56 /* Base */, - 4798BACA27BA766A002583BC /* tr */, - F8E4DCD92805F7FA007CF822 /* da */, - 4166BFB628C3501400199980 /* el */, - ); - name = Interface.storyboard; - sourceTree = ""; - }; 470CE1FE246802EB00D5CB74 /* BluetoothPeripheralsView.strings */ = { isa = PBXVariantGroup; children = ( @@ -4326,19 +4687,6 @@ name = LibreStates.strings; sourceTree = ""; }; - F870D3D725126A49008967B0 /* MainInterface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F870D3D825126A49008967B0 /* Base */, - 2867F5C725BC209400AA1E98 /* fi */, - F81F3A5F25C9E5A800520946 /* es */, - 4798BAC927BA766A002583BC /* tr */, - F8E4DCD82805F7FA007CF822 /* da */, - 4166BFB528C3501400199980 /* el */, - ); - name = MainInterface.storyboard; - sourceTree = ""; - }; F889CB71236D84AC00A81068 /* M5StackView.strings */ = { isa = PBXVariantGroup; children = ( @@ -4687,30 +5035,6 @@ name = SettingsViews.strings; sourceTree = ""; }; - F8E53FD0251D35FB00052CE5 /* Common.strings */ = { - isa = PBXVariantGroup; - children = ( - F8E53FCF251D35FB00052CE5 /* en */, - F8E53FD1251D360200052CE5 /* ar */, - F8E53FD2251D360300052CE5 /* zh */, - F8E53FD3251D361600052CE5 /* nl */, - F8E53FD7251D363B00052CE5 /* pt */, - 2867F5CA25BC209500AA1E98 /* fi */, - F81F3A6225C9E5A800520946 /* es */, - CE1B2FE425D026B400F642F5 /* fr */, - F88EC17325FABAAE00DF0EAF /* de */, - F88EC17425FABAB000DF0EAF /* it */, - F88EC17525FABAB300DF0EAF /* pl-PL */, - F88EC17625FABAB700DF0EAF /* ru */, - F88EC17725FABAB800DF0EAF /* sl */, - 198D44C3260A3A3300A2B4A2 /* sv */, - F8E4DCDC2805F7FA007CF822 /* da */, - F8D094EB2846BDD50087FFEA /* uk */, - 4166BFB928C3501500199980 /* el */, - ); - name = Common.strings; - sourceTree = ""; - }; F8E6C79324CEC2E3007C1199 /* Snooze.strings */ = { isa = PBXVariantGroup; children = ( @@ -4764,121 +5088,228 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 470B61A3270C448200561E56 /* Debug */ = { + 4716A4FF2B406C3F00419052 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "xDrip Widget Extension.entitlements"; CODE_SIGN_IDENTITY = "$(XDRIP_CODE_SIGN_IDENTITY_DEBUG)"; 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_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_MODULE_NAME).ComplicationController"; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Johan Degraeve. All rights reserved."; - INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(MAIN_APP_BUNDLE_IDENTIFIER)"; + INFOPLIST_FILE = "xDrip Widget/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Johan Degraeve. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MAIN_APP_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER)"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = "$(XDRIP_MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).watchkitapp"; - PRODUCT_NAME = "$(MAIN_APP_DISPLAY_NAME)"; - SDKROOT = watchos; + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).xDripWidget"; + 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 = 4; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 470B61A4270C448200561E56 /* Release */ = { + 4716A5002B406C3F00419052 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "xDrip Widget Extension.entitlements"; CODE_SIGN_IDENTITY = "$(XDRIP_CODE_SIGN_IDENTITY_RELEASE)"; 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_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_MODULE_NAME).ComplicationController"; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Johan Degraeve. All rights reserved."; - INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(MAIN_APP_BUNDLE_IDENTIFIER)"; + INFOPLIST_FILE = "xDrip Widget/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Johan Degraeve. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MAIN_APP_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER)"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = "$(XDRIP_MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).watchkitapp"; - PRODUCT_NAME = "$(MAIN_APP_DISPLAY_NAME)"; - SDKROOT = watchos; + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).xDripWidget"; + PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; - F870D3DF25126A49008967B0 /* Debug */ = { + 479359922B88B95B007D3CEE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; - CODE_SIGN_ENTITLEMENTS = "$(XDRIP_WIDGET_ENTITLEMENTS_DEBUG)"; - CODE_SIGN_IDENTITY = "$(XDRIP_CODE_SIGN_IDENTITY_DEBUG)"; - CODE_SIGN_STYLE = "$(XDRIP_CODE_SIGN_STYLE)"; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "xDrip Watch Complication Extension.entitlements"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; - INFOPLIST_FILE = "xDrip4iOS Widget/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "xDrip Watch Complication/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Johan Degraeve. All rights reserved."; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", + "@executable_path/../../../../Frameworks", ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = "$(XDRIP_MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).xDrip4iOS-Widget"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).watchkitapp.xDripWatchComplication"; PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; }; name = Debug; }; - F870D3E025126A49008967B0 /* Release */ = { + 479359932B88B95B007D3CEE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; - CODE_SIGN_ENTITLEMENTS = "$(XDRIP_WIDGET_ENTITLEMENTS_RELEASE)"; - CODE_SIGN_IDENTITY = "$(XDRIP_CODE_SIGN_IDENTITY_RELEASE)"; - CODE_SIGN_STYLE = "$(XDRIP_CODE_SIGN_STYLE)"; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "xDrip Watch Complication Extension.entitlements"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; - INFOPLIST_FILE = "xDrip4iOS Widget/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "xDrip Watch Complication/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Johan Degraeve. All rights reserved."; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", + "@executable_path/../../../../Frameworks", ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = "$(XDRIP_MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).xDrip4iOS-Widget"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).watchkitapp.xDripWatchComplication"; PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Release; + }; + 47A6ABEA2B790CC70047A4BA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "xDrip Watch App/xDrip Watch App.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_ASSET_PATHS = "\"xDrip Watch App/Preview Content\""; + DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; + ENABLE_PREVIEWS = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "xDrip-Watch-App-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(MAIN_APP_BUNDLE_IDENTIFIER)"; + INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MAIN_APP_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER)"; + MARKETING_VERSION = "$(XDRIP_MARKETING_VERSION)"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Debug; + }; + 47A6ABEB2B790CC70047A4BA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "xDrip Watch App/xDrip Watch App.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_ASSET_PATHS = "\"xDrip Watch App/Preview Content\""; + DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; + ENABLE_PREVIEWS = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "xDrip-Watch-App-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(MAIN_APP_BUNDLE_IDENTIFIER)"; + INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MAIN_APP_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER)"; + MARKETING_VERSION = "$(XDRIP_MARKETING_VERSION)"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; }; name = Release; }; @@ -5014,16 +5445,17 @@ F8AC426F21ADEBD70078C348 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "$(XDRIP_ENTITLEMENTS_DEBUG)"; - CODE_SIGN_IDENTITY = "$(XDRIP_CODE_SIGN_IDENTITY_DEBUG)"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = "$(XDRIP_CODE_SIGN_STYLE)"; CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; - INFOPLIST_FILE = "$(SRCROOT)/xdrip/Supporting Files/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + INFOPLIST_FILE = "xdrip/Supporting Files/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -5042,16 +5474,17 @@ F8AC427021ADEBD70078C348 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "$(XDRIP_ENTITLEMENTS_RELEASE)"; - CODE_SIGN_IDENTITY = "(XDRIP_CODE_SIGN_IDENTITY_RELEASE)"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = "$(XDRIP_CODE_SIGN_STYLE)"; CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; - INFOPLIST_FILE = "$(SRCROOT)/xdrip/Supporting Files/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + INFOPLIST_FILE = "xdrip/Supporting Files/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -5069,20 +5502,29 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 470B61A9270C448200561E56 /* Build configuration list for PBXNativeTarget "Watch App" */ = { + 4716A5012B406C3F00419052 /* Build configuration list for PBXNativeTarget "xDrip Widget Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( - 470B61A3270C448200561E56 /* Debug */, - 470B61A4270C448200561E56 /* Release */, + 4716A4FF2B406C3F00419052 /* Debug */, + 4716A5002B406C3F00419052 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F870D3E125126A49008967B0 /* Build configuration list for PBXNativeTarget "xDrip4iOS Widget" */ = { + 479359942B88B95B007D3CEE /* Build configuration list for PBXNativeTarget "xDrip Watch Complication Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( - F870D3DF25126A49008967B0 /* Debug */, - F870D3E025126A49008967B0 /* Release */, + 479359922B88B95B007D3CEE /* Debug */, + 479359932B88B95B007D3CEE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 47A6ABEC2B790CC70047A4BA /* Build configuration list for PBXNativeTarget "xDrip Watch App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 47A6ABEA2B790CC70047A4BA /* Debug */, + 47A6ABEB2B790CC70047A4BA /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/xdrip.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/xdrip.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 48776ea7..00000000 --- a/xdrip.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,41 +0,0 @@ -{ - "pins" : [ - { - "identity" : "actionclosurable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/takasek/ActionClosurable.git", - "state" : { - "revision" : "a1a589cd4189768c0ac7a49da7296d95a7f4f914", - "version" : "2.1.0" - } - }, - { - "identity" : "cryptoswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", - "state" : { - "revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f", - "version" : "1.7.2" - } - }, - { - "identity" : "piecharts", - "kind" : "remoteSourceControl", - "location" : "https://github.com/paulplant/PieCharts", - "state" : { - "branch" : "master", - "revision" : "15128eb4d1126dcdca4a5b665cece6be9f5dac91" - } - }, - { - "identity" : "swiftcharts", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ivanschuetz/SwiftCharts", - "state" : { - "branch" : "master", - "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2" - } - } - ], - "version" : 2 -} diff --git a/xdrip.xcodeproj/xcshareddata/xcschemes/Watch App.xcscheme b/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Watch App.xcscheme similarity index 66% rename from xdrip.xcodeproj/xcshareddata/xcschemes/Watch App.xcscheme rename to xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Watch App.xcscheme index c83d9884..fc441051 100644 --- a/xdrip.xcodeproj/xcshareddata/xcschemes/Watch App.xcscheme +++ b/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Watch App.xcscheme @@ -1,7 +1,7 @@ + LastUpgradeVersion = "1520" + version = "1.7"> @@ -14,23 +14,9 @@ buildForAnalyzing = "YES"> - - - - @@ -40,9 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> @@ -75,9 +60,9 @@ runnableDebuggingMode = "0"> diff --git a/xdrip.xcodeproj/xcshareddata/xcschemes/Watch App (Complication).xcscheme b/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Watch Complication Extension.xcscheme similarity index 51% rename from xdrip.xcodeproj/xcshareddata/xcschemes/Watch App (Complication).xcscheme rename to xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Watch Complication Extension.xcscheme index 1417a996..4c34d32d 100644 --- a/xdrip.xcodeproj/xcshareddata/xcschemes/Watch App (Complication).xcscheme +++ b/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Watch Complication Extension.xcscheme @@ -1,6 +1,7 @@ + + + + @@ -40,46 +55,71 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + launchAutomaticallySubstyle = "2"> + + + + + + + + + + + + + debugDocumentVersioning = "YES"> diff --git a/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip4iOS Widget.xcscheme b/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Widget Extension.xcscheme similarity index 79% rename from xdrip.xcodeproj/xcshareddata/xcschemes/xDrip4iOS Widget.xcscheme rename to xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Widget Extension.xcscheme index af00631d..b0c1f427 100644 --- a/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip4iOS Widget.xcscheme +++ b/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Widget Extension.xcscheme @@ -1,6 +1,6 @@ @@ -41,9 +41,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + + + + + + + + Int { diff --git a/xdrip/BluetoothPeripheral/Generic/BluetoothPeripheralType.swift b/xdrip/BluetoothPeripheral/Generic/BluetoothPeripheralType.swift index 8b76fdf6..767e7c16 100644 --- a/xdrip/BluetoothPeripheral/Generic/BluetoothPeripheralType.swift +++ b/xdrip/BluetoothPeripheral/Generic/BluetoothPeripheralType.swift @@ -50,7 +50,7 @@ enum BluetoothPeripheralType: String, CaseIterable { case AtomType = "Atom" /// to use a Libre 3 as heartbeat - case Libre3HeartBeatType = "Libre 3 HeartBeat" + case Libre3HeartBeatType = "Libre HeartBeat" /// DexcomG7 heartbeat case DexcomG7HeartBeatType = "Dexcom G7 HeartBeat" diff --git a/xdrip/BluetoothTransmitter/CGM/Dexcom/G5/CGMG5Transmitter.swift b/xdrip/BluetoothTransmitter/CGM/Dexcom/G5/CGMG5Transmitter.swift index 93e37130..1d37c1db 100644 --- a/xdrip/BluetoothTransmitter/CGM/Dexcom/G5/CGMG5Transmitter.swift +++ b/xdrip/BluetoothTransmitter/CGM/Dexcom/G5/CGMG5Transmitter.swift @@ -289,7 +289,11 @@ class CGMG5Transmitter:BluetoothTransmitter, CGMTransmitter { override func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - super.centralManager(central, didDisconnectPeripheral: peripheral, error: error) + let delayInSeconds = 2.0 + + DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { + super.centralManager(central, didDisconnectPeripheral: peripheral, error: error) + } if waitingPairingConfirmation { // device has requested a pairing request and is now in a status of verifying if pairing was successfull or not, this by doing setNotify to writeCharacteristic. If a disconnect occurs now, it means pairing has failed (probably because user didn't approve it diff --git a/xdrip/BluetoothTransmitter/CGM/Dexcom/G7/CGMG7Transmitter.swift b/xdrip/BluetoothTransmitter/CGM/Dexcom/G7/CGMG7Transmitter.swift index 2e95e506..681bcb98 100644 --- a/xdrip/BluetoothTransmitter/CGM/Dexcom/G7/CGMG7Transmitter.swift +++ b/xdrip/BluetoothTransmitter/CGM/Dexcom/G7/CGMG7Transmitter.swift @@ -98,6 +98,9 @@ class CGMG7Transmitter:BluetoothTransmitter, CGMTransmitter { } } + /// after subscribing to receiveAuthenticationCharacteristic (which is also used for authentication), and if not receiving a successful authentication within 2 seconds, then this is not the correct device + private var authenticationTimeOutTimer: Timer? + // MARK: - public functions /// - parameters: @@ -261,6 +264,8 @@ class CGMG7Transmitter:BluetoothTransmitter, CGMTransmitter { bluetoothTransmitterDelegate?.didConnectTo(bluetoothTransmitter: self) + cancelAuthenticationTimer() + } else { trace("Connected to Dexcom G7 that is not paired and/or authenticated by other app. Will disconnect and scan for another Dexcom G7", log: log, category: ConstantsLog.categoryCGMG7, type: .info ) @@ -308,6 +313,14 @@ class CGMG7Transmitter:BluetoothTransmitter, CGMTransmitter { trace(" characteristic: %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .info, String(describing: characteristic.uuid)) peripheral.setNotifyValue(true, for: characteristic) + + if characteristic.uuid == CBUUID(string: CBUUID_Characteristic_UUID.CBUUID_Receive_Authentication.rawValue) { + + // this is the authentication characteristic, if the authentication is not successful within 2 seconds, then this is not the device that is currently being used by the official dexcom app, so let's forget it + authenticationTimeOutTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(authenticationFailed), userInfo: nil, repeats: false) + + } + } } else { trace(" Did discover characteristics, but no characteristics listed. There must be some error.", log: log, category: ConstantsLog.categoryCGMG7, type: .error) @@ -318,10 +331,12 @@ class CGMG7Transmitter:BluetoothTransmitter, CGMTransmitter { override func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let error = error, error.localizedDescription.contains(find: "Encryption is insufficient") { - trace("didUpdateNotificationStateFor for peripheral with name %{public}@, characteristic %{public}@, error contains Encryption is insufficient. This is not the device we're looking for.", log: log, category: ConstantsLog.categoryCGMG7, type: .info) + trace("didUpdateNotificationStateFor for peripheral with name %{public}@, characteristic %{public}@, error contains Encryption is insufficient. This is not the device we're looking for.", log: log, category: ConstantsLog.categoryCGMG7, type: .info, (deviceName != nil ? deviceName! : "unknown"), String(describing: characteristic.uuid)) // it's not the device we're interested in, disconnect, forget this device, and restart scanning for a new, other device + cancelAuthenticationTimer() + disconnectAndForget() _ = startScanning() @@ -375,4 +390,24 @@ class CGMG7Transmitter:BluetoothTransmitter, CGMTransmitter { } + // MARK: - private functions + @objc private func authenticationFailed() { + + trace("Connected to Dexcom G7 but authentication not received. Will disconnect and scan for another Dexcom G7", log: log, category: ConstantsLog.categoryCGMG7, type: .info ) + + disconnectAndForget() + + _ = startScanning() + + } + + private func cancelAuthenticationTimer() { + if let authenticationTimeOutTimer = authenticationTimeOutTimer { + if authenticationTimeOutTimer.isValid { + authenticationTimeOutTimer.invalidate() + self.authenticationTimeOutTimer = nil + } + } + } + } diff --git a/xdrip/BluetoothTransmitter/CGM/Libre/Libre2/CGMLibre2Transmitter.swift b/xdrip/BluetoothTransmitter/CGM/Libre/Libre2/CGMLibre2Transmitter.swift index 1f814fa1..6f2af5e8 100644 --- a/xdrip/BluetoothTransmitter/CGM/Libre/Libre2/CGMLibre2Transmitter.swift +++ b/xdrip/BluetoothTransmitter/CGM/Libre/Libre2/CGMLibre2Transmitter.swift @@ -10,13 +10,13 @@ class CGMLibre2Transmitter:BluetoothTransmitter, CGMTransmitter { // MARK: - properties /// service to be discovered - private let CBUUID_Service_OmniPod: String = "FDE3" + private let CBUUID_Service_Libre2: String = "FDE3" /// receive characteristic - private let CBUUID_ReceiveCharacteristic_OmniPod: String = "F002" + private let CBUUID_ReceiveCharacteristic_Libre2: String = "F002" /// write characteristic - private let CBUUID_WriteCharacteristic_OmniPod: String = "F001" + private let CBUUID_WriteCharacteristic_Libre2: String = "F001" /// how many bytes should we receive from Libre 2 private let expectedBufferSize = 46 @@ -101,7 +101,7 @@ class CGMLibre2Transmitter:BluetoothTransmitter, CGMTransmitter { // initialize webOOPEnabled self.webOOPEnabled = webOOPEnabled ?? false - super.init(addressAndName: newAddressAndName, CBUUID_Advertisement: nil, servicesCBUUIDs: [CBUUID(string: CBUUID_Service_OmniPod)], CBUUID_ReceiveCharacteristic: CBUUID_ReceiveCharacteristic_OmniPod, CBUUID_WriteCharacteristic: CBUUID_WriteCharacteristic_OmniPod, bluetoothTransmitterDelegate: bluetoothTransmitterDelegate) + super.init(addressAndName: newAddressAndName, CBUUID_Advertisement: nil, servicesCBUUIDs: [CBUUID(string: CBUUID_Service_Libre2)], CBUUID_ReceiveCharacteristic: CBUUID_ReceiveCharacteristic_Libre2, CBUUID_WriteCharacteristic: CBUUID_WriteCharacteristic_Libre2, bluetoothTransmitterDelegate: bluetoothTransmitterDelegate) } @@ -358,11 +358,11 @@ class CGMLibre2Transmitter:BluetoothTransmitter, CGMTransmitter { } func getCBUUID_Service() -> String { - return CBUUID_Service_OmniPod + return CBUUID_Service_Libre2 } func getCBUUID_Receive() -> String { - return CBUUID_ReceiveCharacteristic_OmniPod + return CBUUID_ReceiveCharacteristic_Libre2 } } diff --git a/xdrip/BluetoothTransmitter/HeartBeat/DexcomG7HeartbeatBluetoothTransmitter.swift b/xdrip/BluetoothTransmitter/HeartBeat/DexcomG7HeartbeatBluetoothTransmitter.swift index 24bf710b..134ec8ef 100644 --- a/xdrip/BluetoothTransmitter/HeartBeat/DexcomG7HeartbeatBluetoothTransmitter.swift +++ b/xdrip/BluetoothTransmitter/HeartBeat/DexcomG7HeartbeatBluetoothTransmitter.swift @@ -30,7 +30,7 @@ class DexcomG7HeartbeatBluetoothTransmitter: BluetoothTransmitter { private let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryHeartBeatG7) /// when was the last heartbeat - private var lastHeartBeatTimeStamp: Date + private var timeStampOfLastHeartBeat: Date // MARK: - Initialization /// - parameters: @@ -48,7 +48,7 @@ class DexcomG7HeartbeatBluetoothTransmitter: BluetoothTransmitter { } // initially last heartbeat was never (ie 1 1 1970) - self.lastHeartBeatTimeStamp = Date(timeIntervalSince1970: 0) + self.timeStampOfLastHeartBeat = Date(timeIntervalSince1970: 0) super.init(addressAndName: newAddressAndName, CBUUID_Advertisement: CBUUID_Advertisement_G7, servicesCBUUIDs: [CBUUID(string: CBUUID_Service_G7)], CBUUID_ReceiveCharacteristic: CBUUID_ReceiveCharacteristic_G7, CBUUID_WriteCharacteristic: CBUUID_WriteCharacteristic_G7, bluetoothTransmitterDelegate: bluetoothTransmitterDelegate) @@ -66,17 +66,35 @@ class DexcomG7HeartbeatBluetoothTransmitter: BluetoothTransmitter { } // this is the trigger for calling the heartbeat - if (Date()).timeIntervalSince(lastHeartBeatTimeStamp) > ConstantsHeartBeat.minimumTimeBetweenTwoHeartBeats { + if (Date()).timeIntervalSince(timeStampOfLastHeartBeat) > ConstantsHeartBeat.minimumTimeBetweenTwoHeartBeats { - // sleep for a second to allow Loop to read the readings and upload them to NS - Thread.sleep(forTimeInterval: 1) - - self.bluetoothTransmitterDelegate?.heartBeat() - - lastHeartBeatTimeStamp = Date() - + timeStampOfLastHeartBeat = Date() + + UserDefaults.standard.timeStampOfLastHeartBeat = timeStampOfLastHeartBeat + + // wait for a second to allow the official app to upload to LibreView before triggering the heartbeat announcement to the delegate + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.bluetoothTransmitterDelegate?.heartBeat() + } } } + + override func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + + super.centralManager(central, didDisconnectPeripheral: peripheral, error: error) + + // this is the trigger for calling the heartbeat + if (Date()).timeIntervalSince(timeStampOfLastHeartBeat) > ConstantsHeartBeat.minimumTimeBetweenTwoHeartBeats { + + timeStampOfLastHeartBeat = Date() + + UserDefaults.standard.timeStampOfLastHeartBeat = timeStampOfLastHeartBeat + + // no need to wait for a second, because the disconnect usually happens about 1' seconds after connect + // this case is for when a follower would be using an expired Dexcom G7 as a heartbeat + self.bluetoothTransmitterDelegate?.heartBeat() + } + } } diff --git a/xdrip/BluetoothTransmitter/HeartBeat/Libre3HeartbeatBluetoothTransmitter.swift b/xdrip/BluetoothTransmitter/HeartBeat/Libre3HeartbeatBluetoothTransmitter.swift index 3da5e8f4..f4828e42 100644 --- a/xdrip/BluetoothTransmitter/HeartBeat/Libre3HeartbeatBluetoothTransmitter.swift +++ b/xdrip/BluetoothTransmitter/HeartBeat/Libre3HeartbeatBluetoothTransmitter.swift @@ -28,7 +28,7 @@ class Libre3HeartBeatBluetoothTransmitter: BluetoothTransmitter { private let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryHeartBeatLibre3) /// when was the last heartbeat - private var lastHeartBeatTimeStamp: Date + private var timeStampOfLastHeartBeat: Date // MARK: - Initialization /// - parameters: @@ -47,7 +47,7 @@ class Libre3HeartBeatBluetoothTransmitter: BluetoothTransmitter { } // initially last heartbeat was never (ie 1 1 1970) - self.lastHeartBeatTimeStamp = Date(timeIntervalSince1970: 0) + self.timeStampOfLastHeartBeat = Date(timeIntervalSince1970: 0) // using nil as servicesCBUUIDs, that works. super.init(addressAndName: newAddressAndName, CBUUID_Advertisement: CBUUID_Advertisement_Libre3, servicesCBUUIDs: nil, CBUUID_ReceiveCharacteristic: CBUUID_ReceiveCharacteristic_Libre3, CBUUID_WriteCharacteristic: CBUUID_WriteCharacteristic_Libre3, bluetoothTransmitterDelegate: bluetoothTransmitterDelegate) @@ -56,6 +56,25 @@ class Libre3HeartBeatBluetoothTransmitter: BluetoothTransmitter { // MARK: CBCentralManager overriden functions + override func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + + super.centralManager(central, didConnect: peripheral) + + // this is the trigger for calling the heartbeat + if (Date()).timeIntervalSince(timeStampOfLastHeartBeat) > ConstantsHeartBeat.minimumTimeBetweenTwoHeartBeats { + + timeStampOfLastHeartBeat = Date() + + UserDefaults.standard.timeStampOfLastHeartBeat = timeStampOfLastHeartBeat + + // wait for a second to allow the official app to upload to LibreView before triggering the heartbeat announcement to the delegate + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.bluetoothTransmitterDelegate?.heartBeat() + } + } + + } + override func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { // trace the received value and uuid @@ -64,17 +83,35 @@ class Libre3HeartBeatBluetoothTransmitter: BluetoothTransmitter { } // this is the trigger for calling the heartbeat - if (Date()).timeIntervalSince(lastHeartBeatTimeStamp) > ConstantsHeartBeat.minimumTimeBetweenTwoHeartBeats { + if (Date()).timeIntervalSince(timeStampOfLastHeartBeat) > ConstantsHeartBeat.minimumTimeBetweenTwoHeartBeats { - // sleep for a second to allow the official app to upload to LibreView - Thread.sleep(forTimeInterval: 1) + timeStampOfLastHeartBeat = Date() - bluetoothTransmitterDelegate?.heartBeat() - - lastHeartBeatTimeStamp = Date() + UserDefaults.standard.timeStampOfLastHeartBeat = timeStampOfLastHeartBeat + // wait for a second to allow the official app to upload to LibreView before triggering the heartbeat announcement to the delegate + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.bluetoothTransmitterDelegate?.heartBeat() + } } } + override func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + + trace("didDiscoverCharacteristicsFor for peripheral with name %{public}@, for service with uuid %{public}@", log: log, category: ConstantsLog.categoryHeartBeatLibre3, type: .info, deviceName ?? "'unknown'", String(describing:service.uuid)) + + if let error = error { + trace(" didDiscoverCharacteristicsFor error: %{public}@", log: log, category: ConstantsLog.categoryBlueToothTransmitter, type: .error , error.localizedDescription) + } + + if let characteristics = service.characteristics { + for characteristic in characteristics { + peripheral.setNotifyValue(true, for: characteristic) + } + } else { + trace(" Did discover characteristics, but no characteristics listed. There must be some error.", log: log, category: ConstantsLog.categoryHeartBeatLibre3, type: .error) + } + } + } diff --git a/xdrip/BluetoothTransmitter/HeartBeat/OmniPodHeartbeatBluetoothTransmitter.swift b/xdrip/BluetoothTransmitter/HeartBeat/OmniPodHeartbeatBluetoothTransmitter.swift index 6b79b41d..bad696f5 100644 --- a/xdrip/BluetoothTransmitter/HeartBeat/OmniPodHeartbeatBluetoothTransmitter.swift +++ b/xdrip/BluetoothTransmitter/HeartBeat/OmniPodHeartbeatBluetoothTransmitter.swift @@ -17,10 +17,10 @@ class OmniPodHeartBeatTransmitter: BluetoothTransmitter { /// service to be discovered private let CBUUID_Service_OmniPod: String = "1A7E4024-E3ED-4464-8B7E-751E03D0DC5F" - + /// advertisement private let CBUUID_Advertisement_OmniPod: String? = "00004024-0000-1000-8000-00805f9b34fb" - + /// receive characteristic private let CBUUID_ReceiveCharacteristic_OmniPod: String = "1A7E2442-E3ED-4464-8B7E-751E03D0DC5F" @@ -31,15 +31,15 @@ class OmniPodHeartBeatTransmitter: BluetoothTransmitter { private let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryHeartBeatOmnipod) /// when was the last heartbeat - private var lastHeartBeatTimeStamp: Date - + private var timeStampOfLastHeartBeat: Date + // MARK: - Initialization /// - parameters: /// - address: if already connected before, then give here the address that was received during previous connect, if not give nil /// - name : if already connected before, then give here the name that was received during previous connect, if not give nil /// - bluetoothTransmitterDelegate : a bluetoothTransmitterDelegate init(address:String?, name: String?, bluetoothTransmitterDelegate: BluetoothTransmitterDelegate) { - + // then device name of each omnipod contains "TWI BOARD" var newAddressAndName:BluetoothTransmitter.DeviceAddressAndName = BluetoothTransmitter.DeviceAddressAndName.notYetConnected(expectedName: "BOARD") @@ -49,8 +49,8 @@ class OmniPodHeartBeatTransmitter: BluetoothTransmitter { } // initially last heartbeat was never (ie 1 1 1970) - self.lastHeartBeatTimeStamp = Date(timeIntervalSince1970: 0) - + self.timeStampOfLastHeartBeat = Date(timeIntervalSince1970: 0) + super.init(addressAndName: newAddressAndName, CBUUID_Advertisement: CBUUID_Advertisement_OmniPod, servicesCBUUIDs: [CBUUID(string: CBUUID_Service_OmniPod)], CBUUID_ReceiveCharacteristic: CBUUID_ReceiveCharacteristic_OmniPod, CBUUID_WriteCharacteristic: CBUUID_WriteCharacteristic_OmniPod, bluetoothTransmitterDelegate: bluetoothTransmitterDelegate) } @@ -58,33 +58,32 @@ class OmniPodHeartBeatTransmitter: BluetoothTransmitter { // MARK: CBCentralManager overriden functions override func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - + super.centralManager(central, didConnect: peripheral) - // this is a trigger for calling the heartbeat - if (Date()).timeIntervalSince(lastHeartBeatTimeStamp) > ConstantsHeartBeat.minimumTimeBetweenTwoHeartBeats { - - bluetoothTransmitterDelegate?.heartBeat() - - lastHeartBeatTimeStamp = Date() - + timeStampOfLastHeartBeat = Date() + + UserDefaults.standard.timeStampOfLastHeartBeat = timeStampOfLastHeartBeat + + // wait for a second to allow the official app to upload to LibreView before triggering the heartbeat announcement to the delegate + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.bluetoothTransmitterDelegate?.heartBeat() } - + } - override func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { - + super.peripheral(peripheral, didUpdateValueFor: characteristic, error: error) - - // this is a trigger for calling the heartbeat - if (Date()).timeIntervalSince(lastHeartBeatTimeStamp) > ConstantsHeartBeat.minimumTimeBetweenTwoHeartBeats { - - bluetoothTransmitterDelegate?.heartBeat() - - lastHeartBeatTimeStamp = Date() - + + timeStampOfLastHeartBeat = Date() + + UserDefaults.standard.timeStampOfLastHeartBeat = timeStampOfLastHeartBeat + + // wait for a second to allow the official app to upload to LibreView before triggering the heartbeat announcement to the delegate + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.bluetoothTransmitterDelegate?.heartBeat() } } diff --git a/xdrip/Constants/ConstantsWatch.swift b/xdrip/Constants/ConstantsCalendar.swift similarity index 97% rename from xdrip/Constants/ConstantsWatch.swift rename to xdrip/Constants/ConstantsCalendar.swift index 8f561cb3..ae264bd9 100644 --- a/xdrip/Constants/ConstantsWatch.swift +++ b/xdrip/Constants/ConstantsCalendar.swift @@ -1,6 +1,6 @@ import Foundation -enum ConstantsWatch { +enum ConstantsCalendar { /// text to add as notes in glucose events static let textInCreatedEvent = "created by xDrip4iOS" diff --git a/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift b/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift new file mode 100644 index 00000000..3de70f98 --- /dev/null +++ b/xdrip/Constants/ConstantsGlucoseChartSwiftUI.swift @@ -0,0 +1,145 @@ +// +// ConstantsGlucoseChartSwiftUI.swift +// xDripWidgetExtension +// +// Created by Paul Plant on 31/12/23. +// Copyright © 2023 Johan Degraeve. All rights reserved. +// + +import SwiftUI + +enum ConstantsGlucoseChartSwiftUI { + + static let mmollToMgdl = 18.01801801801802 + static let mgDlToMmoll = 0.0555 + + /// application name, appears in licenseInfo as title + static let applicationName: String = { + + guard let dictionary = Bundle.main.infoDictionary else {return "unknown"} + + guard let version = dictionary["CFBundleDisplayName"] as? String else {return "unknown"} + + return version + + }() + + + // live activity + static let viewWidthLiveActivityNormal: CGFloat = 180 + static let viewHeightLiveActivityNormal: CGFloat = 70 + static let hoursToShowLiveActivityNormal: Double = 3 + static let intervalBetweenXAxisValuesLiveActivityNormal: Int = 1 + static let glucoseCircleDiameterLiveActivityNormal: Double = 36 + + static let viewWidthLiveActivityLarge: CGFloat = 340 + static let viewHeightLiveActivityLarge: CGFloat = 90 + static let hoursToShowLiveActivityLarge: Double = 8 + static let intervalBetweenXAxisValuesLiveActivityLarge: Int = 1 + static let glucoseCircleDiameterLiveActivityLarge: Double = 24 + + static let lowHighLineColorLiveActivity = Color(white: 0.6) + static let urgentLowHighLineLiveActivity = Color(white: 0.4) + static let xAxisGridLineColorLiveActivity = Color(white: 0.4) + static let relativeYAxisLineSizeLiveActivity: Double = 1 + static let xAxisLabelOffsetLiveActivity: Double = -10 + + // dynamic island bottom (expanded) + static let viewWidthDynamicIsland: CGFloat = 330 + static let viewHeightDynamicIsland: CGFloat = 70 + + static let lowHighLineColorDynamicIsland = Color(white: 0.6) + static let urgentLowHighLineColorDynamicIsland = Color(white: 0.4) + static let xAxisGridLineColorDynamicIsland = Color(white: 0.4) + static let hoursToShowDynamicIsland: Double = 12 + static let intervalBetweenXAxisValuesDynamicIsland: Int = 2 + static let glucoseCircleDiameterDynamicIsland: Double = 14 + static let relativeYAxisLineSizeDynamicIsland: Double = 0.8 + static let xAxisLabelOffsetDynamicIsland: Double = -10 + + // watch chart + static let viewWidthWatchApp: CGFloat = 190 + static let viewHeightWatchApp: CGFloat = 90 + + // watch chart sizes for smaller watches + static let viewWidthWatchAppSmall: CGFloat = 155 + static let viewHeightWatchAppSmall: CGFloat = 75 + + static let lowHighLineColorWatchApp = Color(white: 0.6) + static let urgentLowHighLineColorWatchApp = Color(white: 0.4) + static let xAxisGridLineColorWatchApp = Color(white: 0.3) + static let hoursToShowWatchApp: Double = 4 + static let intervalBetweenXAxisValuesWatchApp: Int = 1 + static let glucoseCircleDiameterWatchApp: Double = 20 + static let relativeYAxisLineSizeWatchApp: Double = 0.8 + static let xAxisLabelOffsetWatchApp: Double = -10 + + // watch complication .accessoryRectangular chart + static let viewWidthWatchAccessoryRectangular: CGFloat = 180 + static let viewHeightWatchAccessoryRectangular: CGFloat = 55 + + // watch complication sizes for smaller watches + static let viewWidthWatchAccessoryRectangularSmall: CGFloat = 160 + static let viewHeightWatchAccessoryRectangularSmall: CGFloat = 45 + + static let lowHighLineColorWatchAccessoryRectangular = Color(white: 0.7) + static let urgentLowHighLineColorWatchAccessoryRectangular = Color(white: 0.5) + static let xAxisGridLineColorWatchAccessoryRectangular = Color(white: 0.4) + static let hoursToShowWatchAccessoryRectangular: Double = 5 + static let intervalBetweenXAxisValuesWatchAccessoryRectangular: Int = 1 + static let glucoseCircleDiameterWatchAccessoryRectangular: Double = 14 + static let relativeYAxisLineSizeWatchAccessoryRectangular: Double = 0.8 + static let xAxisLabelOffsetWatchAccessoryRectangular: Double = -10 + + // widget systemSmall chart + static let viewWidthWidgetSystemSmall: CGFloat = 120 + static let viewHeightWidgetSystemSmall: CGFloat = 80 + + static let lowHighLineColorWidgetSystemSmall = Color(white: 0.6) + static let urgentLowHighLineColorWidgetSystemSmall = Color(white: 0.4) + static let xAxisGridLineColorWidgetSystemSmall = Color(white: 0.3) + static let hoursToShowWidgetSystemSmall: Double = 3 + static let intervalBetweenXAxisValuesWidgetSystemSmall: Int = 1 + static let glucoseCircleDiameterWidgetSystemSmall: Double = 20 + static let relativeYAxisLineSizeWidgetSystemSmall: Double = 0.8 + static let xAxisLabelOffsetWidgetSystemSmall: Double = -10 + + // widget systemMedium chart + static let viewWidthWidgetSystemMedium: CGFloat = 300 + static let viewHeightWidgetSystemMedium: CGFloat = 80 + + static let lowHighLineColorWidgetSystemMedium = Color(white: 0.6) + static let urgentLowHighLineColorWidgetSystemMedium = Color(white: 0.4) + static let xAxisGridLineColorWidgetSystemMedium = Color(white: 0.3) + static let hoursToShowWidgetSystemMedium: Double = 8 + static let intervalBetweenXAxisValuesWidgetSystemMedium: Int = 1 + static let glucoseCircleDiameterWidgetSystemMedium: Double = 14 + static let relativeYAxisLineSizeWidgetSystemMedium: Double = 0.8 + static let xAxisLabelOffsetWidgetSystemMedium: Double = -10 + + // widget systemLarge chart + static let viewWidthWidgetSystemLarge: CGFloat = 300 + static let viewHeightWidgetSystemLarge: CGFloat = 260 + + static let lowHighLineColorWidgetSystemLarge = Color(white: 0.6) + static let urgentLowHighLineColorWidgetSystemLarge = Color(white: 0.4) + static let xAxisGridLineColorWidgetSystemLarge = Color(white: 0.3) + static let hoursToShowWidgetSystemLarge: Double = 4 + static let intervalBetweenXAxisValuesWidgetSystemLarge: Int = 1 + static let glucoseCircleDiameterWidgetSystemLarge: Double = 30 + static let relativeYAxisLineSizeWidgetSystemLarge: Double = 0.8 + static let xAxisLabelOffsetWidgetSystemLarge: Double = -10 + + // widget (lock screen) .accessoryRectangular chart + static let viewWidthWidgetAccessoryRectangular: CGFloat = 130 + static let viewHeightWidgetAccessoryRectangular: CGFloat = 40 + + static let lowHighLineColorWidgetAccessoryRectangular = Color(white: 0.7) + static let urgentLowHighLineColorWidgetAccessoryRectangular = Color(white: 0.5) + static let xAxisGridLineColorWidgetAccessoryRectangular = Color(white: 0.4) + static let hoursToShowWidgetAccessoryRectangular: Double = 4 + static let intervalBetweenXAxisValuesWidgetAccessoryRectangular: Int = 1 + static let glucoseCircleDiameterWidgetAccessoryRectangular: Double = 14 + static let relativeYAxisLineSizeWidgetAccessoryRectangular: Double = 0.8 + static let xAxisLabelOffsetWidgetAccessoryRectangular: Double = -10 +} diff --git a/xdrip/Constants/ConstantsHeartBeat.swift b/xdrip/Constants/ConstantsHeartBeat.swift index 112c8ead..ecdf40fb 100644 --- a/xdrip/Constants/ConstantsHeartBeat.swift +++ b/xdrip/Constants/ConstantsHeartBeat.swift @@ -5,6 +5,14 @@ enum ConstantsHeartBeat { /// minimum time between two heartbeats static let minimumTimeBetweenTwoHeartBeats = TimeInterval(30) + /// how many seconds should pass since the previous Libre 3 BLE heartbeat until we show it as disconnected (i.e. having missed a heartbeat) + static let secondsUntilHeartBeatDisconnectWarningLibre3: Double = 70 + + /// how many seconds should pass since the previous Dexcom G7 heartbeat until we show it as disconnected (i.e. having missed a heartbeat) + static let secondsUntilHeartBeatDisconnectWarningDexcomG7: Double = 60 * 5.5 + + /// how many seconds should pass since the previous OmniPod heartbeat until we show it as disconnected (i.e. having missed a heartbeat) + static let secondsUntilHeartBeatDisconnectWarningOmniPod: Double = 60 * 5.5 } diff --git a/xdrip/Constants/ConstantsHomeView.swift b/xdrip/Constants/ConstantsHomeView.swift index 6d176655..f8d77ce0 100644 --- a/xdrip/Constants/ConstantsHomeView.swift +++ b/xdrip/Constants/ConstantsHomeView.swift @@ -1,4 +1,8 @@ /// constants for home view, ie first view + +import UIKit +import SwiftUI + enum ConstantsHomeView { /// how often to update the labels in the homeview (ie label with latest reading, minutes ago, etc..) @@ -42,17 +46,22 @@ enum ConstantsHomeView { /// warning time left / colour static let sensorProgressViewWarningInMinutes: Double = 60 * 24.0 // 24 hours before the sensor reaches max age - static let sensorProgressViewProgressColorWarning: UIColor = .systemYellow + static let sensorProgressViewProgressColorWarning: UIColor = .yellow + static let sensorProgressViewProgressColorWarningSwiftUI: Color = .yellow /// urgent time left / colour static let sensorProgressViewUrgentInMinutes: Double = 60 * 12.0 // 12 hours before the sensor reaches max age - static let sensorProgressViewProgressColorUrgent: UIColor = .systemRed + static let sensorProgressViewProgressColorUrgent: UIColor = .red + static let sensorProgressViewProgressColorUrgentSwiftUI: Color = .red /// colour for an expired sensor - static let sensorProgressExpired: UIColor = .systemPurple + static let sensorProgressExpired: UIColor = .purple + static let sensorProgressExpiredSwiftUI: Color = .purple /// colour for an normal text static let sensorProgressNormalTextColor: UIColor = .lightGray + static let sensorProgressNormalTextColorSwiftUI: Color = .white + static let sensorProgressViewNormalColorSwiftUI: Color = .gray /// colour for the dimmed screen lock overlay view static let screenLockDimmingOptionsDimmed: UIColor = .black.withAlphaComponent(0.3) diff --git a/xdrip/Constants/ConstantsLiveActivity.swift b/xdrip/Constants/ConstantsLiveActivity.swift new file mode 100644 index 00000000..6da2bc16 --- /dev/null +++ b/xdrip/Constants/ConstantsLiveActivity.swift @@ -0,0 +1,22 @@ +// +// ConstantsLiveActivity.swift +// xDripWidgetExtension +// +// Created by Paul Plant on 19/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation + +enum ConstantsLiveActivity { + // restart the live activity after time in (minutes) + // this is to prevent it being restarted too often + static let allowLiveActivityRestartAfterMinutes: Double = 4 * 60 * 60 + + // warn that live activity will soon end (in minutes) + static let warnLiveActivityAfterMinutes: Double = 7.25 * 60 * 60 + + // end live activity after time in (minutes) we give a bit of margin + // in case there is a missed reading (and therefore no update cycle) towards the end + static let endLiveActivityAfterMinutes: Double = 7.75 * 60 * 60 +} diff --git a/xdrip/Constants/ConstantsLog.swift b/xdrip/Constants/ConstantsLog.swift index e9b9f75d..70fffaed 100644 --- a/xdrip/Constants/ConstantsLog.swift +++ b/xdrip/Constants/ConstantsLog.swift @@ -7,83 +7,86 @@ enum ConstantsLog { /// for use in NSLog static let tracePrefix = "xDrip-NSLog" - /// for use in OSLog - static let categoryBlueToothTransmitter = "BlueToothTransmitter " - - /// for use in cgm transmitter miaomiao - static let categoryCGMMiaoMiao = "CGMMiaoMiao " - - /// for use in cgm transmitter bubble - static let categoryCGMBubble = "CGMBubble " - - /// for use in cgm xdripg4 - static let categoryCGMxDripG4 = "CGMxDripG4 " - - /// for use in firstview - static let categoryRootView = "RootView " - - /// calibration - static let categoryCalibration = "Calibration " - /// debuglogging static let debuglogging = "xdripdebuglogging" - + /// timestamp format for nslog static let dateFormatNSLog = "y-MM-dd HH:mm:ss.SSSS" + + // MARK: - Categories + + /// for use in OSLog + static let categoryBlueToothTransmitter = "BlueToothTransmitter " + /// for use in cgm transmitter miaomiao + static let categoryCGMMiaoMiao = "CGMMiaoMiao " + + /// for use in cgm transmitter bubble + static let categoryCGMBubble = "CGMBubble " + + /// for use in cgm xdripg4 + static let categoryCGMxDripG4 = "CGMxDripG4 " + + /// for use in firstview + static let categoryRootView = "RootView " + + /// calibration + static let categoryCalibration = "Calibration " + /// G5 - static let categoryCGMG5 = "CGMG5 " + static let categoryCGMG5 = "CGMG5 " /// G7 static let categoryCGMG7 = "CGMG7 " /// watlaa - static let categoryWatlaa = "Watlaa" + static let categoryWatlaa = "Watlaa" /// GNSEntry - static let categoryCGMGNSEntry = "CGMGNSEntry " + static let categoryCGMGNSEntry = "CGMGNSEntry " /// Blucon - static let categoryBlucon = "Blucon " + static let categoryBlucon = "Blucon " /// Libre2 - static let categoryCGMLibre2 = "Libre2 " + static let categoryCGMLibre2 = "Libre2 " /// core data manager - static let categoryCoreDataManager = "CoreDataManager " + static let categoryCoreDataManager = "CoreDataManager " /// application data bgreadings - static let categoryApplicationDataBgReadings = "ApplicationDataBgReadings " + static let categoryApplicationDataBgReadings = "ApplicationDataBgReadings " /// application data Treatments - static let categoryApplicationDataTreatments = "ApplicationDataTreatments " + static let categoryApplicationDataTreatments = "ApplicationDataTreatments " /// application data calibrations - static let categoryApplicationDataCalibrations = "ApplicationDataCalibrations " + static let categoryApplicationDataCalibrations = "ApplicationDataCalibrations " /// application data sensors - static let categoryApplicationDataSensors = "ApplicationDataSensors " + static let categoryApplicationDataSensors = "ApplicationDataSensors " /// application data alerttypes - static let categoryApplicationDataAlertTypes = "ApplicationDataAlertTypes " + static let categoryApplicationDataAlertTypes = "ApplicationDataAlertTypes " /// application data alertentries - static let categoryApplicationDataAlertEntries = "ApplicationDataAlertEntries " + static let categoryApplicationDataAlertEntries = "ApplicationDataAlertEntries " /// application data for M5Stack - static let categoryApplicationDataM5Stacks = "ApplicationDataM5Stacks " + static let categoryApplicationDataM5Stacks = "ApplicationDataM5Stacks " /// application data for M5Stack - static let categoryApplicationDataWatlaa = "ApplicationDataWatlaa" + static let categoryApplicationDataWatlaa = "ApplicationDataWatlaa" /// application data for BLEPeripheral - static let categoryApplicationDataBLEPeripheral = "ApplicationDataBLEPeripheral" + static let categoryApplicationDataBLEPeripheral = + "ApplicationDataBLEPeripheral" /// application data for DexcomG5 - static let categoryApplicationDataDexcomG5 = "ApplicationDataDexcomG5" + static let categoryApplicationDataDexcomG5 = "ApplicationDataDexcomG5" /// application for for M5StackName - static let categoryApplicationDataM5StackNames = "ApplicationDataM5StackNames " + static let categoryApplicationDataM5StackNames = "ApplicationDataM5StackNames " /// nightscout uploader static let categoryNightScoutUploadManager = "NightScoutUploadManager " @@ -92,7 +95,7 @@ enum ConstantsLog { static let categoryNightScoutFollowManager = "NightScoutFollowManager " /// nightscout follow - static let categoryLibreLinkUpFollowManager = "LibreLinkUpFollowManager " + static let categoryLibreLinkUpFollowManager = "LibreLinkUpFollowManager " /// alertmanager static let categoryAlertManager = "AlertManager " @@ -136,11 +139,14 @@ enum ConstantsLog { /// SettingsViewCalendarEventsSettingsViewModel logging static let categorySettingsViewCalendarEventsSettingsViewModel = "SettingsViewCalendarEventsSettingsViewModel" + /// CalendarManager logging + static let categoryCalendarManager = "CalendarManager " + /// SettingsViewContactTrickSettingsViewModel logging static let categorySettingsViewContactTrickSettingsViewModel = "SettingsViewContactTrickSettingsViewModel " - + /// WatchManager logging - static let categoryWatchManager = "WatchManager " + static let categoryWatchManager = "WatchManager " /// ContactTrickManager logging static let categoryContactTrickManager = "ContactTrickManager " @@ -170,13 +176,13 @@ enum ConstantsLog { static let categoryLibre2BLEUtilities = "Libre2BLEUtilities " /// for use in Libre2BLEUtilities - static let categoryAppDelegate = "AppDelegate " + static let categoryAppDelegate = "AppDelegate " /// for use in DataExporter static let categoryDataExporter = "DataExporter " - /// for use in LoopManager - static let categoryLoopManager = "LoopManager " + // for use in LoopManager + static let categoryLoopManager = "LoopManager " /// for use in Bg Readings view static let categoryBgReadingsView = "BgReadingsView " @@ -184,6 +190,13 @@ enum ConstantsLog { /// SettingsViewCalendarEventsSettingsViewModel logging static let categorySettingsViewDataSourceSettingsViewModel = "SettingsViewDataSourceSettingsViewModel" + /// SettingsViewNotificationsSettingsViewModel logging + static let categorySettingsViewNotificationsSettingsViewModel = + "NotificationsViewModel " + + /// for use in LiveActivityManager + static let categoryLiveActivityManager = "LiveActivityManager " + /// for use in Libre3HeartBeatTransmitter static let categoryHeartBeatLibre3 = "HeartBeatLibre3 " diff --git a/xdrip/Constants/ConstantsNightScout.swift b/xdrip/Constants/ConstantsNightScout.swift index cd3b684b..065458e3 100644 --- a/xdrip/Constants/ConstantsNightScout.swift +++ b/xdrip/Constants/ConstantsNightScout.swift @@ -24,5 +24,7 @@ enum ConstantsNightScout { /// the text used by Nightscout for the "unit" json attribute for BG Checks stored in mmol/l static let mmolNightscoutUnitString = "mmol" - + + /// how many seconds should we force the app to wait between treatment sync attempts + static let minimiumTimeBetweenTwoTreatmentSyncsInSeconds: Double = 10 } diff --git a/xdrip/Extensions/Bundle.swift b/xdrip/Extensions/Bundle.swift index 2cf1da39..0888cb27 100644 --- a/xdrip/Extensions/Bundle.swift +++ b/xdrip/Extensions/Bundle.swift @@ -4,4 +4,8 @@ extension Bundle { var appGroupSuiteName: String { return object(forInfoDictionaryKey: "AppGroupIdentifier") as! String } + + var mainAppBundleIdentifier: String { + return object(forInfoDictionaryKey: "MainAppBundleIdentifier") as! String + } } diff --git a/xdrip/Extensions/Double.swift b/xdrip/Extensions/Double.swift index 21a7b0be..a01cc5a4 100644 --- a/xdrip/Extensions/Double.swift +++ b/xdrip/Extensions/Double.swift @@ -109,12 +109,10 @@ extension Double: RawRepresentable { // set a default value assuming that we're unable to calculate the hours + days var daysAndHoursString: String = "n/a" - - let days = floor(self / (24 * 60)) - let hoursInMinutes = self.truncatingRemainder(dividingBy: 24 * 60) - let hours = hoursInMinutes / 60 - let minutes = self.truncatingRemainder(dividingBy: 24 * 60 * 60) - + + let days = Int(floor(self / (24 * 60))) + let hours = Int(self.truncatingRemainder(dividingBy: 24 * 60) / 60) + let minutes = Int(self.truncatingRemainder(dividingBy: 24 * 60 * 60)) - (hours * 60) if days == 0 && hours < 1 { diff --git a/xdrip/Extensions/String.swift b/xdrip/Extensions/String.swift index e170860a..164ab8d4 100644 --- a/xdrip/Extensions/String.swift +++ b/xdrip/Extensions/String.swift @@ -12,24 +12,18 @@ extension String { let idx2 = index(startIndex, offsetBy: min(self.count, range.upperBound)) return String(self[idx1.. Bool { let range = NSRange(self.startIndex..., in: self) let matchRange = regex.rangeOfFirstMatch(in: self, options: .reportProgress, range: range) return matchRange.location != NSNotFound } -} - -extension String { + func startsWith(_ prefix: String) -> Bool { return lowercased().hasPrefix(prefix.lowercased()) } -} - -extension String { + /// converts String to Double, works with decimal seperator . or , - if conversion fails then returns nil func toDouble() -> Double? { @@ -55,25 +49,19 @@ extension String { } return nil } -} - -extension String { + func contains(find: String) -> Bool{ return self.range(of: find) != nil } func containsIgnoringCase(find: String) -> Bool{ return self.range(of: find, options: .caseInsensitive) != nil } -} - -extension String { + func sha1() -> String { // sha1() here is a function in CryptoSwift Library return Data(self.utf8).sha1().hexEncodedString() } -} - -extension String { + /// creates uicolor interpreting hex as hex color code, example #CED430 func hexStringToUIColor () -> UIColor { var cString:String = self.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() @@ -96,10 +84,6 @@ extension String { alpha: CGFloat(1.0) ) } - -} - -extension String { /// checks if string length is > 0 and if so returns self, otherwise returns nil /// @@ -108,9 +92,6 @@ extension String { if self.count > 0 {return self} return nil } -} - -extension String { /// Percent escape value to be added to a URL query value as specified in RFC 3986 /// @@ -163,7 +144,7 @@ extension String { } isOn = !isOn } - + return isOn } @@ -197,10 +178,7 @@ extension String { return data } - -} - -extension String { + func dateFromISOString() -> Date? { let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") @@ -209,45 +187,12 @@ extension String { return dateFormatter.date(from: self) } -} - -extension Optional where Wrapped == String { - - /// - if string doesn't start with http, then add https - /// - if ending with /, remove it - /// - convert to lowercase - /// - if string nil, then returnvalue is nil - func addHttpsIfNeeded() -> String? { - - // if self nil, then return nil - guard var returnValue = self else {return nil} - - // if self doesn't start with http or https, then add https. This might not make sense, but it will guard against throwing fatal errors when trying to get the scheme of the Endpoint - if !returnValue.startsWith("http://") && !returnValue.startsWith("https://") { - returnValue = "https://" + returnValue - } - - // if url ends with /, remove it - if returnValue.last == "/" { - returnValue.removeLast() - } - - return returnValue - - } -} - -extension String { mutating func appendStringAndNewLine(_ stringToAdd: String) { self = self + stringToAdd + "\n" } - -} - -extension String { /// use this to partially obscure a password, API-SECRET, token or other sensitive data. We want the user to see that something recognisable is there that makes sense to them, but it won't reveal any useful private information if they screenshot it func obscured() -> String { @@ -290,5 +235,30 @@ extension String { } - +} + +extension Optional where Wrapped == String { + + /// - if string doesn't start with http, then add https + /// - if ending with /, remove it + /// - convert to lowercase + /// - if string nil, then returnvalue is nil + func addHttpsIfNeeded() -> String? { + + // if self nil, then return nil + guard var returnValue = self else {return nil} + + // if self doesn't start with http or https, then add https. This might not make sense, but it will guard against throwing fatal errors when trying to get the scheme of the Endpoint + if !returnValue.startsWith("http://") && !returnValue.startsWith("https://") { + returnValue = "https://" + returnValue + } + + // if url ends with /, remove it + if returnValue.last == "/" { + returnValue.removeLast() + } + + return returnValue + + } } diff --git a/xdrip/Extensions/UserDefaults.swift b/xdrip/Extensions/UserDefaults.swift index a61c24db..127b3498 100644 --- a/xdrip/Extensions/UserDefaults.swift +++ b/xdrip/Extensions/UserDefaults.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import ActivityKit extension UserDefaults { @@ -70,6 +71,10 @@ extension UserDefaults { case multipleAppBadgeValueWith10 = "multipleAppBadgeValueWith10" /// minimum time between two notifications, set by user case notificationInterval = "notificationInterval" + /// which type of live activities should be shown? + case liveActivityType = "liveActivityType" + /// which size should the live activities be shown? + case liveActivitySize = "liveActivitySize" // Home Screen and main chart settings @@ -305,6 +310,8 @@ extension UserDefaults { // Nightscout /// timestamp lastest reading uploaded to NightScout case timeStampLatestNSUploadedBgReadingToNightScout = "timeStampLatestUploadedBgReading" + /// timestamp lastest treatment sync request to NightScout + case timeStampLatestNightScoutTreatmentSyncRequest = "timeStampLatestNightScoutTreatmentSyncRequest" /// timestamp latest calibration uploaded to NightScout case timeStampLatestNSUploadedCalibrationToNightScout = "timeStampLatestUploadedCalibration" @@ -404,6 +411,12 @@ extension UserDefaults { /// - stored as data as read from transmitter case librePatchInfo = "librePatchInfo" + // heartbeat + /// the last heartbeat connection timestamp + case timeStampOfLastHeartBeat = "timeStampOfLastHeartBeat" + /// how many seconds since the last heartbeat before we raise a disconnection warning + case secondsUntilHeartBeatDisconnectWarning = "secondsUntilHeartBeatDisconnectWarning" + } @@ -554,27 +567,6 @@ extension UserDefaults { } } - /// LibreLinkUp version - @objc dynamic var libreLinkUpVersion: String? { - get { - var returnValue = string(forKey: Key.libreLinkUpVersion.rawValue) - - // if nil set to defaultvalue - if returnValue == nil { - - set(ConstantsLibreLinkUp.libreLinkUpVersionDefault, forKey: Key.libreLinkUpVersion.rawValue) - - returnValue = string(forKey: Key.libreLinkUpVersion.rawValue) - - } - - return returnValue - } - set { - set(newValue, forKey: Key.libreLinkUpVersion.rawValue) - } - } - /// keep track of if the terms of use must be re-accepted true or false, default false @objc dynamic var libreLinkUpReAcceptNeeded: Bool { get { @@ -634,30 +626,6 @@ extension UserDefaults { set(newValue, forKey: Key.notificationInterval.rawValue) } } - - /// to create artificial delay in readings stored in sharedUserDefaults for loop. Minutes - so that Loop receives more smoothed values. - /// - /// Default value 0, if used then recommended value is multiple of 5 (eg 5 ot 10) - @objc dynamic var loopDelaySchedule: String? { - get { - return string(forKey: Key.loopDelaySchedule.rawValue) - } - set { - set(newValue, forKey: Key.loopDelaySchedule.rawValue) - } - } - - /// to create artificial delay in readings stored in sharedUserDefaults for loop. Minutes - so that Loop receives more smoothed values. - /// - /// Default value 0, if used then recommended value is multiple of 5 (eg 5 ot 10) - @objc dynamic var loopDelayValueInMinutes: String? { - get { - return string(forKey: Key.loopDelayValueInMinutes.rawValue) - } - set { - set(newValue, forKey: Key.loopDelayValueInMinutes.rawValue) - } - } /// should reading be shown in app badge yes or no @objc dynamic var showReadingInAppBadge: Bool { @@ -681,6 +649,30 @@ extension UserDefaults { } } + /// holds the enum integer of the type of live activity to be shown + /// default to 0 (disabled) + var liveActivityType: LiveActivityType { + get { + let liveActivityTypeAsInt = integer(forKey: Key.liveActivityType.rawValue) + return LiveActivityType(rawValue: liveActivityTypeAsInt) ?? .disabled + } + set { + set(newValue.rawValue, forKey: Key.liveActivityType.rawValue) + } + } + + /// holds the enum integer of the type of live activity to be shown + /// default to 0 (normal) + var liveActivitySize: LiveActivitySize { + get { + let liveActivitySizeAsInt = integer(forKey: Key.liveActivitySize.rawValue) + return LiveActivitySize(rawValue: liveActivitySizeAsInt) ?? .normal + } + set { + set(newValue.rawValue, forKey: Key.liveActivitySize.rawValue) + } + } + // MARK: Home Screen Settings /// the amount of hours to show in the mini-chart. Usually 24 hours but can be set to 48 hours by the user @@ -1404,6 +1396,16 @@ extension UserDefaults { } } + /// timestamp lastest reading uploaded to NightScout + var timeStampLatestNightScoutTreatmentSyncRequest: Date? { + get { + return object(forKey: Key.timeStampLatestNightScoutTreatmentSyncRequest.rawValue) as? Date + } + set { + set(newValue, forKey: Key.timeStampLatestNightScoutTreatmentSyncRequest.rawValue) + } + } + /// used to trigger view controllers that there's a change in TreatmentEntries /// /// value will be increased with 1 each time there's an update @@ -1947,8 +1949,7 @@ extension UserDefaults { } } - - // MARK: - ===== Loop Share Settings ====== + // MARK: - ===== Loopkit App Group Share variables ====== /// dictionary representation of readings that were shared with Loop. This is not the json representation, it's an array of dictionary var readingsStoredInSharedUserDefaultsAsDictionary: [Dictionary]? { @@ -1970,35 +1971,16 @@ extension UserDefaults { } } - /// Loop sharing will be limited to just once every 5 minutes if true - default false - var shareToLoopOnceEvery5Minutes: Bool { + + // MARK: - ===== Developer Settings ====== + + /// OSLogEnabled - default false + var OSLogEnabled: Bool { get { - return bool(forKey: Key.shareToLoopOnceEvery5Minutes.rawValue) + return bool(forKey: Key.OSLogEnabled.rawValue) } set { - set(newValue, forKey: Key.shareToLoopOnceEvery5Minutes.rawValue) - } - } - - // MARK: - ===== technical settings for testing ====== - - /// G6 factor 1 - @objc dynamic var G6v2ScalingFactor1:String? { - get { - return string(forKey: Key.G6v2ScalingFactor1.rawValue) - } - set { - set(newValue, forKey: Key.G6v2ScalingFactor1.rawValue) - } - } - - /// G6 factor 2 - @objc dynamic var G6v2ScalingFactor2:String? { - get { - return string(forKey: Key.G6v2ScalingFactor2.rawValue) - } - set { - set(newValue, forKey: Key.G6v2ScalingFactor2.rawValue) + set(newValue, forKey: Key.OSLogEnabled.rawValue) } } @@ -2022,13 +2004,15 @@ extension UserDefaults { } } - /// for Libre 2 : suppress sending unlockPayLoad, this will allow to run xDrip4iOS/Libre 2 in parallel with other app(s) - var suppressUnLockPayLoad: Bool { + /// to create artificial delay in readings stored in sharedUserDefaults for loop. Minutes - so that Loop receives more smoothed values. + /// + /// Default value 0, if used then recommended value is multiple of 5 (eg 5 ot 10) + @objc dynamic var loopDelaySchedule: String? { get { - return bool(forKey: Key.suppressUnLockPayLoad.rawValue) + return string(forKey: Key.loopDelaySchedule.rawValue) } set { - set(newValue, forKey: Key.suppressUnLockPayLoad.rawValue) + set(newValue, forKey: Key.loopDelaySchedule.rawValue) } } @@ -2041,6 +2025,82 @@ extension UserDefaults { set(newValue, forKey: Key.suppressLoopShare.rawValue) } } + + /// Loop sharing will be limited to just once every 5 minutes if true - default false + var shareToLoopOnceEvery5Minutes: Bool { + get { + return bool(forKey: Key.shareToLoopOnceEvery5Minutes.rawValue) + } + set { + set(newValue, forKey: Key.shareToLoopOnceEvery5Minutes.rawValue) + } + } + + /// for Libre 2 : suppress sending unlockPayLoad, this will allow to run xDrip4iOS/Libre 2 in parallel with other app(s) + var suppressUnLockPayLoad: Bool { + get { + return bool(forKey: Key.suppressUnLockPayLoad.rawValue) + } + set { + set(newValue, forKey: Key.suppressUnLockPayLoad.rawValue) + } + } + + /// to create artificial delay in readings stored in sharedUserDefaults for loop. Minutes - so that Loop receives more smoothed values. + /// + /// Default value 0, if used then recommended value is multiple of 5 (eg 5 ot 10) + @objc dynamic var loopDelayValueInMinutes: String? { + get { + return string(forKey: Key.loopDelayValueInMinutes.rawValue) + } + set { + set(newValue, forKey: Key.loopDelayValueInMinutes.rawValue) + } + } + + /// LibreLinkUp version + @objc dynamic var libreLinkUpVersion: String? { + get { + var returnValue = string(forKey: Key.libreLinkUpVersion.rawValue) + + // if nil set to defaultvalue + if returnValue == nil { + + set(ConstantsLibreLinkUp.libreLinkUpVersionDefault, forKey: Key.libreLinkUpVersion.rawValue) + + returnValue = string(forKey: Key.libreLinkUpVersion.rawValue) + + } + + return returnValue + } + set { + set(newValue, forKey: Key.libreLinkUpVersion.rawValue) + } + } + + + // MARK: - ===== technical settings for testing ====== + + /// G6 factor 1 + @objc dynamic var G6v2ScalingFactor1:String? { + get { + return string(forKey: Key.G6v2ScalingFactor1.rawValue) + } + set { + set(newValue, forKey: Key.G6v2ScalingFactor1.rawValue) + } + } + + /// G6 factor 2 + @objc dynamic var G6v2ScalingFactor2:String? { + get { + return string(forKey: Key.G6v2ScalingFactor2.rawValue) + } + set { + set(newValue, forKey: Key.G6v2ScalingFactor2.rawValue) + } + } /// used for Libre data parsing - for processing in LibreDataParser which is only in case of reading with NFC (ie bubble etc) var previousRawLibreValues: [Double] { @@ -2102,16 +2162,6 @@ extension UserDefaults { } } - /// OSLogEnabled - default false - var OSLogEnabled: Bool { - get { - return bool(forKey: Key.OSLogEnabled.rawValue) - } - set { - set(newValue, forKey: Key.OSLogEnabled.rawValue) - } - } - /// addDebugLevelLogsInTraceFileAndNSLog - default false var addDebugLevelLogsInTraceFileAndNSLog: Bool { get { @@ -2249,6 +2299,29 @@ extension UserDefaults { } } + + // MARK: - Heartbeat + + /// timestamp of last successful connection to follower service + @objc dynamic var timeStampOfLastHeartBeat: Date? { + get { + return object(forKey: Key.timeStampOfLastHeartBeat.rawValue) as? Date + } + set { + set(newValue, forKey: Key.timeStampOfLastHeartBeat.rawValue) + } + } + + /// how many seconds should be considered as the maximum since the last heartbeat before we show a warning/error? + var secondsUntilHeartBeatDisconnectWarning: Double? { + get { + return double(forKey: Key.secondsUntilHeartBeatDisconnectWarning.rawValue) + } + set { + set(newValue, forKey: Key.secondsUntilHeartBeatDisconnectWarning.rawValue) + } + } + } diff --git a/xdrip/Extensions/View.swift b/xdrip/Extensions/View.swift new file mode 100644 index 00000000..52fe14e8 --- /dev/null +++ b/xdrip/Extensions/View.swift @@ -0,0 +1,23 @@ +// +// View.swift +// xdrip +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import SwiftUI +import Foundation +import WidgetKit + +extension View { + func widgetBackground(backgroundView: some View) -> some View { + if #available(watchOS 10.0, iOSApplicationExtension 17.0, iOS 17.0, *) { + return containerBackground(for: .widget) { + backgroundView + } + } else { + return background(backgroundView) + } + } +} diff --git a/xdrip/Managers/Alerts/AlertManager.swift b/xdrip/Managers/Alerts/AlertManager.swift index 0e8a7cc5..76e16643 100644 --- a/xdrip/Managers/Alerts/AlertManager.swift +++ b/xdrip/Managers/Alerts/AlertManager.swift @@ -340,11 +340,11 @@ public class AlertManager:NSObject { if alertKind == .missedreading { if let content = content { - + // schedule missed reading alert with same content self.scheduleMissedReadingAlert(snoozePeriodInMinutes: snoozePeriod, content: content) - - } else { + + } 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) @@ -555,7 +555,7 @@ public class AlertManager:NSObject { } - if alertNeeded { + if alertNeeded && (UserDefaults.standard.isMaster || (!UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType != .disabled)) { // alert needs to be raised @@ -694,7 +694,7 @@ public class AlertManager:NSObject { if delayInSecondsToUse == 0 { trace("in checkAlert, raising alert %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging()) } else { - trace("in checkAlert, raising alert %{public}@ with a delay of %{public}@ minutes - this is a scheduled future alert, it will not go off now", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, ((round(Double(delayInSecondsToUse)/60*10))/10).description) + trace("in checkAlert, raising alert %{public}@ with a delay of %{public}@ minutes - this is a scheduled future alert, it will not go off now", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging(), ((Int(round(Double(delayInSecondsToUse)/60*10))/10)).description) } // check if app is allowed to send local notification and if not write info to trace @@ -719,9 +719,11 @@ public class AlertManager:NSObject { return true } else { - - trace("in checkAlert, there's no need to raise alert %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging()) - + if !UserDefaults.standard.isMaster && !UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { + trace("in checkAlert, there's no need to raise alert %{public}@ because we're in follower mode and keep-alive is: %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging(), UserDefaults.standard.followerBackgroundKeepAliveType.description) + } else { + trace("in checkAlert, there's no need to raise alert %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging()) + } return false } } diff --git a/xdrip/Managers/Calendar/CalendarManager.swift b/xdrip/Managers/Calendar/CalendarManager.swift new file mode 100644 index 00000000..7ec37ee7 --- /dev/null +++ b/xdrip/Managers/Calendar/CalendarManager.swift @@ -0,0 +1,201 @@ +import Foundation +import os +import EventKit + +class CalendarManager: NSObject { + + // MARK: - private properties + + /// CoreDataManager to use + private let coreDataManager:CoreDataManager + + /// BgReadingsAccessor instance + private let bgReadingsAccessor:BgReadingsAccessor + + /// for logging + private var log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryCalendarManager) + + /// to create and delete events + private let eventStore = EKEventStore() + + /// timestamp of last reading for which calendar event is created, initially set to 1 jan 1970 + private var timeStampLastProcessedReading = Date(timeIntervalSince1970: 0.0) + + // MARK: - initializer + + init(coreDataManager: CoreDataManager) { + + self.coreDataManager = coreDataManager + self.bgReadingsAccessor = BgReadingsAccessor(coreDataManager: coreDataManager) + + } + + // MARK: - public functions + + /// process new readings + /// - lastConnectionStatusChangeTimeStamp : when was the last transmitter dis/reconnect - if nil then 1 1 1970 is used + public func processNewReading(lastConnectionStatusChangeTimeStamp: Date?) { + + // check if createCalenderEvent is enabled in the settings and if so create calender event + if UserDefaults.standard.createCalendarEvent { + createCalendarEvent(lastConnectionStatusChangeTimeStamp: lastConnectionStatusChangeTimeStamp) + } + + } + + // MARK: - private functions + + private func createCalendarEvent(lastConnectionStatusChangeTimeStamp: Date?) { + + // check that access to calendar is authorized by the user + guard EKEventStore.authorizationStatus(for: .event) == .authorized else { + trace("in createCalendarEvent, createCalendarEvent is enabled but access to calendar is not authorized, setting UserDefaults.standard.createCalendarEvent to false", log: log, category: ConstantsLog.categoryCalendarManager, type: .info) + return + } + + // check that there is a calendar (should be) + guard let calendar = getCalendar() else { + trace("in createCalendarEvent, there's no calendar", log: log, category: ConstantsLog.categoryCalendarManager, type: .info) + return + } + + // if an interval is defined, and if time since last created event is less than interval, then don't create a new event + // substract 10 seconds, because user will probably select a multiple of 5, and also readings usually arrive every 5 minutes + // example user selects 10 minutes interval, next reading will arrive in exactly 10 minutes, time interval to be checked will be 590 seconds + if Int(Date().timeIntervalSince(timeStampLastProcessedReading)) < (UserDefaults.standard.calendarInterval * 60 - 10) { + + trace("in createCalendarEvent, less than %{public}@ minutes since last event, will not create a new event", log: log, category: ConstantsLog.categoryCalendarManager, type: .info, UserDefaults.standard.calendarInterval.description) + + return + + } + + // get 2 last Readings, with a calculatedValue + let lastReading = bgReadingsAccessor.get2LatestBgReadings(minimumTimeIntervalInMinutes: 4.0) + + // there should be at least one reading + guard lastReading.count > 0 else { + trace("in createCalendarEvent, there are no new readings to process", log: log, category: ConstantsLog.categoryCalendarManager, type: .info) + return + } + + // latest reading should be less than 5 minutes old + guard abs(lastReading[0].timeStamp.timeIntervalSinceNow) < 5 * 60 else { + trace("in createCalendarEvent, the latest reading is older than 5 minutes", log: log, category: ConstantsLog.categoryCalendarManager, type: .info) + return } + + // time to delete any existing events + deleteAllEvents(in: calendar) + + // compose the event title + // start with the reading in correct unit + var title = lastReading[0].unitizedString(unitIsMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl).description + + // add the visual indicator to the title to show what range the current + // reading is in + if (UserDefaults.standard.displayVisualIndicatorInCalendarEvent){ + + var visualIndicator = "" + + // get the current range of the last reading then + // configure the indicator based on the relevant range + switch lastReading[0].bgRangeDescription() { + case .inRange: + visualIndicator = ConstantsCalendar.visualIndicatorInRange + case .notUrgent: + visualIndicator = ConstantsCalendar.visualIndicatorNotUrgent + case .urgent: + visualIndicator = ConstantsCalendar.visualIndicatorUrgent + } + + // pre-append the indicator to the title + title = visualIndicator + " " + title + } + + // add trend if needed and available + if (!lastReading[0].hideSlope && UserDefaults.standard.displayTrendInCalendarEvent) { + title = title + " " + lastReading[0].slopeArrow() + } + + // add delta if needed + if UserDefaults.standard.displayDeltaInCalendarEvent && lastReading.count > 1 { + + title = title + " " + lastReading[0].unitizedDeltaString(previousBgReading: lastReading[1], showUnit: UserDefaults.standard.displayUnitInCalendarEvent, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) + + } else if UserDefaults.standard.displayUnitInCalendarEvent { + + // add unit if needed + title = title + " " + (UserDefaults.standard.bloodGlucoseUnitIsMgDl ? Texts_Common.mgdl : Texts_Common.mmol) + + } + + // create an event now + let event = EKEvent(eventStore: eventStore) + event.title = title + event.notes = ConstantsCalendar.textInCreatedEvent + event.startDate = Date() + event.endDate = Date(timeIntervalSinceNow: 60 * 10) + event.calendar = calendar + + do{ + + try eventStore.save(event, span: .thisEvent) + + timeStampLastProcessedReading = lastReading[0].timeStamp + + } catch let error { + + trace("in createCalendarEvent, error while saving : %{public}@", log: log, category: ConstantsLog.categoryCalendarManager, type: .error, error.localizedDescription) + + } + + } + + /// - gets all calendars on the device, if one of them has a title that matches the name stored in UserDefaults.standard.calenderId, then it returns that calendar. + /// - else returns the default calendar and sets the value in the UserDefaults to that default value + /// - also if currently there's no value in the UserDefaults, then value will be assigned here to UserDefaults.standard.calenderId + /// - nil as return value should normally not happen, because there should always be at least one calendar on the device + private func getCalendar() -> EKCalendar? { + + // get calendar title stored in the settings and compare to list + if let calendarIdInUserDefaults = UserDefaults.standard.calenderId { + + // get all calendars, if there's one having the same title return that one + for calendar in eventStore.calendars(for: .event) { + + if calendar.title == calendarIdInUserDefaults { + return calendar + } + } + + } + + // so there's no value in UserDefaults.standard.calenderId or there isn't a calendar that has a title as stored in UserDefaults.standard.calenderId + // set now UserDefaults.standard.calenderId to default calendar and return that one + UserDefaults.standard.calenderId = eventStore.defaultCalendarForNewEvents?.title + + return eventStore.defaultCalendarForNewEvents + + } + + // deletes all xdrip events in the calendar, for the last 24 hours + private func deleteAllEvents(in calendar:EKCalendar) { + + let predicate = eventStore.predicateForEvents(withStart: Date(timeIntervalSinceNow: -24*3600), end: Date(), calendars: [calendar]) + + let events = eventStore.events(matching: predicate) + + for event in events { + if let notes = event.notes { + if notes.contains(find: ConstantsCalendar.textInCreatedEvent) { + do{ + try eventStore.remove(event, span: .thisEvent) + } catch let error { + trace("in deleteAllEvents, error while removing : %{public}@", log: log, category: ConstantsLog.categoryCalendarManager, type: .error, error.localizedDescription) + } + } + } + } + } + +} diff --git a/xdrip/Managers/Charts/GlucoseChartType.swift b/xdrip/Managers/Charts/GlucoseChartType.swift new file mode 100644 index 00000000..8095c020 --- /dev/null +++ b/xdrip/Managers/Charts/GlucoseChartType.swift @@ -0,0 +1,260 @@ +// +// GlucoseChartType.swift +// xdrip +// +// Created by Paul Plant on 5/1/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation +import UIKit +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 + case watchAccessoryRectangular = 3 + case widgetSystemSmall = 4 + case widgetSystemMedium = 5 + case widgetSystemLarge = 6 + case widgetAccessoryRectangular = 7 + + var description: String { + switch self { + case .liveActivity: + return "Live Activity Notification Chart" + case .dynamicIsland: + return "Dynamic Island (Expanded) Chart" + case .watchApp: + return "Apple Watch Chart" + case .watchAccessoryRectangular: + return "Watch Chart .accessoryRectangular" + case .widgetSystemSmall: + return "Widget Chart .systemSmall" + case .widgetSystemMedium: + return "Widget Chart .systemMedium" + case .widgetSystemLarge: + return "Widget Chart .systemLarge" + case .widgetAccessoryRectangular: + return "Widget Chart .accessoryRectangular" + } + } + + + func viewSize(liveActivitySize: LiveActivitySize) -> (width: CGFloat, height: CGFloat) { + switch self { + case .liveActivity: + switch liveActivitySize { + case .large: + return (ConstantsGlucoseChartSwiftUI.viewWidthLiveActivityLarge, ConstantsGlucoseChartSwiftUI.viewHeightLiveActivityLarge) + default: + return (ConstantsGlucoseChartSwiftUI.viewWidthLiveActivityNormal, ConstantsGlucoseChartSwiftUI.viewHeightLiveActivityNormal) + } + case .dynamicIsland: + return (ConstantsGlucoseChartSwiftUI.viewWidthDynamicIsland, ConstantsGlucoseChartSwiftUI.viewHeightDynamicIsland) + case .watchApp: + return (ConstantsGlucoseChartSwiftUI.viewWidthWatchApp, ConstantsGlucoseChartSwiftUI.viewHeightWatchApp) + case .watchAccessoryRectangular: + return (ConstantsGlucoseChartSwiftUI.viewWidthWatchAccessoryRectangular, ConstantsGlucoseChartSwiftUI.viewHeightWatchAccessoryRectangular) + case .widgetSystemSmall: + return (ConstantsGlucoseChartSwiftUI.viewWidthWidgetSystemSmall, ConstantsGlucoseChartSwiftUI.viewHeightWidgetSystemSmall) + case .widgetSystemMedium: + return (ConstantsGlucoseChartSwiftUI.viewWidthWidgetSystemMedium, ConstantsGlucoseChartSwiftUI.viewHeightWidgetSystemMedium) + case .widgetSystemLarge: + return (ConstantsGlucoseChartSwiftUI.viewWidthWidgetSystemMedium, ConstantsGlucoseChartSwiftUI.viewHeightWidgetSystemLarge) + case .widgetAccessoryRectangular: + return (ConstantsGlucoseChartSwiftUI.viewWidthWidgetAccessoryRectangular, ConstantsGlucoseChartSwiftUI.viewHeightWidgetAccessoryRectangular) + } + } + + func hoursToShow(liveActivitySize: LiveActivitySize) -> Double { + switch self { + case .liveActivity: + switch liveActivitySize { + case .large: + return ConstantsGlucoseChartSwiftUI.hoursToShowLiveActivityLarge + default: + return ConstantsGlucoseChartSwiftUI.hoursToShowLiveActivityNormal + } + case .dynamicIsland: + return ConstantsGlucoseChartSwiftUI.hoursToShowDynamicIsland + case .watchApp: + return ConstantsGlucoseChartSwiftUI.hoursToShowWatchApp + case .watchAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.hoursToShowWatchAccessoryRectangular + case .widgetSystemSmall: + return ConstantsGlucoseChartSwiftUI.hoursToShowWidgetSystemSmall + case .widgetSystemMedium: + return ConstantsGlucoseChartSwiftUI.hoursToShowWidgetSystemMedium + case .widgetSystemLarge: + return ConstantsGlucoseChartSwiftUI.hoursToShowWidgetSystemLarge + case .widgetAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.hoursToShowWidgetAccessoryRectangular + } + } + + func intervalBetweenAxisValues(liveActivitySize: LiveActivitySize) -> Int { + switch self { + case .liveActivity: + switch liveActivitySize { + case .large: + return ConstantsGlucoseChartSwiftUI.intervalBetweenXAxisValuesLiveActivityLarge + default: + return ConstantsGlucoseChartSwiftUI.intervalBetweenXAxisValuesLiveActivityNormal + } + case .dynamicIsland: + return ConstantsGlucoseChartSwiftUI.intervalBetweenXAxisValuesDynamicIsland + case .watchApp: + return ConstantsGlucoseChartSwiftUI.intervalBetweenXAxisValuesWatchApp + case .watchAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.intervalBetweenXAxisValuesWatchAccessoryRectangular + case .widgetSystemSmall: + return ConstantsGlucoseChartSwiftUI.intervalBetweenXAxisValuesWidgetSystemSmall + case .widgetSystemMedium: + return ConstantsGlucoseChartSwiftUI.intervalBetweenXAxisValuesWidgetSystemMedium + case .widgetSystemLarge: + return ConstantsGlucoseChartSwiftUI.intervalBetweenXAxisValuesWidgetSystemLarge + case .widgetAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.intervalBetweenXAxisValuesWidgetAccessoryRectangular + } + } + + func glucoseCircleDiameter(liveActivitySize: LiveActivitySize) -> Double { + switch self { + case .liveActivity: + switch liveActivitySize { + case .large: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterLiveActivityLarge + default: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterLiveActivityNormal + } + case .dynamicIsland: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterDynamicIsland + case .watchApp: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWatchApp + case .watchAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWatchAccessoryRectangular + case .widgetSystemSmall: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWidgetSystemSmall + case .widgetSystemMedium: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWidgetSystemMedium + case .widgetSystemLarge: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWidgetSystemLarge + case .widgetAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.glucoseCircleDiameterWidgetAccessoryRectangular + } + } + + var lowHighLineColor: Color { + switch self { + case .liveActivity: + return ConstantsGlucoseChartSwiftUI.lowHighLineColorLiveActivity + case .dynamicIsland: + return ConstantsGlucoseChartSwiftUI.lowHighLineColorDynamicIsland + case .watchApp: + return ConstantsGlucoseChartSwiftUI.lowHighLineColorWatchApp + case .watchAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.lowHighLineColorWatchAccessoryRectangular + case .widgetSystemSmall: + return ConstantsGlucoseChartSwiftUI.lowHighLineColorWidgetSystemSmall + case .widgetSystemMedium: + return ConstantsGlucoseChartSwiftUI.lowHighLineColorWidgetSystemMedium + case .widgetSystemLarge: + return ConstantsGlucoseChartSwiftUI.lowHighLineColorWidgetSystemLarge + case .widgetAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.lowHighLineColorWidgetAccessoryRectangular + } + } + + var urgentLowHighLineColor: Color { + switch self { + case .liveActivity: + return ConstantsGlucoseChartSwiftUI.urgentLowHighLineLiveActivity + case .dynamicIsland: + return ConstantsGlucoseChartSwiftUI.urgentLowHighLineColorDynamicIsland + case .watchApp: + return ConstantsGlucoseChartSwiftUI.urgentLowHighLineColorWatchApp + case .watchAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.urgentLowHighLineColorWatchAccessoryRectangular + case .widgetSystemSmall: + return ConstantsGlucoseChartSwiftUI.urgentLowHighLineColorWidgetSystemSmall + case .widgetSystemMedium: + return ConstantsGlucoseChartSwiftUI.urgentLowHighLineColorWidgetSystemMedium + case .widgetSystemLarge: + return ConstantsGlucoseChartSwiftUI.urgentLowHighLineColorWidgetSystemLarge + case .widgetAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.urgentLowHighLineColorWidgetAccessoryRectangular + } + } + + var relativeYAxisLineSize: Double { + switch self { + case .liveActivity: + return ConstantsGlucoseChartSwiftUI.relativeYAxisLineSizeLiveActivity + case .dynamicIsland: + return ConstantsGlucoseChartSwiftUI.relativeYAxisLineSizeDynamicIsland + case .watchApp: + return ConstantsGlucoseChartSwiftUI.relativeYAxisLineSizeWatchApp + case .watchAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.relativeYAxisLineSizeWatchAccessoryRectangular + case .widgetSystemSmall: + return ConstantsGlucoseChartSwiftUI.relativeYAxisLineSizeWidgetSystemSmall + case .widgetSystemMedium: + return ConstantsGlucoseChartSwiftUI.relativeYAxisLineSizeWidgetSystemMedium + case .widgetSystemLarge: + return ConstantsGlucoseChartSwiftUI.relativeYAxisLineSizeWidgetSystemLarge + case .widgetAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.relativeYAxisLineSizeWidgetAccessoryRectangular + } + } + + var xAxisLabelOffset: Double { + switch self { + case .liveActivity: + return ConstantsGlucoseChartSwiftUI.xAxisLabelOffsetLiveActivity + case .dynamicIsland: + return ConstantsGlucoseChartSwiftUI.xAxisLabelOffsetDynamicIsland + case .watchApp: + return ConstantsGlucoseChartSwiftUI.xAxisLabelOffsetWatchApp + case .watchAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.xAxisLabelOffsetWatchAccessoryRectangular + case .widgetSystemSmall: + return ConstantsGlucoseChartSwiftUI.xAxisLabelOffsetWidgetSystemSmall + case .widgetSystemMedium: + return ConstantsGlucoseChartSwiftUI.xAxisLabelOffsetWidgetSystemMedium + case .widgetSystemLarge: + return ConstantsGlucoseChartSwiftUI.xAxisLabelOffsetWidgetSystemLarge + case .widgetAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.xAxisLabelOffsetWidgetAccessoryRectangular + } + } + + var xAxisGridLineColor: Color { + switch self { + case .liveActivity: + return ConstantsGlucoseChartSwiftUI.xAxisGridLineColorLiveActivity + case .dynamicIsland: + return ConstantsGlucoseChartSwiftUI.xAxisGridLineColorDynamicIsland + case .watchApp: + return ConstantsGlucoseChartSwiftUI.xAxisGridLineColorWatchApp + case .watchAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.xAxisGridLineColorWatchAccessoryRectangular + case .widgetSystemSmall: + return ConstantsGlucoseChartSwiftUI.xAxisGridLineColorWidgetSystemSmall + case .widgetSystemMedium: + return ConstantsGlucoseChartSwiftUI.xAxisGridLineColorWidgetSystemMedium + case .widgetSystemLarge: + return ConstantsGlucoseChartSwiftUI.xAxisGridLineColorWidgetSystemLarge + case .widgetAccessoryRectangular: + return ConstantsGlucoseChartSwiftUI.xAxisGridLineColorWidgetAccessoryRectangular + } + } + +} diff --git a/xdrip/Managers/Followers/FollowerBackgroundKeepAliveType.swift b/xdrip/Managers/Followers/FollowerBackgroundKeepAliveType.swift index 0cd6ff70..40e70706 100644 --- a/xdrip/Managers/Followers/FollowerBackgroundKeepAliveType.swift +++ b/xdrip/Managers/Followers/FollowerBackgroundKeepAliveType.swift @@ -7,7 +7,8 @@ // import Foundation - +import UIKit +import SwiftUI /// types of background keep-alive public enum FollowerBackgroundKeepAliveType: Int, CaseIterable { @@ -19,6 +20,7 @@ public enum FollowerBackgroundKeepAliveType: Int, CaseIterable { case disabled = 0 case normal = 1 case aggressive = 2 + case heartbeat = 3 var description: String { switch self { @@ -28,18 +30,57 @@ public enum FollowerBackgroundKeepAliveType: Int, CaseIterable { return Texts_SettingsView.followerKeepAliveTypeNormal case .aggressive: return Texts_SettingsView.followerKeepAliveTypeAggressive + case .heartbeat: + return Texts_SettingsView.followerKeepAliveTypeHeartbeat } } - var keepAliveImage: UIImage { + public var rawValue: Int { switch self { case .disabled: - return UIImage(systemName: "d.circle") ?? UIImage() + return 0 case .normal: - return UIImage(systemName: "n.circle") ?? UIImage() + return 1 case .aggressive: - return UIImage(systemName: "a.circle") ?? UIImage() + return 2 + case .heartbeat: + return 3 } } + // return true if in follower mode and if the keep-alive type should provoke a background keep-alive action + // basically if not .disabled and if not .heartbeat + var shouldKeepAlive: Bool { + switch self { + case .disabled, .heartbeat: + return false + default: + return true + } + } + + // return the keep-alive image for SwiftUI views + var keepAliveImageString: String { + switch self { + case .disabled: + return "d.circle" + case .normal: + return "n.circle" + case .aggressive: + return "a.circle" + case .heartbeat: + return "heart.circle" + } + } + + // return the keep-alive image for UIKit views + var keepAliveUIImage: UIImage { + return UIImage(systemName: keepAliveImageString) ?? UIImage() + } + + // return the keep-alive image for SwiftUI views + var keepAliveImage: Image { + return Image(systemName: keepAliveImageString) + } + } diff --git a/xdrip/Managers/Followers/FollowerDataSourceType.swift b/xdrip/Managers/Followers/FollowerDataSourceType.swift index 32579a02..7b9085a2 100644 --- a/xdrip/Managers/Followers/FollowerDataSourceType.swift +++ b/xdrip/Managers/Followers/FollowerDataSourceType.swift @@ -18,6 +18,14 @@ public enum FollowerDataSourceType: Int, CaseIterable { case nightscout = 0 case libreLinkUp = 1 + public var rawValue: Int { + switch self { + case .nightscout: + return 0 + case .libreLinkUp: + return 1 + } + } var description: String { switch self { case .nightscout: diff --git a/xdrip/Managers/LibreLinkUp/LibreLinkUpFollowManager.swift b/xdrip/Managers/LibreLinkUp/LibreLinkUpFollowManager.swift index 388b26d5..997e4734 100644 --- a/xdrip/Managers/LibreLinkUp/LibreLinkUpFollowManager.swift +++ b/xdrip/Managers/LibreLinkUp/LibreLinkUpFollowManager.swift @@ -24,9 +24,6 @@ class LibreLinkUpFollowManager: NSObject { /// for logging private var log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryLibreLinkUpFollowManager) - /// when to do next download - private var nextFollowDownloadTimeStamp: Date - /// reference to coredatamanager private var coreDataManager: CoreDataManager @@ -93,9 +90,6 @@ class LibreLinkUpFollowManager: NSObject { /// initializer public init(coreDataManager:CoreDataManager, followerDelegate: FollowerDelegate) { - // initialize nextFollowDownloadTimeStamp to now, which is at the moment FollowManager is instantiated - nextFollowDownloadTimeStamp = Date() - // initialize non optional private properties self.coreDataManager = coreDataManager self.bgReadingsAccessor = BgReadingsAccessor(coreDataManager: coreDataManager) @@ -207,10 +201,10 @@ class LibreLinkUpFollowManager: NSObject { trace("in download", log: self.log, category: ConstantsLog.categoryLibreLinkUpFollowManager, type: .info) - trace(" setting nightScoutSyncTreatmentsRequired to true, this will also initiate a treatments sync", log: self.log, category: ConstantsLog.categoryLibreLinkUpFollowManager, type: .info) - - // TODO: crash here sometimes - DispatchQueue.main.async { + if (UserDefaults.standard.timeStampLatestNightScoutTreatmentSyncRequest ?? Date.distantPast).timeIntervalSinceNow > 15 { + trace(" setting nightScoutSyncTreatmentsRequired to true, this will also initiate a treatments sync", log: self.log, category: ConstantsLog.categoryLibreLinkUpFollowManager, type: .info) + + UserDefaults.standard.timeStampLatestNightScoutTreatmentSyncRequest = .now UserDefaults.standard.nightScoutSyncTreatmentsRequired = true } @@ -250,9 +244,6 @@ class LibreLinkUpFollowManager: NSObject { // this takes care of 1 and 2 try await checkLoginAndConnections() - // store the current timestamp as a successful server connection with valid login - UserDefaults.standard.timeStampOfLastFollowerConnection = Date() - // this takes care of 3 if (self.libreLinkUpToken != nil && self.libreLinkUpPatientId != nil) { @@ -330,12 +321,8 @@ class LibreLinkUpFollowManager: NSObject { // rescheduling the timer must be done in main thread // we do it here at the end of the function so that it is always rescheduled once a valid connection is established, irrespective of whether we get values. DispatchQueue.main.sync { - - // schedule new download, only if followerBackgroundKeepAliveType != disabled - if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled { - self.scheduleNewDownload() - } - + // schedule new download + self.scheduleNewDownload() } } } @@ -520,6 +507,7 @@ class LibreLinkUpFollowManager: NSObject { } + print(loginUrl.description) trace(" in requestLogin, processing login request with URL: %{public}@", log: self.log, category: ConstantsLog.categoryLibreLinkUpFollowManager, type: .info, loginUrl) guard let url = URL(string: loginUrl) else { @@ -654,6 +642,9 @@ class LibreLinkUpFollowManager: NSObject { if statusCode == 200 { + // store the current timestamp as a successful server connection with valid login + UserDefaults.standard.timeStampOfLastFollowerConnection = Date() + return try decode(Response.self, data: data) } @@ -692,6 +683,8 @@ class LibreLinkUpFollowManager: NSObject { /// schedule new download with timer, when timer expires download() will be called private func scheduleNewDownload() { + guard UserDefaults.standard.followerBackgroundKeepAliveType != .heartbeat else { return } + trace("in scheduleNewDownload", log: self.log, category: ConstantsLog.categoryLibreLinkUpFollowManager, type: .info) // schedule a timer for 60 seconds and assign it to a let property @@ -761,10 +754,10 @@ class LibreLinkUpFollowManager: NSObject { /// launches timer that will regular play sound - this will be played only when app goes to background and only if the user wants to keep the app alive private func enableSuspensionPrevention() { - // if keep-alive is disabled, then just return and do nothing - if UserDefaults.standard.followerBackgroundKeepAliveType == .disabled { + // if keep-alive is disabled or if using a heartbeat, then just return and do nothing + if !UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { - print("not enabling suspension prevention as keep-alive is disabled") + print("not enabling suspension prevention as keep-alive type is: \(UserDefaults.standard.followerBackgroundKeepAliveType.description)") return @@ -786,11 +779,13 @@ class LibreLinkUpFollowManager: NSObject { // schedulePlaySoundTimer needs to be created when app goes to background ApplicationManager.shared.addClosureToRunWhenAppDidEnterBackground(key: applicationManagerKeyResumePlaySoundTimer, closure: { - if let playSoundTimer = self.playSoundTimer { - playSoundTimer.resume() - } - if let audioPlayer = self.audioPlayer, !audioPlayer.isPlaying { - audioPlayer.play() + if UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { + if let playSoundTimer = self.playSoundTimer { + playSoundTimer.resume() + } + if let audioPlayer = self.audioPlayer, !audioPlayer.isPlaying { + audioPlayer.play() + } } }) @@ -806,17 +801,16 @@ class LibreLinkUpFollowManager: NSObject { private func verifyUserDefaultsAndStartOrStopFollowMode() { if !UserDefaults.standard.isMaster && UserDefaults.standard.followerDataSourceType == .libreLinkUp && UserDefaults.standard.libreLinkUpEmail != nil && UserDefaults.standard.libreLinkUpPassword != nil { - // this will enable the suspension prevention sound playing if background keep-alive is enabled - if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled { - + // this will enable the suspension prevention sound playing if background keep-alive is needed + // (i.e. not disabled and not using a heartbeat) + if UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { enableSuspensionPrevention() - - // do initial download, this will also schedule future downloads - download() - } else { disableSuspensionPrevention() } + + // do initial download, this will also schedule future downloads + download() } else { diff --git a/xdrip/Managers/LiveActivity/LiveActivityManager.swift b/xdrip/Managers/LiveActivity/LiveActivityManager.swift new file mode 100644 index 00000000..1501c6f1 --- /dev/null +++ b/xdrip/Managers/LiveActivity/LiveActivityManager.swift @@ -0,0 +1,178 @@ +// +// LiveActivityManager.swift +// xdrip +// +// Created by Paul Plant on 31/12/23. +// Copyright © 2023 Johan Degraeve. All rights reserved. +// + +import Foundation +import ActivityKit +import OSLog +import SwiftUI + +@available(iOS 16.2, *) +/// manager class to handle the live activity events +public final class LiveActivityManager { + + // MARK: - Private variables + /// for trace + private let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryLiveActivityManager) + + private var eventAttributes: XDripWidgetAttributes + private var eventActivity: Activity? + + // the start date of the event so when know track when to proactively end/restart the activity + private var eventStartDate: Date + + static let shared = LiveActivityManager() + + private init() { + eventAttributes = XDripWidgetAttributes() + eventStartDate = Date() + } + +} + +// MARK: - Helper Extension +@available(iOS 16.2, *) +extension LiveActivityManager { + + /// start or update the live activity based upon whether it currently exists or not + /// - Parameter contentState: the contentState to show + /// - Parameter forceRestart: will force the function to end and restart the live activity + func runActivity(contentState: XDripWidgetAttributes.ContentState, forceRestart: Bool) { + + trace("in runActivity", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + + // checking whether 'Live activities' is enabled for the app in settings + if ActivityAuthorizationInfo().areActivitiesEnabled { + + // live activities are enabled. Now check if there is a currently + // running activity (in which case update it) or if not, start a new one + if eventActivity == nil { + trace(" starting new live activity", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + + startActivity(contentState: contentState) + } else if forceRestart && eventStartDate < Date().addingTimeInterval(-ConstantsLiveActivity.allowLiveActivityRestartAfterMinutes) { + // force an end/start cycle of the activity when the app comes to the foreground assuming at least 'x' hours have passed. This restarts the 8 hour limit. + trace(" restarting live activity", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + + Task { + await endActivity() + startActivity(contentState: contentState) + } + } else if eventStartDate < Date().addingTimeInterval(-ConstantsLiveActivity.endLiveActivityAfterMinutes) { + // if the activity has been running for almost 8 hours, proactively end the activity before it goes stale + trace(" ending live activity on purpose to avoid staying on the screen when stale", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + + Task { + await endActivity() + } + } else { + // none of the above conditions are true so let's just update the activity + trace(" updating live activity", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + + Task { + await updateActivity(to: contentState) + } + } + } else { + trace(" live activities are disabled in the iPhone Settings or permission has not been given.", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + } + } + + + /// end all live activities that are spawned from the app + func endAllActivities() { + + // https://developer.apple.com/forums/thread/732418 + // Add a semaphore to force it to wait for the activities to end before returning from the method + let semaphore = DispatchSemaphore(value: 0) + + Task + { + for activity in Activity.activities { + let idString = "\(String(describing: eventActivity?.id))" + trace("Ending live activity: %{public}@", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info, idString) + + await activity.end(nil, dismissalPolicy: .immediate) + } + semaphore.signal() + } + semaphore.wait() + + eventActivity = nil + } + + + /// will start a new live activity event based upon the content state passed to the function + /// - Parameter contentState: the content state of the new activity + private func startActivity(contentState: XDripWidgetAttributes.ContentState) { + + // as we're starting a new activity in the current event, let's set the eventStartDate so we can track how long it has been running + eventStartDate = Date() + var updatedContentState = contentState + updatedContentState.warnUserToOpenApp = false + updatedContentState.eventStartDate = Date() + + let content = ActivityContent(state: updatedContentState, staleDate: nil, relevanceScore: 1.0) + + do { + eventActivity = try Activity.request( + attributes: eventAttributes, + content: content, + pushType: nil + ) + let idString = "\(String(describing: eventActivity?.id))" + + trace("new live activity started: %{public}@", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info, idString) + } catch { + trace("error: %{public}@", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info, error.localizedDescription) + } + } + + /// update the current live activity + /// - Parameter contentState: the updated context state of the activity + private func updateActivity(to contentState: XDripWidgetAttributes.ContentState) async { + + // check if the activity is dismissed by the user (by swiping away the notification) + // if so, then end it completely and start a new one + if eventActivity?.activityState == .dismissed { + Task { + trace("Previous live activity was dismissed by the user so it will be ended and will try to start a new one.", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + + endAllActivities() + + startActivity(contentState: contentState) + } + } else { + // update the warnUserToOpenApp flag if needed and then update the activity + var updatedContentState = contentState + + // if the event was started more than 'x' time ago, then let's inform the user that the live activity will soon end so that they open the app + updatedContentState.warnUserToOpenApp = eventStartDate < Date().addingTimeInterval(-ConstantsLiveActivity.warnLiveActivityAfterMinutes) ? true : false + + let updatedContent = ActivityContent(state: updatedContentState, staleDate: nil) + + await eventActivity?.update(updatedContent) + } + } + + /// end the live activity if it is being shown, do nothing if there is no eventyActivity + private func endActivity() async { + + if eventActivity != nil { + Task { + for activity in Activity.activities + { + let idString = "\(String(describing: eventActivity?.id))" + trace("Ending live activity: %{public}@", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info, idString) + + await activity.end(nil, dismissalPolicy: .immediate) + } + } + eventActivity = nil + } + } +} diff --git a/xdrip/Managers/LiveActivity/LiveActivitySize.swift b/xdrip/Managers/LiveActivity/LiveActivitySize.swift new file mode 100644 index 00000000..345b5580 --- /dev/null +++ b/xdrip/Managers/LiveActivity/LiveActivitySize.swift @@ -0,0 +1,53 @@ +// +// LiveActivitySize.swift +// xdrip +// +// Created by Paul Plant on 14/1/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation + +/// what size should the live activity notification be? +public enum LiveActivitySize: Int, CaseIterable, Codable { + + // when adding liveActivitySize, 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) + + // override the allCases property to define our own order. + // this must then be handled with the forRowAt options + public static var allCasesForList: [LiveActivitySize] { + return [.minimal, .normal, .large] + } + + case normal = 0 // default upon initialization + case minimal = 1 + case large = 2 + + var description: String { + switch self { + case .normal: + return Texts_SettingsView.liveActivitySizeNormal + case .minimal: + return Texts_SettingsView.liveActivitySizeMinimal + case .large: + return Texts_SettingsView.liveActivitySizeLarge + } + } + + /// this is used for presentation in list. It allows to order the size kinds in the view, different than they case ordering, and so allows to add new cases + init?(forRowAt row: Int) { + switch row { + case 0: + self = .minimal + case 1: + self = .normal + case 2: + self = .large + default: + fatalError("in liveActivitySize initializer init(forRowAt row: Int), there's no case for the rownumber") + } + } + +} diff --git a/xdrip/Managers/LiveActivity/LiveActivityType.swift b/xdrip/Managers/LiveActivity/LiveActivityType.swift new file mode 100644 index 00000000..d1af9d04 --- /dev/null +++ b/xdrip/Managers/LiveActivity/LiveActivityType.swift @@ -0,0 +1,42 @@ +// +// LiveActivityType.swift +// xdrip +// +// Created by Paul Plant on 1/1/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation + +/// types of live activity, namely when we should show the live activities +public enum LiveActivityType: Int, CaseIterable { + + // when adding to LiveActivityType, 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 + + case disabled = 0 + case always = 1 + case urgentLow = 2 + case low = 3 + case lowHigh = 4 + case urgentLowHigh = 5 + + var description: String { + switch self { + case .disabled: + return Texts_SettingsView.liveActivityTypeDisabled + case .always: + return Texts_SettingsView.liveActivityTypeAlways + case .urgentLow: + return Texts_SettingsView.liveActivityTypeUrgentLow + case .low: + return Texts_SettingsView.liveActivityTypeLow + case .lowHigh: + return Texts_SettingsView.liveActivityTypeLowHigh + case .urgentLowHigh: + return Texts_SettingsView.liveActivityTypeUrgentLowHigh + } + } + +} + diff --git a/xdrip/Managers/Loop/LoopManager.swift b/xdrip/Managers/Loop/LoopManager.swift index fea3643b..6ee4a3cb 100644 --- a/xdrip/Managers/Loop/LoopManager.swift +++ b/xdrip/Managers/Loop/LoopManager.swift @@ -210,7 +210,9 @@ public class LoopManager:NSObject { sharedUserDefaults.set(data, forKey: "latestReadings") // store in local userdefaults - UserDefaults.standard.readingsStoredInSharedUserDefaultsAsDictionary = dictionary + if !dictionary.isEmpty { + UserDefaults.standard.readingsStoredInSharedUserDefaultsAsDictionary = dictionary + } // initially set timeStampLatestLoopSharedBgReading to timestamp of first reading - may get another value later, in case loopdelay > 0 // add 5 seconds to last Readings timestamp, because due to the way timestamp for libre readings is calculated, it may happen that the same reading shifts 1 or 2 seconds in next reading cycle diff --git a/xdrip/Managers/NightScout/NightScoutFollowManager.swift b/xdrip/Managers/NightScout/NightScoutFollowManager.swift index bc49cd60..c0699539 100644 --- a/xdrip/Managers/NightScout/NightScoutFollowManager.swift +++ b/xdrip/Managers/NightScout/NightScoutFollowManager.swift @@ -123,17 +123,18 @@ class NightScoutFollowManager: NSObject { @objc public func download() { trace("in download", log: self.log, category: ConstantsLog.categoryNightScoutFollowManager, type: .info) - - trace(" setting nightScoutSyncTreatmentsRequired to true, this will also initiate a treatments sync", log: self.log, category: ConstantsLog.categoryNightScoutFollowManager, type: .info) - DispatchQueue.main.async { - UserDefaults.standard.nightScoutSyncTreatmentsRequired = true - } - guard UserDefaults.standard.nightScoutEnabled else { trace(" nightscout not enabled", log: self.log, category: ConstantsLog.categoryNightScoutFollowManager, type: .info) return } + + if (UserDefaults.standard.timeStampLatestNightScoutTreatmentSyncRequest ?? Date.distantPast).timeIntervalSinceNow < -ConstantsNightScout.minimiumTimeBetweenTwoTreatmentSyncsInSeconds { + trace(" setting nightScoutSyncTreatmentsRequired to true, this will also initiate a treatments sync", log: self.log, category: ConstantsLog.categoryNightScoutFollowManager, type: .info) + + UserDefaults.standard.timeStampLatestNightScoutTreatmentSyncRequest = .now + UserDefaults.standard.nightScoutSyncTreatmentsRequired = true + } guard !UserDefaults.standard.isMaster else { trace(" not follower", log: self.log, category: ConstantsLog.categoryNightScoutFollowManager, type: .info) @@ -162,10 +163,8 @@ class NightScoutFollowManager: NSObject { trace(" last reading is less than 30 seconds old, will not download now", log: self.log, category: ConstantsLog.categoryNightScoutFollowManager, type: .info) - // schedule new download, only if followerBackgroundKeepAliveType != disabled - if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled { - self.scheduleNewDownload() - } + // schedule new download + self.scheduleNewDownload() return } @@ -203,11 +202,9 @@ class NightScoutFollowManager: NSObject { if let followerDelegate = self.followerDelegate { followerDelegate.followerInfoReceived(followGlucoseDataArray: &followGlucoseDataArray) } - - // schedule new download, only if followerBackgroundKeepAliveType != disabled - if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled { - self.scheduleNewDownload() - } + + // schedule new download + self.scheduleNewDownload() } @@ -250,6 +247,8 @@ class NightScoutFollowManager: NSObject { /// schedule new download with timer, when timer expires download() will be called private func scheduleNewDownload() { + guard UserDefaults.standard.followerBackgroundKeepAliveType != .heartbeat else { return } + trace("in scheduleNewDownload", log: self.log, category: ConstantsLog.categoryNightScoutFollowManager, type: .info) // schedule a timer for 15 seconds and assign it to a let property @@ -367,10 +366,10 @@ class NightScoutFollowManager: NSObject { /// launches timer that will regular play sound - this will be played only when app goes to background and only if the user wants to keep the app alive private func enableSuspensionPrevention() { - // if keep-alive is disabled, then just return and do nothing - if UserDefaults.standard.followerBackgroundKeepAliveType == .disabled { + // if keep-alive is not needed, then just return and do nothing + if !UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { - print("not enabling suspension prevention as keep-alive is disabled") + print("not enabling suspension prevention as keep-alive type is: \(UserDefaults.standard.followerBackgroundKeepAliveType.description)") return @@ -392,11 +391,13 @@ class NightScoutFollowManager: NSObject { // schedulePlaySoundTimer needs to be created when app goes to background ApplicationManager.shared.addClosureToRunWhenAppDidEnterBackground(key: applicationManagerKeyResumePlaySoundTimer, closure: { - if let playSoundTimer = self.playSoundTimer { - playSoundTimer.resume() - } - if let audioPlayer = self.audioPlayer, !audioPlayer.isPlaying { - audioPlayer.play() + if UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { + if let playSoundTimer = self.playSoundTimer { + playSoundTimer.resume() + } + if let audioPlayer = self.audioPlayer, !audioPlayer.isPlaying { + audioPlayer.play() + } } }) @@ -413,18 +414,17 @@ class NightScoutFollowManager: NSObject { if !UserDefaults.standard.isMaster && UserDefaults.standard.followerDataSourceType == .nightscout && UserDefaults.standard.nightScoutUrl != nil && UserDefaults.standard.nightScoutEnabled { - // this will enable the suspension prevention sound playing if background keep-alive is enabled - if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled { - + // this will enable the suspension prevention sound playing if background keep-alive is needed + // (i.e. not disabled and not using a heartbeat) + if UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { enableSuspensionPrevention() - - // do initial download, this will also schedule future downloads - download() - } else { disableSuspensionPrevention() } + // do initial download, this will also schedule future downloads + download() + } else { // disable the suspension prevention diff --git a/xdrip/Managers/NightScout/NightScoutUploadManager.swift b/xdrip/Managers/NightScout/NightScoutUploadManager.swift index f43d9c08..8ddd8e39 100644 --- a/xdrip/Managers/NightScout/NightScoutUploadManager.swift +++ b/xdrip/Managers/NightScout/NightScoutUploadManager.swift @@ -1,7 +1,6 @@ import Foundation import os import UIKit -import xDrip4iOS_Widget public class NightScoutUploadManager: NSObject, ObservableObject { @@ -105,10 +104,10 @@ public class NightScoutUploadManager: NSObject, ObservableObject { // and nightScoutUrl exists guard UserDefaults.standard.nightScoutEnabled, UserDefaults.standard.nightScoutUrl != nil else {return} - // TODO: crash here - trace(" setting nightScoutSyncTreatmentsRequired to true, this will also initiate a treatments sync", log: self.oslog, category: ConstantsLog.categoryNightScoutUploadManager, type: .info) - - DispatchQueue.main.async { + if (UserDefaults.standard.timeStampLatestNightScoutTreatmentSyncRequest ?? Date.distantPast).timeIntervalSinceNow < -ConstantsNightScout.minimiumTimeBetweenTwoTreatmentSyncsInSeconds { + trace(" setting nightScoutSyncTreatmentsRequired to true, this will also initiate a treatments sync", log: self.oslog, category: ConstantsLog.categoryNightScoutUploadManager, type: .info) + + UserDefaults.standard.timeStampLatestNightScoutTreatmentSyncRequest = .now UserDefaults.standard.nightScoutSyncTreatmentsRequired = true } @@ -171,7 +170,7 @@ public class NightScoutUploadManager: NSObject, ObservableObject { guard UserDefaults.standard.nightScoutEnabled, UserDefaults.standard.nightScoutUrl != nil else {return} // no sync needed if app is running in the background - guard UserDefaults.standard.appInForeGround else {return} + //guard UserDefaults.standard.appInForeGround else {return} // if sync already running, then set nightScoutTreatmentSyncRequired to true // sync is running already, once stopped it will rerun @@ -433,13 +432,8 @@ public class NightScoutUploadManager: NSObject, ObservableObject { guard UserDefaults.standard.nightScoutSyncTreatmentsRequired else {return} UserDefaults.standard.nightScoutSyncTreatmentsRequired = false - - // if Nightscout is enabled, then a nightscout sync is required - if UserDefaults.standard.nightScoutEnabled { - syncTreatmentsWithNightScout() - - } + syncTreatmentsWithNightScout() } @@ -1520,6 +1514,15 @@ public class NightScoutUploadManager: NSObject, ObservableObject { } + // set the flag to sync Nightscout treatments if a short time has passed since the last time + // as accessing userdefaults is not thread-safe + private func setNightscoutSyncTreatmentsRequiredToTrue() { + if (UserDefaults.standard.timeStampLatestNightScoutTreatmentSyncRequest ?? Date.distantPast).timeIntervalSinceNow < -ConstantsNightScout.minimiumTimeBetweenTwoTreatmentSyncsInSeconds { + UserDefaults.standard.timeStampLatestNightScoutTreatmentSyncRequest = .now + UserDefaults.standard.nightScoutSyncTreatmentsRequired = true + } + } + } // MARK: - enum's diff --git a/xdrip/Managers/Watch/WatchManager.swift b/xdrip/Managers/Watch/WatchManager.swift index 18bc1448..c8b97519 100644 --- a/xdrip/Managers/Watch/WatchManager.swift +++ b/xdrip/Managers/Watch/WatchManager.swift @@ -1,201 +1,167 @@ -import Foundation -import os -import EventKit +// +// WatchManager.swift +// xdrip +// +// Created by Paul Plant on 9/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// -class WatchManager: NSObject { +import Foundation +import WatchConnectivity +import WidgetKit + +public final class WatchManager: NSObject, ObservableObject { // MARK: - private properties - /// CoreDataManager to use - private let coreDataManager:CoreDataManager - - /// BgReadingsAccessor instance - private let bgReadingsAccessor:BgReadingsAccessor - - /// for logging - private var log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryWatchManager) + /// a watch connectivity session instance + private let session: WCSession - /// to create and delete events - private let eventStore = EKEventStore() + /// a BgReadingsAccessor instance + private var bgReadingsAccessor: BgReadingsAccessor - /// timestamp of last reading for which calendar event is created, initially set to 1 jan 1970 - private var timeStampLastProcessedReading = Date(timeIntervalSince1970: 0.0) + /// a coreDataManager instance (must be passed from RVC in the initializer) + private var coreDataManager: CoreDataManager - // MARK: - initializer + /// hold the current watch state model + private var watchState = WatchState() - init(coreDataManager: CoreDataManager) { + // MARK: - intializer + + init(coreDataManager: CoreDataManager, session: WCSession = .default) { + // set coreDataManager and bgReadingsAccessor self.coreDataManager = coreDataManager self.bgReadingsAccessor = BgReadingsAccessor(coreDataManager: coreDataManager) - } - - // MARK: - public functions - - /// process new readings - /// - lastConnectionStatusChangeTimeStamp : when was the last transmitter dis/reconnect - if nil then 1 1 1970 is used - public func processNewReading(lastConnectionStatusChangeTimeStamp: Date?) { + self.session = session + super.init() - // check if createCalenderEvent is enabled in the settings and if so create calender event - if UserDefaults.standard.createCalendarEvent { - createCalendarEvent(lastConnectionStatusChangeTimeStamp: lastConnectionStatusChangeTimeStamp) + if WCSession.isSupported() { + session.delegate = self + session.activate() } + processWatchState() + } - // MARK: - private functions - - private func createCalendarEvent(lastConnectionStatusChangeTimeStamp: Date?) { - - // check that access to calendar is authorized by the user - guard EKEventStore.authorizationStatus(for: .event) == .authorized else { - trace("in createCalendarEvent, createCalendarEvent is enabled but access to calendar is not authorized, setting UserDefaults.standard.createCalendarEvent to false", log: log, category: ConstantsLog.categoryWatchManager, type: .info) - return - } - - // check that there is a calendar (should be) - guard let calendar = getCalendar() else { - trace("in createCalendarEvent, there's no calendar", log: log, category: ConstantsLog.categoryWatchManager, type: .info) - return - } - - // if an interval is defined, and if time since last created event is less than interval, then don't create a new event - // substract 10 seconds, because user will probably select a multiple of 5, and also readings usually arrive every 5 minutes - // example user selects 10 minutes interval, next reading will arrive in exactly 10 minutes, time interval to be checked will be 590 seconds - if Int(Date().timeIntervalSince(timeStampLastProcessedReading)) < (UserDefaults.standard.calendarInterval * 60 - 10) { + private func processWatchState() { + DispatchQueue.main.async { - trace("in createCalendarEvent, less than %{public}@ minutes since last event, will not create a new event", log: log, category: ConstantsLog.categoryWatchManager, type: .info, UserDefaults.standard.calendarInterval.description) + // create two simple arrays to send to the live activiy. One with the bg values in mg/dL and another with the corresponding timestamps + // this is needed due to the not being able to pass structs that are not codable/hashable + let hoursOfBgReadingsToSend: Double = 12 - return + let bgReadings = self.bgReadingsAccessor.getLatestBgReadings(limit: nil, fromDate: Date().addingTimeInterval(-3600 * hoursOfBgReadingsToSend), forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false) - } - - // get 2 last Readings, with a calculatedValue - let lastReading = bgReadingsAccessor.get2LatestBgReadings(minimumTimeIntervalInMinutes: 4.0) - - // there should be at least one reading - guard lastReading.count > 0 else { - trace("in createCalendarEvent, there are no new readings to process", log: log, category: ConstantsLog.categoryWatchManager, type: .info) - return - } - - // latest reading should be less than 5 minutes old - guard abs(lastReading[0].timeStamp.timeIntervalSinceNow) < 5 * 60 else { - trace("in createCalendarEvent, the latest reading is older than 5 minutes", log: log, category: ConstantsLog.categoryWatchManager, type: .info) - return } - - // time to delete any existing events - deleteAllEvents(in: calendar) - - // compose the event title - // start with the reading in correct unit - var title = lastReading[0].unitizedString(unitIsMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl).description - - // add the visual indicator to the title to show what range the current - // reading is in - if (UserDefaults.standard.displayVisualIndicatorInCalendarEvent){ + let slopeOrdinal: Int = !bgReadings.isEmpty ? bgReadings[0].slopeOrdinal() : 1 - var visualIndicator = "" - - // get the current range of the last reading then - // configure the indicator based on the relevant range - switch lastReading[0].bgRangeDescription() { - case .inRange: - visualIndicator = ConstantsWatch.visualIndicatorInRange - case .notUrgent: - visualIndicator = ConstantsWatch.visualIndicatorNotUrgent - case .urgent: - visualIndicator = ConstantsWatch.visualIndicatorUrgent + var deltaChangeInMgDl: Double? + + // add delta if needed + if bgReadings.count > 1 { + deltaChangeInMgDl = bgReadings[0].currentSlope(previousBgReading: bgReadings[1]) * bgReadings[0].timeStamp.timeIntervalSince(bgReadings[1].timeStamp) * 1000; } - // pre-append the indicator to the title - title = visualIndicator + " " + title - } - - // add trend if needed and available - if (!lastReading[0].hideSlope && UserDefaults.standard.displayTrendInCalendarEvent) { - title = title + " " + lastReading[0].slopeArrow() - } - - // add delta if needed - if UserDefaults.standard.displayDeltaInCalendarEvent && lastReading.count > 1 { + var bgReadingValues: [Double] = [] + var bgReadingDates: [Date] = [] - title = title + " " + lastReading[0].unitizedDeltaString(previousBgReading: lastReading[1], showUnit: UserDefaults.standard.displayUnitInCalendarEvent, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) - - } else if UserDefaults.standard.displayUnitInCalendarEvent { - - // add unit if needed - title = title + " " + (UserDefaults.standard.bloodGlucoseUnitIsMgDl ? Texts_Common.mgdl : Texts_Common.mmol) - - } - - // create an event now - let event = EKEvent(eventStore: eventStore) - event.title = title - event.notes = ConstantsWatch.textInCreatedEvent - event.startDate = Date() - event.endDate = Date(timeIntervalSinceNow: 60 * 10) - event.calendar = calendar - - do{ - - try eventStore.save(event, span: .thisEvent) - - timeStampLastProcessedReading = lastReading[0].timeStamp - - } catch let error { - - trace("in createCalendarEvent, error while saving : %{public}@", log: log, category: ConstantsLog.categoryWatchManager, type: .error, error.localizedDescription) - - } - - } - - /// - gets all calendars on the device, if one of them has a title that matches the name stored in UserDefaults.standard.calenderId, then it returns that calendar. - /// - else returns the default calendar and sets the value in the UserDefaults to that default value - /// - also if currently there's no value in the UserDefaults, then value will be assigned here to UserDefaults.standard.calenderId - /// - nil as return value should normally not happen, because there should always be at least one calendar on the device - private func getCalendar() -> EKCalendar? { - - // get calendar title stored in the settings and compare to list - if let calendarIdInUserDefaults = UserDefaults.standard.calenderId { - - // get all calendars, if there's one having the same title return that one - for calendar in eventStore.calendars(for: .event) { - - if calendar.title == calendarIdInUserDefaults { - return calendar - } + for bgReading in bgReadings { + bgReadingValues.append(bgReading.calculatedValue) + bgReadingDates.append(bgReading.timeStamp) } - } - - // so there's no value in UserDefaults.standard.calenderId or there isn't a calendar that has a title as stored in UserDefaults.standard.calenderId - // set now UserDefaults.standard.calenderId to default calendar and return that one - UserDefaults.standard.calenderId = eventStore.defaultCalendarForNewEvents?.title - - return eventStore.defaultCalendarForNewEvents - - } - - // deletes all xdrip events in the calendar, for the last 24 hours - private func deleteAllEvents(in calendar:EKCalendar) { - - let predicate = eventStore.predicateForEvents(withStart: Date(timeIntervalSinceNow: -24*3600), end: Date(), calendars: [calendar]) - - let events = eventStore.events(matching: predicate) - - for event in events { - if let notes = event.notes { - if notes.contains(find: ConstantsWatch.textInCreatedEvent) { - do{ - try eventStore.remove(event, span: .thisEvent) - } catch let error { - trace("in deleteAllEvents, error while removing : %{public}@", log: log, category: ConstantsLog.categoryWatchManager, type: .error, error.localizedDescription) - } - } + // now process the WatchState + self.watchState.bgReadingValues = bgReadingValues + self.watchState.bgReadingDates = bgReadingDates + self.watchState.isMgDl = UserDefaults.standard.bloodGlucoseUnitIsMgDl + self.watchState.slopeOrdinal = slopeOrdinal + self.watchState.deltaChangeInMgDl = deltaChangeInMgDl + self.watchState.urgentLowLimitInMgDl = UserDefaults.standard.urgentLowMarkValue + self.watchState.lowLimitInMgDl = UserDefaults.standard.lowMarkValue + self.watchState.highLimitInMgDl = UserDefaults.standard.highMarkValue + self.watchState.urgentHighLimitInMgDl = UserDefaults.standard.urgentHighMarkValue + self.watchState.activeSensorDescription = UserDefaults.standard.activeSensorDescription + self.watchState.isMaster = UserDefaults.standard.isMaster + self.watchState.followerDataSourceTypeRawValue = UserDefaults.standard.followerDataSourceType.rawValue + self.watchState.followerBackgroundKeepAliveTypeRawValue = UserDefaults.standard.followerBackgroundKeepAliveType.rawValue + self.watchState.disableComplications = !UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType == .disabled + + if let sensorStartDate = UserDefaults.standard.activeSensorStartDate { + self.watchState.sensorAgeInMinutes = Double(Calendar.current.dateComponents([.minute], from: sensorStartDate, to: Date()).minute!) + } else { + self.watchState.sensorAgeInMinutes = 0 } + + self.watchState.sensorMaxAgeInMinutes = (UserDefaults.standard.activeSensorMaxSensorAgeInDays ?? 0) * 24 * 60 + + // let's set the state values if we're using a heartbeat + if let timeStampOfLastHeartBeat = UserDefaults.standard.timeStampOfLastHeartBeat, let secondsUntilHeartBeatDisconnectWarning = UserDefaults.standard.secondsUntilHeartBeatDisconnectWarning { + self.watchState.secondsUntilHeartBeatDisconnectWarning = Int(secondsUntilHeartBeatDisconnectWarning) + self.watchState.timeStampOfLastHeartBeat = timeStampOfLastHeartBeat + } + + // let's set the follower server connection values if we're using follower mode + if let timeStampOfLastFollowerConnection = UserDefaults.standard.timeStampOfLastFollowerConnection { + self.watchState.secondsUntilFollowerDisconnectWarning = UserDefaults.standard.followerDataSourceType.secondsUntilFollowerDisconnectWarning + self.watchState.timeStampOfLastFollowerConnection = timeStampOfLastFollowerConnection + } + + self.sendToWatch() } } + private func sendToWatch() { + guard let data = try? JSONEncoder().encode(watchState) else { + print("Watch state JSON encoding error") + return + } + + guard session.isReachable else { return } + + session.sendMessageData(data, replyHandler: nil) { error in + print("Cannot send data message to watch") + } + } + + + // MARK: - Public functions + + func updateWatchApp() { + processWatchState() + } +} + + +// MARK: - conform to WCSessionDelegate protocol + +extension WatchManager: WCSessionDelegate { + public func sessionDidBecomeInactive(_: WCSession) {} + + public func sessionDidDeactivate(_: WCSession) {} + + public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + } + + // process any received messages from the watch app + public func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { + + // if the action: refreshBGData message is received, then force the app to send new data to the Watch App + if let requestWatchStateUpdate = message["requestWatchStateUpdate"] as? Bool, requestWatchStateUpdate { + DispatchQueue.main.async { + self.sendToWatch() + } + } + } + + public func session(_: WCSession, didReceiveMessageData _: Data) {} + + public func sessionReachabilityDidChange(_ session: WCSession) { + if session.isReachable { + DispatchQueue.main.async { + self.sendToWatch() + } + } + } } diff --git a/xdrip/Managers/Watch/WatchState.swift b/xdrip/Managers/Watch/WatchState.swift new file mode 100644 index 00000000..caa101e0 --- /dev/null +++ b/xdrip/Managers/Watch/WatchState.swift @@ -0,0 +1,34 @@ +// +// WatchState.swift +// xdrip +// +// Created by Paul Plant on 21/2/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation + +/// model of the data we'll use to manage the watch views +struct WatchState: Codable { + var bgReadingValues: [Double] = [] + var bgReadingDates: [Date] = [] + var isMgDl: Bool? + var slopeOrdinal: Int? + var deltaChangeInMgDl: Double? + var urgentLowLimitInMgDl: Double? + var lowLimitInMgDl: Double? + var highLimitInMgDl: Double? + var urgentHighLimitInMgDl: Double? + var updatedDate: Date? + var activeSensorDescription: String? + var sensorAgeInMinutes: Double? + var sensorMaxAgeInMinutes: Double? + var isMaster: Bool? + var followerDataSourceTypeRawValue: Int? + var followerBackgroundKeepAliveTypeRawValue: Int? + var timeStampOfLastFollowerConnection: Date? + var secondsUntilFollowerDisconnectWarning: Int? + var timeStampOfLastHeartBeat: Date? + var secondsUntilHeartBeatDisconnectWarning: Int? + var disableComplications: Bool? +} diff --git a/xdrip/Managers/Widgets/WidgetSharedUserDefaultsModel.swift b/xdrip/Managers/Widgets/WidgetSharedUserDefaultsModel.swift new file mode 100644 index 00000000..f05022db --- /dev/null +++ b/xdrip/Managers/Widgets/WidgetSharedUserDefaultsModel.swift @@ -0,0 +1,24 @@ +// +// WidgetSharedUserDefaultsModel.swift +// xDrip Widget Extension +// +// Created by Paul Plant on 4/3/24. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import Foundation + +/// model of the data we'll store in the shared app group to pass from the watch app to the widgets +struct WidgetSharedUserDefaultsModel: Codable { + var bgReadingValues: [Double] + var bgReadingDatesAsDouble: [Double] + var isMgDl: Bool + var slopeOrdinal: Int + var deltaChangeInMgDl: Double + var urgentLowLimitInMgDl: Double + var lowLimitInMgDl: Double + var highLimitInMgDl: Double + var urgentHighLimitInMgDl: Double + var dataSourceDescription: String + var keepAliveImageString: String? +} diff --git a/xdrip/Storyboards/Base.lproj/Main.storyboard b/xdrip/Storyboards/Base.lproj/Main.storyboard index 95675075..031a4d16 100644 --- a/xdrip/Storyboards/Base.lproj/Main.storyboard +++ b/xdrip/Storyboards/Base.lproj/Main.storyboard @@ -1,9 +1,8 @@ - + - - + @@ -397,7 +396,7 @@ - + @@ -423,7 +422,7 @@ - + @@ -439,14 +438,14 @@ -