initial implementation of watch/complications background refresh

This commit is contained in:
Paul Plant 2024-04-09 18:53:27 +02:00
parent bfe1e9dcb4
commit d2daad8b9a
11 changed files with 250 additions and 144 deletions

View File

@ -14,7 +14,7 @@ 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 {
final class WatchStateModel: NSObject, ObservableObject {
/// the Watch Connectivity session
var session: WCSession
@ -26,8 +26,9 @@ class WatchStateModel: NSObject, ObservableObject {
var bgReadingValues: [Double] = []
var bgReadingDates: [Date] = []
var bgReadingDatesAsDouble: [Double] = []
@Published var updatedDatesString: String = ""
// @Published var updatedDatesString: String = ""
@Published var isMgDl: Bool = true
@Published var slopeOrdinal: Int = 0
@ -48,12 +49,14 @@ class WatchStateModel: NSObject, ObservableObject {
@Published var followerDataSourceType: FollowerDataSourceType = .nightscout
@Published var followerBackgroundKeepAliveType: FollowerBackgroundKeepAliveType = .normal
@Published var disableComplications: Bool = false
@Published var remainingComplicationUserInfoTransfers: Int = 99
@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
@Published var lastComplicationUpdateTimeStamp: Date = .distantPast
init(session: WCSession = .default) {
self.session = session
@ -240,39 +243,41 @@ class WatchStateModel: NSObject, ObservableObject {
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)
print("Requesting watch state update from iOS")
session.sendMessage(["requestWatchUpdate": "watchState"], 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()
private func processWatchStateFromDictionary(dictionary: [String: Any]) {
let bgReadingDatesFromDictionary: [Double] = dictionary["bgReadingDatesAsDouble"] as? [Double] ?? [0]
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
bgReadingDates = bgReadingDatesFromDictionary.map { (bgReadingDateAsDouble) -> Date in
return Date(timeIntervalSince1970: bgReadingDateAsDouble)
}
bgReadingValues = dictionary["bgReadingValues"] as? [Double] ?? [100]
isMgDl = dictionary["isMgDl"] as? Bool ?? true
slopeOrdinal = dictionary["slopeOrdinal"] as? Int ?? 0
deltaChangeInMgDl = dictionary["deltaChangeInMgDl"] as? Double ?? 0
urgentLowLimitInMgDl = dictionary["urgentLowLimitInMgDl"] as? Double ?? 60
lowLimitInMgDl = dictionary["lowLimitInMgDl"] as? Double ?? 70
highLimitInMgDl = dictionary["highLimitInMgDl"] as? Double ?? 180
urgentHighLimitInMgDl = dictionary["urgentHighLimitInMgDl"] as? Double ?? 250
updatedDate = dictionary["updatedDate"] as? Date ?? .now
activeSensorDescription = dictionary["activeSensorDescription"] as? String ?? ""
sensorAgeInMinutes = dictionary["sensorAgeInMinutes"] as? Double ?? 0
sensorMaxAgeInMinutes = dictionary["sensorMaxAgeInMinutes"] as? Double ?? 0
isMaster = dictionary["isMaster"] as? Bool ?? true
followerDataSourceType = FollowerDataSourceType(rawValue: dictionary["followerDataSourceTypeRawValue"] as? Int ?? 0) ?? .nightscout
followerBackgroundKeepAliveType = FollowerBackgroundKeepAliveType(rawValue: dictionary["followerBackgroundKeepAliveTypeRawValue"] as? Int ?? 0) ?? .normal
timeStampOfLastHeartBeat = dictionary["timeStampOfLastHeartBeat"] as? Date ?? .distantPast
secondsUntilHeartBeatDisconnectWarning = dictionary["secondsUntilHeartBeatDisconnectWarning"] as? Int ?? 0
disableComplications = dictionary["disableComplications"] as? Bool ?? false
remainingComplicationUserInfoTransfers = dictionary["remainingComplicationUserInfoTransfers"] as? Int ?? 99
// check if there is any BG data available before updating the data source info strings accordingly
if let bgReadingDate = bgReadingDate() {
@ -286,17 +291,14 @@ class WatchStateModel: NSObject, ObservableObject {
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
}
updateComplicationData()
}
/// 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() {
private func updateComplicationData() {
// limit the update calls to once every 50 seconds. No real reason but it makes no sense to try and
// update userdefaults every few seconds if there is no way the data could have changed yet
guard lastComplicationUpdateTimeStamp < Date().addingTimeInterval(-50) else { return }
guard let sharedUserDefaults = UserDefaults(suiteName: Bundle.main.appGroupSuiteName) else { return }
@ -304,7 +306,7 @@ class WatchStateModel: NSObject, ObservableObject {
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)
let complicationSharedUserDefaultsModel = ComplicationSharedUserDefaultsModel(bgReadingValues: bgReadingValues, bgReadingDatesAsDouble: bgReadingDatesAsDouble, isMgDl: isMgDl, slopeOrdinal: slopeOrdinal, deltaChangeInMgDl: deltaChangeInMgDl, urgentLowLimitInMgDl: urgentLowLimitInMgDl, lowLimitInMgDl: lowLimitInMgDl, highLimitInMgDl: highLimitInMgDl, urgentHighLimitInMgDl: urgentHighLimitInMgDl, disableComplications: disableComplications, remainingComplicationUserInfoTransfers: remainingComplicationUserInfoTransfers)
// 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
@ -314,8 +316,11 @@ class WatchStateModel: NSObject, ObservableObject {
// now that the new data is stored in the app group, try to force the complications to reload
WidgetCenter.shared.reloadAllTimelines()
lastComplicationUpdateTimeStamp = .now
}
// generate a debugString
private func generateDebugString() -> String {
@ -330,6 +335,9 @@ class WatchStateModel: NSObject, ObservableObject {
debugString += "\nBG values: \(bgReadingValues.count)"
debugString += "\nComp updated: \(lastComplicationUpdateTimeStamp.formatted(date: .omitted, time: .standard))"
debugString += "\nComp remain: \(remainingComplicationUserInfoTransfers.description)/50"
if !isMaster {
debugString += "\nFollower conn.: \(timeStampOfLastFollowerConnection.formatted(date: .omitted, time: .standard))"
@ -345,26 +353,37 @@ class WatchStateModel: NSObject, ObservableObject {
}
}
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()
extension WatchStateModel: WCSessionDelegate {
func session(_: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error _: Error?) {
if activationState == .activated {
requestWatchStateUpdate()
}
}
func session(_: WCSession, didReceiveMessage _: [String: Any]) {}
func sessionReachabilityDidChange(_ session: WCSession) {}
func sessionReachabilityDidChange(_ session: WCSession) {
}
func session(_: WCSession, didReceiveMessageData messageData: Data) {}
func session(_: WCSession, didReceiveMessageData messageData: Data) {
if let watchState = try? JSONDecoder().decode(WatchState.self, from: messageData) {
DispatchQueue.main.async {
self.processState(watchState)
func session(_: WCSession, didReceiveMessage message: [String : Any]) {
let watchStateAsDictionary = message["watchState"] as! [String : Any]
DispatchQueue.main.async {
self.processWatchStateFromDictionary(dictionary: watchStateAsDictionary)
// 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
}
}
}
// func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
let watchStateAsDictionary = userInfo["watchState"] as! [String : Any]
DispatchQueue.main.async {
self.processWatchStateFromDictionary(dictionary: watchStateAsDictionary)
}
// }
}
}

View File

@ -20,4 +20,7 @@ struct ComplicationSharedUserDefaultsModel: Codable {
var highLimitInMgDl: Double
var urgentHighLimitInMgDl: Double
var disableComplications: Bool
// TODO: Debug only. Remove for production.
var remainingComplicationUserInfoTransfers: Int
}

View File

@ -29,6 +29,12 @@ extension XDripWatchComplication.EntryView {
Spacer()
// TODO: Debug only. Remove for production.
Text("[\(entry.widgetState.remainingComplicationUserInfoTransfers)]")
.font(.system(size: entry.widgetState.isSmallScreen() ? 12 : 14))
.foregroundStyle(Color(.cyan))
.minimumScaleFactor(0.2)
Text("\(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")")
.font(.system(size: entry.widgetState.isSmallScreen() ? 14 : 16))
.foregroundStyle(Color(white: 0.7))

View File

@ -32,14 +32,17 @@ extension XDripWatchComplication.Entry {
var lowLimitInMgDl: Double
var highLimitInMgDl: Double
var urgentHighLimitInMgDl: Double
var disableComplications: Bool
// TODO: Debug only. Remove for production.
var remainingComplicationUserInfoTransfers: Int
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) {
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, remainingComplicationUserInfoTransfers: Int? = 99) {
self.bgReadingValues = bgReadingValues
self.bgReadingDates = bgReadingDates
self.isMgDl = isMgDl ?? true
@ -49,13 +52,15 @@ extension XDripWatchComplication.Entry {
self.lowLimitInMgDl = lowLimitInMgDl ?? 80
self.highLimitInMgDl = highLimitInMgDl ?? 180
self.urgentHighLimitInMgDl = urgentHighLimitInMgDl ?? 250
self.disableComplications = disableComplications ?? false
// TODO: Debug only. Remove for production.
self.remainingComplicationUserInfoTransfers = remainingComplicationUserInfoTransfers ?? 99
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

View File

@ -51,7 +51,7 @@ extension XDripWatchComplication.Provider {
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)
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, remainingComplicationUserInfoTransfers: data.remainingComplicationUserInfoTransfers)
} catch {
print(error.localizedDescription)
}

View File

@ -85,6 +85,7 @@
478A92652B90AB040084C394 /* GlucoseChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C210EF2B52A05B00005711 /* GlucoseChartView.swift */; };
478A92662B90AB230084C394 /* GlucoseChartType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C210ED2B5298EB00005711 /* GlucoseChartType.swift */; };
478A92682B90ABB30084C394 /* ConstantsGlucoseChartSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716A50C2B416EE100419052 /* ConstantsGlucoseChartSwiftUI.swift */; };
47930E822BC5A001000BD8A1 /* ConstantsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47930E812BC5A001000BD8A1 /* ConstantsWidget.swift */; };
479359862B88B95A007D3CEE /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4716A4EE2B406C3D00419052 /* WidgetKit.framework */; };
479359872B88B95A007D3CEE /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4716A4F02B406C3D00419052 /* SwiftUI.framework */; };
4793598A2B88B95A007D3CEE /* XDripWatchComplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 479359892B88B95A007D3CEE /* XDripWatchComplication.swift */; };
@ -871,6 +872,7 @@
478A925E2B8FB5290084C394 /* XDripWatchComplication+Entry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XDripWatchComplication+Entry.swift"; sourceTree = "<group>"; };
478A92602B8FB53B0084C394 /* XDripWatchComplication+EntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XDripWatchComplication+EntryView.swift"; sourceTree = "<group>"; };
478A92622B8FB5490084C394 /* XDripWatchComplication+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XDripWatchComplication+Provider.swift"; sourceTree = "<group>"; };
47930E812BC5A001000BD8A1 /* ConstantsWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConstantsWidget.swift; sourceTree = "<group>"; };
479359852B88B95A007D3CEE /* xDrip Watch Complication Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "xDrip Watch Complication Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
479359892B88B95A007D3CEE /* XDripWatchComplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDripWatchComplication.swift; sourceTree = "<group>"; };
4793598B2B88B95B007D3CEE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -3083,6 +3085,7 @@
F8A1586E22EDC7EE007F5B5D /* ConstantsSuspensionPrevention.swift */,
F8AF36142455C6F700B5977B /* ConstantsTrace.swift */,
F8FDFEAC260DE1B90047597D /* ConstantsUI.swift */,
47930E812BC5A001000BD8A1 /* ConstantsWidget.swift */,
);
name = Constants;
path = xdrip/Constants;
@ -4241,6 +4244,7 @@
F8C97859242AAE7B00A09483 /* MiaoMiao+BluetoothPeripheral.swift in Sources */,
476FE8FF2B2F1D1700537E0A /* ConstantsFollower.swift in Sources */,
F8EEDD5422FF685400D2D610 /* NSMutableURLRequest.swift in Sources */,
47930E822BC5A001000BD8A1 /* ConstantsWidget.swift in Sources */,
F897AAFB2201018800CDDD10 /* String.swift in Sources */,
F85542342B7574330058CE09 /* OmniPodHeartBeat+CoreDataProperties.swift in Sources */,
F8B3A847227F090E004BA588 /* SettingsViewNightScoutSettingsViewModel.swift in Sources */,

View File

@ -43,6 +43,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableAddressSanitizer = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@ -143,7 +143,7 @@ enum ConstantsLog {
static let categoryCalendarManager = "CalendarManager "
/// WatchManager logging
static let categoryWatchManager = "WatchManager "
static let categoryWatchManager = "WatchManager "
/// bluetoothPeripheralViewController
static let categoryBluetoothPeripheralViewController = "blePeripheralViewController "

View File

@ -0,0 +1,17 @@
//
// ConstantsWidget.swift
// xdrip
//
// Created by Paul Plant on 9/4/24.
// Copyright © 2024 Johan Degraeve. All rights reserved.
//
import Foundation
enum ConstantsWidget {
/// the amount of time that should pass before a complication refresh is forced
/// bear in mind there is a limit to 50 times per day whilst the Watch session is available
/// so maybe 17 hours / 50 times = every 20.4 minutes
static let forceComplicationRefreshTimeInMinutes = TimeInterval(minutes: 20)
}

View File

@ -9,13 +9,14 @@
import Foundation
import WatchConnectivity
import WidgetKit
import OSLog
public final class WatchManager: NSObject, ObservableObject {
final class WatchManager: NSObject, ObservableObject {
// MARK: - private properties
/// a watch connectivity session instance
private let session: WCSession
private var session: WCSession
/// a BgReadingsAccessor instance
private var bgReadingsAccessor: BgReadingsAccessor
@ -26,6 +27,11 @@ public final class WatchManager: NSObject, ObservableObject {
/// hold the current watch state model
private var watchState = WatchState()
private var lastForcedComplicationUpdateTimeStamp: Date = .distantPast
/// for logging
private var log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryWatchManager)
// MARK: - intializer
init(coreDataManager: CoreDataManager, session: WCSession = .default) {
@ -33,8 +39,8 @@ public final class WatchManager: NSObject, ObservableObject {
// set coreDataManager and bgReadingsAccessor
self.coreDataManager = coreDataManager
self.bgReadingsAccessor = BgReadingsAccessor(coreDataManager: coreDataManager)
self.session = session
super.init()
if WCSession.isSupported() {
@ -47,81 +53,109 @@ public final class WatchManager: NSObject, ObservableObject {
}
private func processWatchState() {
DispatchQueue.main.async {
// create two simple arrays to send to the live activiy. One with the bg values in mg/dL and another with the corresponding timestamps
// this is needed due to the not being able to pass structs that are not codable/hashable
let hoursOfBgReadingsToSend: Double = 12
let bgReadings = self.bgReadingsAccessor.getLatestBgReadings(limit: nil, fromDate: Date().addingTimeInterval(-3600 * hoursOfBgReadingsToSend), forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false)
let slopeOrdinal: Int = !bgReadings.isEmpty ? bgReadings[0].slopeOrdinal() : 1
var deltaChangeInMgDl: Double?
// add delta if needed
if bgReadings.count > 1 {
deltaChangeInMgDl = bgReadings[0].currentSlope(previousBgReading: bgReadings[1]) * bgReadings[0].timeStamp.timeIntervalSince(bgReadings[1].timeStamp) * 1000;
}
var bgReadingValues: [Double] = []
var bgReadingDates: [Date] = []
for bgReading in bgReadings {
bgReadingValues.append(bgReading.calculatedValue)
bgReadingDates.append(bgReading.timeStamp)
}
// now process the WatchState
self.watchState.bgReadingValues = bgReadingValues
self.watchState.bgReadingDates = bgReadingDates
self.watchState.isMgDl = UserDefaults.standard.bloodGlucoseUnitIsMgDl
self.watchState.slopeOrdinal = slopeOrdinal
self.watchState.deltaChangeInMgDl = deltaChangeInMgDl
self.watchState.urgentLowLimitInMgDl = UserDefaults.standard.urgentLowMarkValue
self.watchState.lowLimitInMgDl = UserDefaults.standard.lowMarkValue
self.watchState.highLimitInMgDl = UserDefaults.standard.highMarkValue
self.watchState.urgentHighLimitInMgDl = UserDefaults.standard.urgentHighMarkValue
self.watchState.activeSensorDescription = UserDefaults.standard.activeSensorDescription
self.watchState.isMaster = UserDefaults.standard.isMaster
self.watchState.followerDataSourceTypeRawValue = UserDefaults.standard.followerDataSourceType.rawValue
self.watchState.followerBackgroundKeepAliveTypeRawValue = UserDefaults.standard.followerBackgroundKeepAliveType.rawValue
self.watchState.disableComplications = !UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType == .disabled
if let sensorStartDate = UserDefaults.standard.activeSensorStartDate {
self.watchState.sensorAgeInMinutes = Double(Calendar.current.dateComponents([.minute], from: sensorStartDate, to: Date()).minute!)
} else {
self.watchState.sensorAgeInMinutes = 0
}
self.watchState.sensorMaxAgeInMinutes = (UserDefaults.standard.activeSensorMaxSensorAgeInDays ?? 0) * 24 * 60
// let's set the state values if we're using a heartbeat
if let timeStampOfLastHeartBeat = UserDefaults.standard.timeStampOfLastHeartBeat, let secondsUntilHeartBeatDisconnectWarning = UserDefaults.standard.secondsUntilHeartBeatDisconnectWarning {
self.watchState.secondsUntilHeartBeatDisconnectWarning = Int(secondsUntilHeartBeatDisconnectWarning)
self.watchState.timeStampOfLastHeartBeat = timeStampOfLastHeartBeat
}
// let's set the follower server connection values if we're using follower mode
if let timeStampOfLastFollowerConnection = UserDefaults.standard.timeStampOfLastFollowerConnection {
self.watchState.secondsUntilFollowerDisconnectWarning = UserDefaults.standard.followerDataSourceType.secondsUntilFollowerDisconnectWarning
self.watchState.timeStampOfLastFollowerConnection = timeStampOfLastFollowerConnection
}
self.sendToWatch()
// create two simple arrays to send to the live activiy. One with the bg values in mg/dL and another with the corresponding timestamps
// this is needed due to the not being able to pass structs that are not codable/hashable
let hoursOfBgReadingsToSend: Double = 12
let bgReadings = self.bgReadingsAccessor.getLatestBgReadings(limit: nil, fromDate: Date().addingTimeInterval(-3600 * hoursOfBgReadingsToSend), forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false)
let slopeOrdinal: Int = !bgReadings.isEmpty ? bgReadings[0].slopeOrdinal() : 1
var deltaChangeInMgDl: Double?
// add delta if needed
if bgReadings.count > 1 {
deltaChangeInMgDl = bgReadings[0].currentSlope(previousBgReading: bgReadings[1]) * bgReadings[0].timeStamp.timeIntervalSince(bgReadings[1].timeStamp) * 1000;
}
var bgReadingValues: [Double] = []
var bgReadingDatesAsDouble: [Double] = []
for bgReading in bgReadings {
bgReadingValues.append(bgReading.calculatedValue)
bgReadingDatesAsDouble.append(bgReading.timeStamp.timeIntervalSince1970)
}
// now process the WatchState
self.watchState.bgReadingValues = bgReadingValues
self.watchState.bgReadingDatesAsDouble = bgReadingDatesAsDouble
self.watchState.isMgDl = UserDefaults.standard.bloodGlucoseUnitIsMgDl
self.watchState.slopeOrdinal = slopeOrdinal
self.watchState.deltaChangeInMgDl = deltaChangeInMgDl
self.watchState.urgentLowLimitInMgDl = UserDefaults.standard.urgentLowMarkValue
self.watchState.lowLimitInMgDl = UserDefaults.standard.lowMarkValue
self.watchState.highLimitInMgDl = UserDefaults.standard.highMarkValue
self.watchState.urgentHighLimitInMgDl = UserDefaults.standard.urgentHighMarkValue
self.watchState.activeSensorDescription = UserDefaults.standard.activeSensorDescription
self.watchState.isMaster = UserDefaults.standard.isMaster
self.watchState.followerDataSourceTypeRawValue = UserDefaults.standard.followerDataSourceType.rawValue
self.watchState.followerBackgroundKeepAliveTypeRawValue = UserDefaults.standard.followerBackgroundKeepAliveType.rawValue
self.watchState.disableComplications = !UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType == .disabled
if let sensorStartDate = UserDefaults.standard.activeSensorStartDate {
self.watchState.sensorAgeInMinutes = Double(Calendar.current.dateComponents([.minute], from: sensorStartDate, to: Date()).minute!)
} else {
self.watchState.sensorAgeInMinutes = 0
}
self.watchState.sensorMaxAgeInMinutes = (UserDefaults.standard.activeSensorMaxSensorAgeInDays ?? 0) * 24 * 60
// let's set the state values if we're using a heartbeat
if let timeStampOfLastHeartBeat = UserDefaults.standard.timeStampOfLastHeartBeat, let secondsUntilHeartBeatDisconnectWarning = UserDefaults.standard.secondsUntilHeartBeatDisconnectWarning {
self.watchState.secondsUntilHeartBeatDisconnectWarning = Int(secondsUntilHeartBeatDisconnectWarning)
self.watchState.timeStampOfLastHeartBeat = timeStampOfLastHeartBeat
}
// let's set the follower server connection values if we're using follower mode
if let timeStampOfLastFollowerConnection = UserDefaults.standard.timeStampOfLastFollowerConnection {
self.watchState.secondsUntilFollowerDisconnectWarning = UserDefaults.standard.followerDataSourceType.secondsUntilFollowerDisconnectWarning
self.watchState.timeStampOfLastFollowerConnection = timeStampOfLastFollowerConnection
}
self.watchState.remainingComplicationUserInfoTransfers = self.session.remainingComplicationUserInfoTransfers
self.sendStateToWatch()
}
private func sendToWatch() {
guard let data = try? JSONEncoder().encode(watchState) else {
print("Watch state JSON encoding error")
func sendStateToWatch() {
guard session.isPaired else {
trace("No Watch is paired", log: self.log, category: ConstantsLog.categoryWatchManager, type: .debug)
return
}
guard session.isReachable else { return }
guard session.isWatchAppInstalled else {
trace("Watch app is not installed", log: self.log, category: ConstantsLog.categoryWatchManager, type: .debug)
return
}
session.sendMessageData(data, replyHandler: nil) { error in
print("Cannot send data message to watch")
guard session.activationState == .activated else {
let activationStateString = "\(session.activationState)"
trace("Watch session activationState = %{public}@. Reactivating", log: self.log, category: ConstantsLog.categoryWatchManager, type: .error, activationStateString)
session.activate()
return
}
// if the WCSession is reachable it means that Watch app is in the foreground so send the watch state as a message
// if it's not reachable, then it means it's in the background so send the state as a userInfo
// if more than x minutes have passed since the last complication update, call transferCurrentComplicationUserInfo to force an update
// if not, then just send it as a normal priority transferUserInfo which will be queued and sent as soon as the watch app is reachable again (this will help get the app showing data quicker)
if let userInfo: [String: Any] = watchState.asDictionary {
if session.isReachable {
session.sendMessage(["watchState": userInfo], replyHandler: nil, errorHandler: { (error) -> Void in
trace("Error sending watch state, error = %{public}@", log: self.log, category: ConstantsLog.categoryWatchManager, type: .error, error.localizedDescription)
})
} else {
if lastForcedComplicationUpdateTimeStamp < Date().addingTimeInterval(-ConstantsWidget.forceComplicationRefreshTimeInMinutes * 60), session.isComplicationEnabled {
trace("Forcing background complication update, remaining complication transfers today = %{public}@", log: self.log, category: ConstantsLog.categoryWatchManager, type: .info, session.remainingComplicationUserInfoTransfers.description)
session.transferCurrentComplicationUserInfo(["watchState": userInfo])
lastForcedComplicationUpdateTimeStamp = .now
} else {
trace("Sending background watch state update", log: self.log, category: ConstantsLog.categoryWatchManager, type: .info)
session.transferUserInfo(["watchState": userInfo])
}
}
}
}
@ -137,30 +171,41 @@ public final class WatchManager: NSObject, ObservableObject {
// MARK: - conform to WCSessionDelegate protocol
extension WatchManager: WCSessionDelegate {
public func sessionDidBecomeInactive(_: WCSession) {}
func sessionDidBecomeInactive(_: WCSession) {}
public func sessionDidDeactivate(_: WCSession) {}
func sessionDidDeactivate(_: WCSession) {
session = WCSession.default
session.delegate = self
session.activate()
}
public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
// process any received messages from the watch app
public func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
// if the action: refreshBGData message is received, then force the app to send new data to the Watch App
if let requestWatchStateUpdate = message["requestWatchStateUpdate"] as? Bool, requestWatchStateUpdate {
DispatchQueue.main.async {
self.sendToWatch()
// check which type of update the Watch is requesting and call the correct sending function as needed
if let requestWatchUpdate = message["requestWatchUpdate"] as? String {
switch requestWatchUpdate {
case "watchState":
DispatchQueue.main.async {
self.sendStateToWatch()
}
default:
break
}
}
}
public func session(_: WCSession, didReceiveMessageData _: Data) {}
func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {}
public func sessionReachabilityDidChange(_ session: WCSession) {
func session(_: WCSession, didReceiveMessageData _: Data) {}
func sessionReachabilityDidChange(_ session: WCSession) {
if session.isReachable {
DispatchQueue.main.async {
self.sendToWatch()
self.sendStateToWatch()
}
}
}

View File

@ -11,7 +11,7 @@ import Foundation
/// model of the data we'll use to manage the watch views
struct WatchState: Codable {
var bgReadingValues: [Double] = []
var bgReadingDates: [Date] = []
var bgReadingDatesAsDouble: [Double] = []
var isMgDl: Bool?
var slopeOrdinal: Int?
var deltaChangeInMgDl: Double?
@ -31,4 +31,10 @@ struct WatchState: Codable {
var timeStampOfLastHeartBeat: Date?
var secondsUntilHeartBeatDisconnectWarning: Int?
var disableComplications: Bool?
var remainingComplicationUserInfoTransfers: Int?
var asDictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}