Merge branch 'develop' into feature/contact-trick

# Conflicts:
#	xdrip.xcodeproj/project.pbxproj
#	xdrip/Constants/ConstantsLog.swift
#	xdrip/View Controllers/Root View Controller/RootViewController.swift
This commit is contained in:
Iurii Malchenko 2024-04-11 15:52:13 +02:00
commit da1d5ab74a
329 changed files with 10683 additions and 3402 deletions

View File

@ -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

View File

@ -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: |

View File

@ -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

View File

@ -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

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ xDripConfigOverride.xcconfig
UserInterfaceState.xcuserstate
iBreakpoints_v2.xcbkptlist
xdrip.xcodeproj/xcuserdata/johandegraeve.xcuserdatad/xcdebugger/
Package.resolved
# Ruby version settings
.ruby-version

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -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"
}
}

View File

@ -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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -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"
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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<WKRefreshBackgroundTask>) {
// 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 youre 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 youre done.
connectivityTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once youre 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
}
}

View File

@ -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()
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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
}
}

View File

@ -1,83 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder.WatchKit.Storyboard" version="3.0" toolsVersion="19455" targetRuntime="watchKit" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="AgC-eL-Hgc">
<device id="watch45"/>
<dependencies>
<deployment identifier="watchOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBWatchKitPlugin" version="19454"/>
</dependencies>
<scenes>
<!--Interface Controller-->
<scene sceneID="aou-V4-d1y">
<objects>
<controller id="AgC-eL-Hgc" customClass="InterfaceController" customModule="xDrip4iO5" customModuleProvider="target">
<items>
<group width="1" alignment="center" verticalAlignment="center" layout="vertical" spacing="5" id="Wqm-hO-ln6">
<items>
<group width="1" alignment="left" id="QuI-vf-khn">
<items>
<imageView alignment="left" verticalAlignment="center" image="50" id="R1P-qv-01V">
<variation key="device=watch38mm" image="40"/>
</imageView>
<group alignment="right" layout="vertical" spacing="10" id="olC-KT-ibd">
<items>
<label alignment="right" text="---" textAlignment="right" minimumScaleFactor="0.0" id="oq4-iM-qEv">
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="font" style="UICTFontTextStyleBody"/>
</label>
<label alignment="right" text="---" textAlignment="right" minimumScaleFactor="0.0" id="RJK-M1-Bu5">
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="font" style="UICTFontTextStyleBody"/>
</label>
</items>
<edgeInsets key="margins" left="0.0" right="0.0" top="5" bottom="5"/>
</group>
</items>
<edgeInsets key="margins" left="0.0" right="0.0" top="0.0" bottom="0.0"/>
<variation key="device=watch38mm">
<edgeInsets key="margins" left="0.0" right="0.0" top="0.0" bottom="0.0"/>
</variation>
<variation key="device=watch40mm">
<edgeInsets key="margins" left="0.0" right="0.0" top="0.0" bottom="0.0"/>
</variation>
<variation key="device=watch41mm">
<edgeInsets key="margins" left="0.0" right="0.0" top="0.0" bottom="0.0"/>
</variation>
<variation key="device=watch42mm">
<edgeInsets key="margins" left="0.0" right="0.0" top="0.0" bottom="0.0"/>
</variation>
</group>
<separator alignment="center" id="Y5j-Vc-Vi6">
<color key="color" white="0.20000000000000001" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</separator>
<label width="1" alignment="center" text="Waiting for data..." textAlignment="center" minimumScaleFactor="0.0" id="ies-y9-I8N">
<gestureRecognizers>
<longPressGestureRecognizer minimumPressDuration="1" id="WxN-fs-nqg">
<connections>
<action selector="longPressToRefresh:" destination="AgC-eL-Hgc" id="cET-Bm-UeH"/>
</connections>
</longPressGestureRecognizer>
</gestureRecognizers>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="font" type="system" pointSize="70"/>
</label>
</items>
<edgeInsets key="margins" left="0.0" right="0.0" top="0.0" bottom="0.0"/>
</group>
</items>
<connections>
<outlet property="deltaLabelOutlet" destination="RJK-M1-Bu5" id="l8b-2v-mUL"/>
<outlet property="iconImageOutlet" destination="R1P-qv-01V" id="sjM-Oo-khn"/>
<outlet property="minutesAgoLabelOutlet" destination="oq4-iM-qEv" id="gWX-Fo-FxU"/>
<outlet property="valueLabelOutlet" destination="ies-y9-I8N" id="Epb-zJ-poe"/>
</connections>
</controller>
</objects>
<point key="canvasLocation" x="163" y="80"/>
</scene>
</scenes>
<resources>
<image name="40" width="40" height="40"/>
<image name="50" width="50" height="50"/>
</resources>
</document>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

View File

@ -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" = "---";

View File

@ -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" = "---";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

View File

@ -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" = "---";

View File

@ -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" = "---";

View File

@ -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

View File

@ -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)_

View File

@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "Icon-1025.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

View File

@ -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"
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "Icon-1025.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

View File

@ -1,11 +1,11 @@
{
"images" : [
{
"filename" : "20.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Icon-122.png",
"idiom" : "universal",
"scale" : "2x"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}

View File

@ -2,15 +2,14 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
<key>MainAppBundleIdentifier</key>
<string>$(MAIN_APP_BUNDLE_IDENTIFIER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>WKAppBundleIdentifier</key>
<string>$(MAIN_APP_BUNDLE_IDENTIFIER).watchkitapp</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.watchkit</string>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)")
}
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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")
}
}
}
}

View File

@ -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<Entry>) -> ()) {
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)
}
}

View File

@ -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
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "Icon-1025.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

Some files were not shown because too many files have changed in this diff Show More