Merge branch 'develop' into feature/contact-trick
# Conflicts: # xdrip.xcodeproj/project.pbxproj # xdrip/Constants/ConstantsLog.swift # xdrip/View Controllers/Root View Controller/RootViewController.swift
|
@ -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
|
||||
|
|
|
@ -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: |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,6 +6,7 @@ xDripConfigOverride.xcconfig
|
|||
UserInterfaceState.xcuserstate
|
||||
iBreakpoints_v2.xcbkptlist
|
||||
xdrip.xcodeproj/xcuserdata/johandegraeve.xcuserdatad/xcdebugger/
|
||||
Package.resolved
|
||||
|
||||
# Ruby version settings
|
||||
.ruby-version
|
||||
|
|
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.5 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 61 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 15 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 15 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 5.1 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 77 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 8.7 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 5.9 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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 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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 3.2 KiB |
|
@ -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
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 4.5 KiB |
|
@ -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
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 5.9 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 606 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
Before Width: | Height: | Size: 606 KiB |
|
@ -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" = "---";
|
|
@ -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" = "---";
|
Before Width: | Height: | Size: 270 KiB |
|
@ -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" = "---";
|
|
@ -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" = "---";
|
|
@ -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
|
||||
|
|
|
@ -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)_
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon-1025.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 588 KiB |
|
@ -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"
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon-1025.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 588 KiB |
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "20.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-122.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
BIN
xDrip Watch Complication/Assets.xcassets/ComplicationIcon.imageset/Icon-122.png
vendored
Normal file
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon-1025.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 588 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|