improvements to Watch app in follower mode
This commit is contained in:
parent
b1773513e9
commit
9fc843a849
|
@ -7,6 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
enum ConstantsAppleWatch {
|
enum ConstantsAppleWatch {
|
||||||
|
|
||||||
|
@ -19,4 +20,10 @@ enum ConstantsAppleWatch {
|
||||||
/// less than how many pixels wide should we consider the screen size to the "small"
|
/// less than how many pixels wide should we consider the screen size to the "small"
|
||||||
static let pixelWidthLimitForSmallScreen: Double = 185
|
static let pixelWidthLimitForSmallScreen: Double = 185
|
||||||
|
|
||||||
|
/// colour for the "requesting data" symbol
|
||||||
|
static let requestingDataIconColorActive = Color(.green) //.opacity(0.9)
|
||||||
|
static let requestingDataIconColorInactive = Color(.white).opacity(0.3)
|
||||||
|
static let requestingDataIconFontSize: CGFloat = 6
|
||||||
|
static let requestingDataIconSFSymbolName: String = "circle.fill" // "iphone.gen3.radiowaves.left.and.right" //
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,8 +53,7 @@ class WatchStateModel: NSObject, ObservableObject {
|
||||||
@Published var lastUpdatedTimeString: String = ""
|
@Published var lastUpdatedTimeString: String = ""
|
||||||
@Published var debugString: String = "Debug..."
|
@Published var debugString: String = "Debug..."
|
||||||
@Published var chartHoursIndex: Int = 1
|
@Published var chartHoursIndex: Int = 1
|
||||||
|
@Published var requestingDataIconColor: Color = ConstantsAppleWatch.requestingDataIconColorInactive
|
||||||
var lastComplicationUpdateDate: Date = .distantPast
|
|
||||||
|
|
||||||
init(session: WCSession = .default) {
|
init(session: WCSession = .default) {
|
||||||
self.session = session
|
self.session = session
|
||||||
|
@ -200,11 +199,17 @@ class WatchStateModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
/// check when the last follower connection was and compare that to the actual time
|
/// check when the last follower connection was and compare that to the actual time
|
||||||
func getFollowerConnectionNetworkStatus() -> (image: Image, color: Color) {
|
func getFollowerConnectionNetworkStatus() -> (image: Image, color: Color) {
|
||||||
if let timeDifferenceInSeconds = Calendar.current.dateComponents([.second], from: timeStampOfLastFollowerConnection, to: Date()).second, timeDifferenceInSeconds > secondsUntilFollowerDisconnectWarning {
|
if timeStampOfLastFollowerConnection > Date().addingTimeInterval(-Double(secondsUntilFollowerDisconnectWarning)) {
|
||||||
return(Image(systemName: "network.slash"), Color(.red))
|
return(Image(systemName: "network"), Color(.green))
|
||||||
} else {
|
} else {
|
||||||
return(Image(systemName: "network"), Color(.green))
|
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
|
/// check when the last heartbeat connection was and compare that to the actual time
|
||||||
|
@ -226,10 +231,11 @@ class WatchStateModel: NSObject, ObservableObject {
|
||||||
guard session.activationState == .activated else {
|
guard session.activationState == .activated else {
|
||||||
session.activate()
|
session.activate()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// change the text, this must be done in the main thread but only do it if the watch app is reachable
|
// change the text, this must be done in the main thread but only do it if the watch app is reachable
|
||||||
if session.isReachable {
|
if session.isReachable {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.requestingDataIconColor = ConstantsAppleWatch.requestingDataIconColorActive
|
||||||
self.debugString.removeLast(4)
|
self.debugString.removeLast(4)
|
||||||
self.debugString += "Fetching"
|
self.debugString += "Fetching"
|
||||||
}
|
}
|
||||||
|
@ -259,7 +265,7 @@ class WatchStateModel: NSObject, ObservableObject {
|
||||||
sensorAgeInMinutes = watchState.sensorAgeInMinutes ?? 0
|
sensorAgeInMinutes = watchState.sensorAgeInMinutes ?? 0
|
||||||
sensorMaxAgeInMinutes = watchState.sensorMaxAgeInMinutes ?? 0
|
sensorMaxAgeInMinutes = watchState.sensorMaxAgeInMinutes ?? 0
|
||||||
timeStampOfLastFollowerConnection = watchState.timeStampOfLastFollowerConnection ?? .distantPast
|
timeStampOfLastFollowerConnection = watchState.timeStampOfLastFollowerConnection ?? .distantPast
|
||||||
secondsUntilFollowerDisconnectWarning = watchState.secondsUntilFollowerDisconnectWarning ?? 5
|
secondsUntilFollowerDisconnectWarning = watchState.secondsUntilFollowerDisconnectWarning ?? 70// give it some more time compared to the iOS app
|
||||||
lastHeartBeatTimeStamp = watchState.lastHeartBeatTimeStamp ?? .distantPast
|
lastHeartBeatTimeStamp = watchState.lastHeartBeatTimeStamp ?? .distantPast
|
||||||
heartbeatShowDisconnectedTimeInSeconds = watchState.heartbeatShowDisconnectedTimeInSeconds ?? 5
|
heartbeatShowDisconnectedTimeInSeconds = watchState.heartbeatShowDisconnectedTimeInSeconds ?? 5
|
||||||
isMaster = watchState.isMaster ?? true
|
isMaster = watchState.isMaster ?? true
|
||||||
|
@ -267,36 +273,25 @@ class WatchStateModel: NSObject, ObservableObject {
|
||||||
followerBackgroundKeepAliveType = FollowerBackgroundKeepAliveType(rawValue: watchState.followerBackgroundKeepAliveTypeRawValue ?? 0) ?? .normal
|
followerBackgroundKeepAliveType = FollowerBackgroundKeepAliveType(rawValue: watchState.followerBackgroundKeepAliveTypeRawValue ?? 0) ?? .normal
|
||||||
disableComplications = watchState.disableComplications ?? false
|
disableComplications = watchState.disableComplications ?? false
|
||||||
|
|
||||||
debugString = "🩸 WATCH DEBUG"
|
// check if there is any BG data available before updating the data source info strings accordingly
|
||||||
debugString += "\nLast state: \(Date().formatted(date: .omitted, time: .standard))"
|
|
||||||
|
|
||||||
|
|
||||||
// check if there is any BG data available before updating the strings accordingly
|
|
||||||
if let bgReadingDate = bgReadingDate() {
|
if let bgReadingDate = bgReadingDate() {
|
||||||
lastUpdatedTextString = "Last reading "
|
lastUpdatedTextString = "Last reading "
|
||||||
lastUpdatedTimeString = bgReadingDate.formatted(date: .omitted, time: .shortened)
|
lastUpdatedTimeString = bgReadingDate.formatted(date: .omitted, time: .shortened)
|
||||||
debugString += "\nBG updated: \(bgReadingDate.formatted(date: .omitted, time: .standard))"
|
|
||||||
} else {
|
} else {
|
||||||
lastUpdatedTextString = "No sensor data"
|
lastUpdatedTextString = "No sensor data"
|
||||||
lastUpdatedTimeString = ""
|
lastUpdatedTimeString = ""
|
||||||
debugString += "\nBG updated: ---"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugString += "\nBG values: \(bgReadingValues.count)"
|
debugString = generateDebugString()
|
||||||
|
|
||||||
if !isMaster {
|
|
||||||
debugString += "\nServer connection: \(timeStampOfLastFollowerConnection.formatted(date: .omitted, time: .standard))"
|
|
||||||
|
|
||||||
if followerBackgroundKeepAliveType == .heartbeat {
|
|
||||||
debugString += "\nLast hearbeat: \(lastHeartBeatTimeStamp.formatted(date: .omitted, time: .standard))"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugString += "\nScreen width: \(Int(WKInterfaceDevice.current().screenBounds.size.width))"
|
|
||||||
debugString += "\n\(ConstantsHomeView.applicationName): Idle"
|
|
||||||
|
|
||||||
// now process the shared user defaults to get data for the WidgetKit complications
|
// now process the shared user defaults to get data for the WidgetKit complications
|
||||||
updateWatchSharedUserDefaults()
|
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
|
/// once we've process the state update, then save this data to the shared app group so that the complication can read it
|
||||||
|
@ -319,6 +314,34 @@ class WatchStateModel: NSObject, ObservableObject {
|
||||||
// now that the new data is stored in the app group, try to force the complications to reload
|
// now that the new data is stored in the app group, try to force the complications to reload
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
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: \(lastHeartBeatTimeStamp.formatted(date: .omitted, time: .standard))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugString += "\nScreen width: \(Int(WKInterfaceDevice.current().screenBounds.size.width))"
|
||||||
|
debugString += "\niOS app: Idle"
|
||||||
|
|
||||||
|
return debugString
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WatchStateModel: WCSessionDelegate {
|
extension WatchStateModel: WCSessionDelegate {
|
||||||
|
|
|
@ -63,7 +63,7 @@ struct MainView: View {
|
||||||
if showDebug {
|
if showDebug {
|
||||||
Text(watchState.debugString)
|
Text(watchState.debugString)
|
||||||
.foregroundStyle(.black)
|
.foregroundStyle(.black)
|
||||||
.font(.system(size: 15)).bold()
|
.font(.system(size: isSmallScreen ? 12 : 14))
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
|
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
|
||||||
.background(.teal).opacity(0.85)
|
.background(.teal).opacity(0.85)
|
||||||
|
@ -145,6 +145,7 @@ struct ContentView_Previews: PreviewProvider {
|
||||||
watchState.sensorAgeInMinutes = Double(Int.random(in: 1..<14400))
|
watchState.sensorAgeInMinutes = Double(Int.random(in: 1..<14400))
|
||||||
watchState.sensorMaxAgeInMinutes = 14400
|
watchState.sensorMaxAgeInMinutes = 14400
|
||||||
watchState.isMaster = false
|
watchState.isMaster = false
|
||||||
|
watchState.followerDataSourceType = .libreLinkUp
|
||||||
watchState.followerBackgroundKeepAliveType = .heartbeat
|
watchState.followerBackgroundKeepAliveType = .heartbeat
|
||||||
|
|
||||||
return Group {
|
return Group {
|
||||||
|
|
|
@ -47,13 +47,19 @@ struct DataSourceView: View {
|
||||||
|
|
||||||
Spacer()
|
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 {
|
if watchState.sensorAgeInMinutes > 0 {
|
||||||
Text(watchState.sensorAgeInMinutes.minutesToDaysAndHours())
|
Text(watchState.sensorAgeInMinutes.minutesToDaysAndHours())
|
||||||
.font(.system(size: textSize))
|
.font(.system(size: textSize))
|
||||||
.foregroundStyle(watchState.activeSensorProgress().textColor)
|
.foregroundStyle(watchState.activeSensorProgress().textColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding([.leading, .trailing], isSmallScreen ? 6 : 10)
|
.padding([.leading, .trailing], isSmallScreen ? 6 : 8)
|
||||||
} else {
|
} else {
|
||||||
ProgressView(value: 0)
|
ProgressView(value: 0)
|
||||||
.tint(ConstantsHomeView.sensorProgressViewNormalColorSwiftUI)
|
.tint(ConstantsHomeView.sensorProgressViewNormalColorSwiftUI)
|
||||||
|
@ -64,6 +70,11 @@ struct DataSourceView: View {
|
||||||
.font(.system(size: textSize)).bold()
|
.font(.system(size: textSize)).bold()
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: ConstantsAppleWatch.requestingDataIconSFSymbolName)
|
||||||
|
.font(.system(size: ConstantsAppleWatch.requestingDataIconFontSize, weight: .heavy))
|
||||||
|
.foregroundStyle(watchState.requestingDataIconColor)
|
||||||
|
.padding(.bottom, -2)
|
||||||
}
|
}
|
||||||
.padding([.leading, .trailing], 10)
|
.padding([.leading, .trailing], 10)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import SwiftUI
|
||||||
extension XDripWatchComplication.EntryView {
|
extension XDripWatchComplication.EntryView {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var accessoryRectangularView: some View {
|
var accessoryRectangularView: some View {
|
||||||
if !entry.widgetState.disableComplications {
|
ZStack {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
HStack(alignment: .center, spacing: 4) {
|
HStack(alignment: .center, spacing: 4) {
|
||||||
|
@ -35,25 +35,22 @@ extension XDripWatchComplication.EntryView {
|
||||||
}
|
}
|
||||||
.padding(0)
|
.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: nil, overrideChartWidth: nil)
|
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.disableComplications ? ConstantsGlucoseChartSwiftUI.viewHeightWatchAccessoryRectangular - 15 : nil, overrideChartWidth: nil)
|
||||||
}
|
|
||||||
.widgetBackground(backgroundView: Color.clear)
|
|
||||||
} else {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
Text(ConstantsHomeView.applicationName)
|
|
||||||
.fontWeight(/*@START_MENU_TOKEN@*/.bold/*@END_MENU_TOKEN@*/)
|
|
||||||
|
|
||||||
HStack(alignment: .center, spacing: 6) {
|
if entry.widgetState.disableComplications {
|
||||||
Image(systemName: "exclamationmark.triangle")
|
HStack(alignment: .center, spacing: 4) {
|
||||||
.font(.system(size: 24))
|
Image(systemName: "exclamationmark.triangle")
|
||||||
|
.font(.system(size: 14))
|
||||||
Text("Enable background keep-alive")
|
|
||||||
.font(.system(size: 16))
|
Text("Keep-alive disabled")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
}
|
||||||
|
.foregroundStyle(.teal)
|
||||||
|
.padding(0)
|
||||||
}
|
}
|
||||||
.foregroundStyle(.yellow)
|
|
||||||
.padding(2)
|
|
||||||
}
|
}
|
||||||
.widgetBackground(backgroundView: Color.clear)
|
|
||||||
}
|
}
|
||||||
|
.widgetBackground(backgroundView: Color.clear)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,13 +83,11 @@ public final class WatchManager: NSObject, ObservableObject {
|
||||||
self.watchState.highLimitInMgDl = UserDefaults.standard.highMarkValue
|
self.watchState.highLimitInMgDl = UserDefaults.standard.highMarkValue
|
||||||
self.watchState.urgentHighLimitInMgDl = UserDefaults.standard.urgentHighMarkValue
|
self.watchState.urgentHighLimitInMgDl = UserDefaults.standard.urgentHighMarkValue
|
||||||
self.watchState.activeSensorDescription = UserDefaults.standard.activeSensorDescription
|
self.watchState.activeSensorDescription = UserDefaults.standard.activeSensorDescription
|
||||||
self.watchState.timeStampOfLastFollowerConnection = UserDefaults.standard.timeStampOfLastFollowerConnection ?? Date()
|
|
||||||
self.watchState.secondsUntilFollowerDisconnectWarning = UserDefaults.standard.followerDataSourceType.secondsUntilFollowerDisconnectWarning
|
|
||||||
self.watchState.isMaster = UserDefaults.standard.isMaster
|
self.watchState.isMaster = UserDefaults.standard.isMaster
|
||||||
self.watchState.followerDataSourceTypeRawValue = UserDefaults.standard.followerDataSourceType.rawValue
|
self.watchState.followerDataSourceTypeRawValue = UserDefaults.standard.followerDataSourceType.rawValue
|
||||||
self.watchState.followerBackgroundKeepAliveTypeRawValue = UserDefaults.standard.followerBackgroundKeepAliveType.rawValue
|
self.watchState.followerBackgroundKeepAliveTypeRawValue = UserDefaults.standard.followerBackgroundKeepAliveType.rawValue
|
||||||
self.watchState.disableComplications = !UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType == .disabled
|
self.watchState.disableComplications = !UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType == .disabled
|
||||||
|
|
||||||
if let sensorStartDate = UserDefaults.standard.activeSensorStartDate {
|
if let sensorStartDate = UserDefaults.standard.activeSensorStartDate {
|
||||||
self.watchState.sensorAgeInMinutes = Double(Calendar.current.dateComponents([.minute], from: sensorStartDate, to: Date()).minute!)
|
self.watchState.sensorAgeInMinutes = Double(Calendar.current.dateComponents([.minute], from: sensorStartDate, to: Date()).minute!)
|
||||||
} else {
|
} else {
|
||||||
|
@ -104,6 +102,12 @@ public final class WatchManager: NSObject, ObservableObject {
|
||||||
self.watchState.lastHeartBeatTimeStamp = lastHeartBeatTimeStamp
|
self.watchState.lastHeartBeatTimeStamp = lastHeartBeatTimeStamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
self.sendToWatch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue