initial implementation of watch/complications background refresh
This commit is contained in:
parent
bfe1e9dcb4
commit
d2daad8b9a
|
@ -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
|
||||
|
||||
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))"
|
||||
|
||||
|
@ -346,25 +354,36 @@ 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?) {
|
||||
func session(_: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error _: Error?) {
|
||||
if activationState == .activated {
|
||||
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) {
|
||||
func sessionReachabilityDidChange(_ session: WCSession) {}
|
||||
|
||||
func session(_: WCSession, didReceiveMessageData messageData: Data) {}
|
||||
|
||||
func session(_: WCSession, didReceiveMessage message: [String : Any]) {
|
||||
let watchStateAsDictionary = message["watchState"] as! [String : Any]
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.processState(watchState)
|
||||
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)
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,8 +53,6 @@ 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
|
||||
|
@ -65,16 +69,16 @@ public final class WatchManager: NSObject, ObservableObject {
|
|||
}
|
||||
|
||||
var bgReadingValues: [Double] = []
|
||||
var bgReadingDates: [Date] = []
|
||||
var bgReadingDatesAsDouble: [Double] = []
|
||||
|
||||
for bgReading in bgReadings {
|
||||
bgReadingValues.append(bgReading.calculatedValue)
|
||||
bgReadingDates.append(bgReading.timeStamp)
|
||||
bgReadingDatesAsDouble.append(bgReading.timeStamp.timeIntervalSince1970)
|
||||
}
|
||||
|
||||
// now process the WatchState
|
||||
self.watchState.bgReadingValues = bgReadingValues
|
||||
self.watchState.bgReadingDates = bgReadingDates
|
||||
self.watchState.bgReadingDatesAsDouble = bgReadingDatesAsDouble
|
||||
self.watchState.isMgDl = UserDefaults.standard.bloodGlucoseUnitIsMgDl
|
||||
self.watchState.slopeOrdinal = slopeOrdinal
|
||||
self.watchState.deltaChangeInMgDl = deltaChangeInMgDl
|
||||
|
@ -108,20 +112,50 @@ public final class WatchManager: NSObject, ObservableObject {
|
|||
self.watchState.timeStampOfLastFollowerConnection = timeStampOfLastFollowerConnection
|
||||
}
|
||||
|
||||
self.sendToWatch()
|
||||
}
|
||||
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 {
|
||||
// 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.sendToWatch()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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] }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue