watch app improvements, follower mode data source, iOS widget improvements

- also widget data is stored in a new userDefaults variable based upon the app bundle to avoid cross-contamination if various versions are installed on the same device
This commit is contained in:
Paul Plant 2024-03-10 11:35:05 +01:00
parent 48beccc216
commit 3229a0e532
37 changed files with 388 additions and 357 deletions

View File

@ -16,11 +16,13 @@ import WidgetKit
/// also used to update the ComplicationSharedUserDefaultsModel in the app group so that the complication can access the data
class WatchStateModel: NSObject, ObservableObject {
/// shared UserDefaults to publish data
private let sharedUserDefaults = UserDefaults(suiteName: Bundle.main.appGroupSuiteName)
/// the Watch Connectivity session
private var session: WCSession
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] = []
@ -37,14 +39,16 @@ class WatchStateModel: NSObject, ObservableObject {
@Published var sensorAgeInMinutes: Double = 0
@Published var sensorMaxAgeInMinutes: Double = 14400
@Published var showAppleWatchDebug: Bool = false
@Published var followerConnectionIsStale: Bool = false
@Published var timeStampOfLastFollowerConnection: Date = Date()
@Published var secondsUntilFollowerDisconnectWarning: 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 = "Updating..."
@Published var lastUpdatedTimeString: String = "12:34"
@Published var lastUpdatedTextString: String = "Requesting data..."
@Published var lastUpdatedTimeString: String = ""
@Published var debugString: String = "Debug info..."
@Published var chartHoursIndex: Int = 1
@ -142,26 +146,29 @@ class WatchStateModel: NSObject, ObservableObject {
/// 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 {
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"
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 {
return deltaSign + valueAsString
if (deltaChangeInMgDl > -0.1) && (deltaChangeInMgDl < 0.1) {
return "+0.0"
} else {
return deltaSign + valueAsString
}
}
} else {
if (deltaChangeInMgDl > -0.1) && (deltaChangeInMgDl < 0.1) {
return "+0.0"
} else {
return deltaSign + valueAsString
}
return "-"
}
}
@ -202,6 +209,18 @@ class WatchStateModel: NSObject, ObservableObject {
return (networkImage, tintColor)
}
func getFollowerConnectionStatusImage() -> (networkImage: Image, tintColor: Color) {
var networkImage: Image = Image(systemName: "network")
var tintColor: Color = .green
if followerConnectionIsStale {
networkImage = Image(systemName: "network.slash")
tintColor = .gray
}
return (networkImage, tintColor)
}
/// request a state update from the iOS companion app
func requestWatchStateUpdate() {
guard session.activationState == .activated else {
@ -211,8 +230,9 @@ class WatchStateModel: NSObject, ObservableObject {
// change the text, this must be done in the main thread
DispatchQueue.main.async {
self.lastUpdatedTextString = "Waiting for data..."
self.lastUpdatedTimeString = ""
self.debugString += "\nRequesting data..."
// self.lastUpdatedTextString = "Updating..."
// self.lastUpdatedTimeString = ""
}
print("Requesting watch state update from iOS")
@ -243,12 +263,14 @@ class WatchStateModel: NSObject, ObservableObject {
isMaster = watchState.isMaster ?? true
followerDataSourceType = FollowerDataSourceType(rawValue: watchState.followerDataSourceTypeRawValue ?? 0) ?? .nightscout
followerBackgroundKeepAliveType = FollowerBackgroundKeepAliveType(rawValue: watchState.followerBackgroundKeepAliveTypeRawValue ?? 0) ?? .normal
disableComplications = watchState.disableComplications ?? false
followerConnectionIsStale = watchState.followerConnectionIsStale ?? false
// check if there is any BG data available before updating the strings accordingly
if let bgReadingDate = bgReadingDate() {
lastUpdatedTextString = "Last reading "
lastUpdatedTimeString = bgReadingDate.formatted(date: .omitted, time: .shortened)
debugString = "State updated: \(Date().formatted(date: .omitted, time: .shortened))\nBG updated: \(bgReadingDate.formatted(date: .omitted, time: .shortened))\nBG values: \(bgReadingValues.count)"
debugString = "State updated: \(Date().formatted(date: .omitted, time: .standard))\nBG updated: \(bgReadingDate.formatted(date: .omitted, time: .standard))\nBG values: \(bgReadingValues.count)"
} else {
lastUpdatedTextString = "No sensor data"
lastUpdatedTimeString = ""
@ -260,17 +282,19 @@ class WatchStateModel: NSObject, ObservableObject {
}
/// 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 = sharedUserDefaults else { return }
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)
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")
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

View File

@ -11,91 +11,69 @@ import SwiftUI
struct MainView: View {
@EnvironmentObject var watchState: WatchStateModel
// 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: 7, on: .main, in: .common).autoconnect()
// 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 currentDate = Date.now
@State private var hoursToShowIndex: Int = ConstantsAppleWatch.hoursToShowDefaultIndex
// MARK: - Body
var body: some View {
VStack {
HeaderView()
.padding([.leading, .trailing], 5)
.padding([.top], 20)
.padding([.bottom], -10)
.onTapGesture(count: 2) {
watchState.requestWatchStateUpdate()
}
ZStack(alignment: Alignment(horizontal: .center, vertical: .top), content: {
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)
.padding(.top, 3)
.padding(.bottom, 3)
.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
}
}
})
)
if !watchState.isMaster && watchState.followerBackgroundKeepAliveType == .disabled {
VStack(spacing: 2) {
Image(systemName: "exclamationmark.triangle")
.foregroundStyle(.black)
.font(.system(size: 20))
.padding(2)
Text("Watch App not available")
.foregroundStyle(.black)
.font(.footnote).bold()
.multilineTextAlignment(.center)
.padding(2)
Text("Follower keep-alive is disabled")
.foregroundStyle(.black)
.font(.footnote).bold()
.multilineTextAlignment(.center)
.padding(2)
ZStack(alignment: .topLeading) {
VStack {
HeaderView()
.padding([.leading, .trailing], 5)
.padding([.top], 20)
.padding([.bottom], -10)
.onTapGesture(count: 2) {
watchState.requestWatchStateUpdate()
}
.background(.red).opacity(0.9)
.cornerRadius(6)
}
if watchState.showAppleWatchDebug {
Text(watchState.debugString)
.foregroundStyle(.black)
.font(.footnote).bold()
.multilineTextAlignment(.leading)
.padding(EdgeInsets(top: 2, leading: 6, bottom: 2, trailing: 6))
.background(.teal).opacity(0.9)
.cornerRadius(6)
.padding(.top, 10)
.padding(.leading, 2)
}
})
Spacer()
DataSourceView()
InfoView()
ZStack(alignment: Alignment(horizontal: .center, vertical: .top), content: {
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)
.padding(.top, 3)
.padding(.bottom, 3)
.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
}
}
})
)
if watchState.showAppleWatchDebug {
Text(watchState.debugString)
.foregroundStyle(.black)
.font(.footnote).bold()
.multilineTextAlignment(.leading)
.padding(EdgeInsets(top: 2, leading: 6, bottom: 2, trailing: 6))
.background(.teal).opacity(0.8)
.cornerRadius(6)
.padding(.top, 10)
.padding(.leading, 2)
}
})
DataSourceView()
InfoView()
}
.padding(.bottom, 20)
}
.padding(.bottom, 20)
.onReceive(timer) { date in
currentDate = date
watchState.requestWatchStateUpdate()
.frame(maxHeight: .infinity)
.onReceive(watchState.timer) { date in
if watchState.updatedDate.timeIntervalSinceNow < -5 {
watchState.timerControlDate = date
watchState.requestWatchStateUpdate()
}
}
.onAppear {
watchState.requestWatchStateUpdate()
@ -152,7 +130,7 @@ struct ContentView_Previews: PreviewProvider {
watchState.bgReadingDates = bgDateArray()
watchState.isMgDl = true
watchState.slopeOrdinal = 5
watchState.deltaChangeInMgDl = -2
watchState.deltaChangeInMgDl = 0
watchState.urgentLowLimitInMgDl = 60
watchState.lowLimitInMgDl = 80
watchState.highLimitInMgDl = 140
@ -167,6 +145,8 @@ struct ContentView_Previews: PreviewProvider {
return Group {
MainView()
MainView().previewDevice("Apple Watch Series 5 - 40mm")
MainView().previewDevice("Apple Watch Series 3 - 38mm")
}.environmentObject(watchState)
}
}

View File

@ -22,12 +22,17 @@ struct DataSourceView: View {
HStack {
if !watchState.isMaster {
HStack(alignment: .center, spacing: 6) {
watchState.getDataTimeStampOfLastFollowerConnection().networkImage
HStack(alignment: .center, spacing: 4) {
watchState.getFollowerConnectionStatusImage().networkImage
.font(.system(size: 14))
.foregroundStyle(watchState.getDataTimeStampOfLastFollowerConnection().tintColor)
.foregroundStyle(watchState.getFollowerConnectionStatusImage().tintColor)
.padding(.bottom, -2)
watchState.followerBackgroundKeepAliveType.keepAliveImage
.font(.system(size: 14))
.foregroundStyle(Color(white: 0.7))
.padding(.bottom, -3)
Text(watchState.followerDataSourceType.fullDescription)
.font(.system(size: 14)).fontWeight(.semibold)
}

View File

@ -13,7 +13,7 @@ struct HeaderView: View {
@EnvironmentObject var watchState: WatchStateModel
var body: some View {
HStack {
HStack(alignment: .lastTextBaseline) {
Text("\(watchState.bgValueStringInUserChosenUnit())\(watchState.trendArrow())")
.font(.system(size: 60)).fontWeight(.semibold)
.foregroundStyle(watchState.bgTextColor())

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Icon-122.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -8,7 +8,7 @@
import Foundation
/// model of the data we'll store in the shared app group to pass from the watch app to the watch complication widget extension
/// model of the data we'll store in the shared app group to pass from the watch app to widgetkit
struct ComplicationSharedUserDefaultsModel: Codable {
var bgReadingValues: [Double]
var bgReadingDatesAsDouble: [Double]
@ -19,4 +19,5 @@ struct ComplicationSharedUserDefaultsModel: Codable {
var lowLimitInMgDl: Double
var highLimitInMgDl: Double
var urgentHighLimitInMgDl: Double
var disableComplications: Bool
}

View File

@ -4,6 +4,8 @@
<dict>
<key>AppGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
<key>MainAppBundleIdentifier</key>
<string>$(MAIN_APP_BUNDLE_IDENTIFIER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@ -12,26 +12,32 @@ import SwiftUI
extension XDripWatchComplication.EntryView {
@ViewBuilder
var accessoryCircularView: some View {
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)
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)
}
.gaugeStyle(.accessoryCircular)
.tint(entry.widgetState.gaugeModel().gaugeGradient)
.widgetBackground(backgroundView: Color.clear)
}
}

View File

@ -12,31 +12,40 @@ import SwiftUI
extension XDripWatchComplication.EntryView {
@ViewBuilder
var accessoryCornerView: some View {
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)
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
}
.tint(entry.widgetState.gaugeModel().gaugeGradient)
.gaugeStyle(LinearCapacityGaugeStyle()) // Doesn't do anything
}
.widgetBackground(backgroundView: Color.clear)
.widgetBackground(backgroundView: Color.clear)
} else {
Text(" ")
.font(.system(size: 20))
.minimumScaleFactor(0.2)
.widgetCurvesContent()
.widgetLabel("\(ConstantsHomeView.applicationName)")
.widgetBackground(backgroundView: Color.clear)
}
}
}

View File

@ -12,7 +12,11 @@ import SwiftUI
extension XDripWatchComplication.EntryView {
@ViewBuilder
var accessoryInlineView: some View {
Text("\(entry.widgetState.bgValueStringInUserChosenUnit) \(entry.widgetState.trendArrow()) \(entry.widgetState.deltaChangeStringInUserChosenUnit())")
if !entry.widgetState.disableComplications {
Text("\(entry.widgetState.bgValueStringInUserChosenUnit) \(entry.widgetState.trendArrow()) \(entry.widgetState.deltaChangeStringInUserChosenUnit())")
} else {
Text("\(ConstantsHomeView.applicationName)")
}
}
}

View File

@ -12,31 +12,49 @@ import SwiftUI
extension XDripWatchComplication.EntryView {
@ViewBuilder
var accessoryRectangularView: some View {
VStack(spacing: 0) {
HStack(alignment: .center) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow()) ")
.font(.system(size: 24)).bold()
.foregroundStyle(entry.widgetState.bgTextColor())
if !entry.widgetState.disableComplications {
VStack(spacing: 0) {
HStack(alignment: .center) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow()) ")
.font(.system(size: 24)).bold()
.foregroundStyle(entry.widgetState.bgTextColor())
Text(entry.widgetState.deltaChangeStringInUserChosenUnit())
.font(.system(size: 24)).bold()
.foregroundStyle(Color(white: 0.9))
.minimumScaleFactor(0.2)
.lineLimit(1)
}
Text(entry.widgetState.deltaChangeStringInUserChosenUnit())
.font(.system(size: 24)).bold()
.foregroundStyle(Color(white: 0.9))
Spacer()
Text(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")
.font(.system(size: 18))
.foregroundStyle(Color(white: 0.6))
.minimumScaleFactor(0.2)
.lineLimit(1)
}
.padding(0)
Spacer()
Text(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")
.font(.system(size: 18))
.foregroundStyle(Color(white: 0.6))
.minimumScaleFactor(0.2)
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)
}
.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)
.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) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 24))
Text("Enable background keep-alive")
.font(.system(size: 16))
}
.foregroundStyle(.yellow)
.padding(2)
}
.widgetBackground(backgroundView: Color.clear)
}
.widgetBackground(backgroundView: Color.clear)
}
}

View File

@ -32,13 +32,16 @@ extension XDripWatchComplication.Entry {
var lowLimitInMgDl: Double
var highLimitInMgDl: Double
var urgentHighLimitInMgDl: Double
// var isMaster: Bool = true
// var followerBackgroundKeepAliveType: FollowerBackgroundKeepAliveType = .normal
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) {
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
@ -48,11 +51,14 @@ extension XDripWatchComplication.Entry {
self.lowLimitInMgDl = lowLimitInMgDl ?? 80
self.highLimitInMgDl = highLimitInMgDl ?? 180
self.urgentHighLimitInMgDl = urgentHighLimitInMgDl ?? 250
// self.isMaster = isMaster ?? true
// self.followerBackgroundKeepAliveType = followerBackgroundKeepAliveType ?? .normal
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 //(!self.isMaster && self.followerBackgroundKeepAliveType == .disabled) ? true : false
}

View File

@ -24,7 +24,7 @@ extension XDripWatchComplication {
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let entry = Entry(date: .now, widgetState: getWidgetStateFromSharedUserDefaults() ?? sampleWidgetStateFromProvider)
completion(.init(entries: [entry], policy: .atEnd))
completion(.init(entries: [entry], policy: .after(Date().addingTimeInterval(30*60))))
}
}
}
@ -34,10 +34,9 @@ extension XDripWatchComplication {
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") else {
guard let encodedLatestReadings = sharedUserDefaults.data(forKey: "complicationSharedUserDefaults.\(Bundle.main.mainAppBundleIdentifier)") else {
return nil
}
@ -52,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)
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)
}
@ -61,7 +60,6 @@ extension XDripWatchComplication.Provider {
}
private var sampleWidgetStateFromProvider: XDripWatchComplication.Entry.WidgetState {
//var widgetState = Entry.WidgetState()
func bgDateArray() -> [Date] {
let endDate = Date()
@ -100,8 +98,6 @@ extension XDripWatchComplication.Provider {
return bgValueArray
}
var widgetState = Entry.WidgetState(bgReadingValues: bgValueArray(), bgReadingDates: bgDateArray(), isMgDl: true, slopeOrdinal: 3, deltaChangeInMgDl: 0, urgentLowLimitInMgDl: ConstantsBGGraphBuilder.defaultUrgentLowMarkInMgdl, lowLimitInMgDl: ConstantsBGGraphBuilder.defaultLowMarkInMgdl, highLimitInMgDl: ConstantsBGGraphBuilder.defaultHighMarkInMgdl, urgentHighLimitInMgDl: ConstantsBGGraphBuilder.defaultUrgentHighMarkInMgdl)
return widgetState
return Entry.WidgetState(bgReadingValues: bgValueArray(), bgReadingDates: bgDateArray(), isMgDl: true, slopeOrdinal: 3, deltaChangeInMgDl: 0, urgentLowLimitInMgDl: ConstantsBGGraphBuilder.defaultUrgentLowMarkInMgdl, lowLimitInMgDl: ConstantsBGGraphBuilder.defaultLowMarkInMgdl, highLimitInMgDl: ConstantsBGGraphBuilder.defaultHighMarkInMgdl, urgentHighLimitInMgDl: ConstantsBGGraphBuilder.defaultUrgentHighMarkInMgdl)
}
}

View File

@ -26,7 +26,6 @@ struct XDripWidgetAttributes: ActivityAttributes {
var urgentHighLimitInMgDl: Double
var eventStartDate: Date = Date()
var warnUserToOpenApp: Bool = true
var showClockAtNight: Bool = false
var liveActivitySize: LiveActivitySize
// computed properties
@ -35,7 +34,7 @@ struct XDripWidgetAttributes: ActivityAttributes {
var bgReadingDate: Date?
var bgValueStringInUserChosenUnit: String
init(bgReadingValues: [Double], bgReadingDates: [Date], isMgDl: Bool, slopeOrdinal: Int, deltaChangeInMgDl: Double?, urgentLowLimitInMgDl: Double, lowLimitInMgDl: Double, highLimitInMgDl: Double, urgentHighLimitInMgDl: Double, showClockAtNight: Bool, liveActivitySize: LiveActivitySize) {
init(bgReadingValues: [Double], bgReadingDates: [Date], isMgDl: Bool, slopeOrdinal: Int, deltaChangeInMgDl: Double?, urgentLowLimitInMgDl: Double, lowLimitInMgDl: Double, highLimitInMgDl: Double, urgentHighLimitInMgDl: Double, liveActivitySize: LiveActivitySize) {
// these are the "passed in" stateful values used to initialize
self.bgReadingValues = bgReadingValues
@ -46,12 +45,8 @@ struct XDripWidgetAttributes: ActivityAttributes {
self.urgentLowLimitInMgDl = urgentLowLimitInMgDl
self.lowLimitInMgDl = lowLimitInMgDl
self.highLimitInMgDl = highLimitInMgDl
self.urgentHighLimitInMgDl = urgentHighLimitInMgDl
let hour = Calendar.current.component(.hour, from: Date())
self.showClockAtNight = (showClockAtNight && (hour >= ConstantsLiveActivity.showClockAtNightFromHour || hour < ConstantsLiveActivity.showClockAtNightToHour)) ? true : false
self.liveActivitySize = self.showClockAtNight ? .large : liveActivitySize
self.urgentHighLimitInMgDl = urgentHighLimitInMgDl
self.liveActivitySize = liveActivitySize
self.bgUnitString = isMgDl ? Texts_Common.mgdl : Texts_Common.mmol

View File

@ -4,6 +4,8 @@
<dict>
<key>AppGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
<key>MainAppBundleIdentifier</key>
<string>$(MAIN_APP_BUNDLE_IDENTIFIER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@ -15,18 +15,11 @@ extension XDripWidget.EntryView {
ZStack {
AccessoryWidgetBackground()
.cornerRadius(8)
VStack(spacing: 0) {
Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow())")
.font(.largeTitle).fontWeight(.semibold)
.foregroundStyle(Color(white: 1))
.lineLimit(1)
Text("Last reading \(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")")
.font(.system(size: 12))
.foregroundStyle(Color(white: 0.6))
}
.padding(8)
Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow())")
.font(.system(size: 50)).fontWeight(.semibold)
.minimumScaleFactor(0.2)
.foregroundStyle(Color(white: 1))
.lineLimit(1)
}
.widgetBackground(backgroundView: Color.black)
}

View File

@ -22,10 +22,10 @@ extension XDripWidget.EntryView {
Spacer()
HStack(alignment: .firstTextBaseline, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(entry.widgetState.deltaChangeStringInUserChosenUnit())
.font(.title).fontWeight(.bold)
.foregroundStyle(Color(white: 0.9))
.foregroundStyle(entry.widgetState.deltaChangeTextColor())
.lineLimit(1)
Text(entry.widgetState.bgUnitString)
.font(.title)

View File

@ -22,10 +22,10 @@ extension XDripWidget.EntryView {
Spacer()
HStack(alignment: .firstTextBaseline, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(entry.widgetState.deltaChangeStringInUserChosenUnit())
.font(.title2).fontWeight(.bold)
.foregroundStyle(Color(white: 0.9))
.foregroundStyle(entry.widgetState.deltaChangeTextColor())
.lineLimit(1)
Text(entry.widgetState.bgUnitString)
.font(.title2)

View File

@ -12,7 +12,7 @@ import SwiftUI
extension XDripWidget.EntryView {
var systemSmallView: some View {
VStack(spacing: 0) {
HStack {
HStack(alignment: .center) {
Text("\(entry.widgetState.bgValueStringInUserChosenUnit)\(entry.widgetState.trendArrow())")
.font(.title).fontWeight(.semibold)
.foregroundStyle(entry.widgetState.bgTextColor())
@ -22,29 +22,21 @@ extension XDripWidget.EntryView {
Spacer()
VStack(alignment: .trailing, spacing: 0) {
Text(entry.widgetState.deltaChangeStringInUserChosenUnit())
.font(.headline).fontWeight(.bold)
.foregroundStyle(Color(white: 0.9))
.lineLimit(1)
.padding(.bottom, -3)
Text(entry.widgetState.bgUnitString)
.font(.footnote)
.foregroundStyle(.gray)
.lineLimit(1)
}
Text(entry.widgetState.deltaChangeStringInUserChosenUnit())
.font(.title).fontWeight(.semibold)
.foregroundStyle(entry.widgetState.deltaChangeTextColor())
.minimumScaleFactor(0.5)
.lineLimit(1)
}
.padding(.top, -6)
.padding(.bottom, 6)
GlucoseChartView(glucoseChartType: .widgetSystemSmall, 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)
HStack {
Spacer()
Text("Last reading \(entry.widgetState.bgReadingDate?.formatted(date: .omitted, time: .shortened) ?? "--:--")")
.font(.caption).bold()
.minimumScaleFactor(0.2)
.font(.system(size: 10)).bold()
.foregroundStyle(Color(white: 0.6))
}
}

View File

@ -72,6 +72,16 @@ extension XDripWidget.Entry {
}
}
/// 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)
}
}
/// 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) {

View File

@ -37,7 +37,7 @@ extension XDripWidget.Provider {
guard let sharedUserDefaults = UserDefaults(suiteName: Bundle.main.appGroupSuiteName) else {return nil}
guard let encodedLatestReadings = sharedUserDefaults.data(forKey: "widgetSharedUserDefaults") else {
guard let encodedLatestReadings = sharedUserDefaults.data(forKey: "widgetSharedUserDefaults.\(Bundle.main.mainAppBundleIdentifier)") else {
return nil
}

View File

@ -112,51 +112,31 @@ struct XDripWidgetLiveActivity: Widget {
// 3 = large chart is final default option
ZStack {
VStack {
if context.state.showClockAtNight {
HStack(alignment: .firstTextBaseline) {
Text(Date().formatted(date: .omitted, time: .shortened))
.font(.system(size: 50)).bold()
.foregroundStyle(Color(white: 0.7))
.minimumScaleFactor(0.2)
Spacer()
Text("\(context.state.bgValueStringInUserChosenUnit)\(context.state.trendArrow()) ")
.font(.system(size: 50)).bold()
.foregroundStyle(context.state.bgTextColor())
}
.padding(.top, 4)
.padding(.bottom, -14)
.padding(.leading, 6)
.padding(.trailing, 6)
} else {
HStack(alignment: .center) {
Text("\(context.state.bgValueStringInUserChosenUnit)\(context.state.trendArrow()) ")
.font(.largeTitle).bold()
.foregroundStyle(context.state.bgTextColor())
Spacer()
HStack(alignment: .center, spacing: 4) {
Text(context.state.deltaChangeStringInUserChosenUnit())
.font(.title2).bold()
.foregroundStyle(Color(white: 0.9))
.minimumScaleFactor(0.2)
.lineLimit(1)
Text(context.state.bgUnitString)
.font(.title2)
.foregroundStyle(Color(white: 0.5))
.minimumScaleFactor(0.2)
.lineLimit(1)
}
}
.padding(.top, 4)
.padding(.bottom, -8)
.padding(.leading, 10)
.padding(.trailing, 10)
HStack(alignment: .center) {
Text("\(context.state.bgValueStringInUserChosenUnit)\(context.state.trendArrow()) ")
.font(.largeTitle).bold()
.foregroundStyle(context.state.bgTextColor())
Spacer()
HStack(alignment: .center, spacing: 4) {
Text(context.state.deltaChangeStringInUserChosenUnit())
.font(.title2).bold()
.foregroundStyle(Color(white: 0.9))
.minimumScaleFactor(0.2)
.lineLimit(1)
Text(context.state.bgUnitString)
.font(.title2)
.foregroundStyle(Color(white: 0.5))
.minimumScaleFactor(0.2)
.lineLimit(1)
}
}
.padding(.top, 4)
.padding(.bottom, -8)
.padding(.leading, 10)
.padding(.trailing, 10)
GlucoseChartView(glucoseChartType: .liveActivity, bgReadingValues: context.state.bgReadingValues, bgReadingDates: context.state.bgReadingDates, isMgDl: context.state.isMgDl, urgentLowLimitInMgDl: context.state.urgentLowLimitInMgDl, lowLimitInMgDl: context.state.lowLimitInMgDl, highLimitInMgDl: context.state.highLimitInMgDl, urgentHighLimitInMgDl: context.state.urgentHighLimitInMgDl, liveActivitySize: .large, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil)
}
@ -267,7 +247,7 @@ struct XDripWidgetLiveActivity_Previews: PreviewProvider {
static let attributes = XDripWidgetAttributes()
static let contentState = XDripWidgetAttributes.ContentState(bgReadingValues: bgValueArray(), bgReadingDates: bgDateArray(), isMgDl: true, slopeOrdinal: 5, deltaChangeInMgDl: -2, urgentLowLimitInMgDl: 70, lowLimitInMgDl: 80, highLimitInMgDl: 140, urgentHighLimitInMgDl: 180, showClockAtNight: false, liveActivitySize: .minimal)
static let contentState = XDripWidgetAttributes.ContentState(bgReadingValues: bgValueArray(), bgReadingDates: bgDateArray(), isMgDl: true, slopeOrdinal: 5, deltaChangeInMgDl: -2, urgentLowLimitInMgDl: 70, lowLimitInMgDl: 80, highLimitInMgDl: 140, urgentHighLimitInMgDl: 180, liveActivitySize: .minimal)
static var previews: some View {
attributes

View File

@ -4,5 +4,7 @@
<dict>
<key>AppGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
<key>MainAppBundleIdentifier</key>
<string>$(MAIN_APP_BUNDLE_IDENTIFIER)</string>
</dict>
</plist>

View File

@ -151,6 +151,7 @@
47DE41B32B8672F90041DA19 /* DataSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DE41B22B8672F90041DA19 /* DataSourceView.swift */; };
47DE41B52B8693CB0041DA19 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DE41B42B8693CB0041DA19 /* HeaderView.swift */; };
47DE41B92B87B2680041DA19 /* ConstantsAppleWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DE41B82B87B2680041DA19 /* ConstantsAppleWatch.swift */; };
47E91BBA2B9A43F20063181B /* FollowerBackgroundKeepAliveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B7FC712B00CF4B004C872B /* FollowerBackgroundKeepAliveType.swift */; };
47FB28082636B04200042FFB /* StatisticsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FB28072636B04200042FFB /* StatisticsManager.swift */; };
CE1B2FE025D0264B00F642F5 /* LaunchScreen.strings in Resources */ = {isa = PBXBuildFile; fileRef = CE1B2FD125D0264900F642F5 /* LaunchScreen.strings */; };
CE1B2FE125D0264B00F642F5 /* Main.strings in Resources */ = {isa = PBXBuildFile; fileRef = CE1B2FD425D0264900F642F5 /* Main.strings */; };
@ -3937,6 +3938,7 @@
471C9BFF2B932952005E1326 /* LibreLinkUpModels.swift in Sources */,
478A923E2B8B64DE0084C394 /* ConstantsHomeView.swift in Sources */,
4796C6072B9516FD00DE2210 /* Bundle.swift in Sources */,
47E91BBA2B9A43F20063181B /* FollowerBackgroundKeepAliveType.swift in Sources */,
478A92582B8FA1F20084C394 /* Date.swift in Sources */,
478A92552B8FA1D80084C394 /* ConstantsBGGraphBuilder.swift in Sources */,
478A925F2B8FB5290084C394 /* XDripWatchComplication+Entry.swift in Sources */,
@ -5010,7 +5012,7 @@
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "xDrip Widget/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = xDrip4iO5;
INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Johan Degraeve. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
@ -5046,7 +5048,7 @@
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "xDrip Widget/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = xDrip4iO5;
INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Johan Degraeve. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
@ -5080,7 +5082,7 @@
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "xDrip Watch Complication/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = xDrip4iO5;
INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Johan Degraeve. All rights reserved.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -5117,7 +5119,7 @@
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "xDrip Watch Complication/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = xDrip4iO5;
INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Johan Degraeve. All rights reserved.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -5156,7 +5158,7 @@
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "xDrip-Watch-App-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = xDrip4iO5;
INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)";
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(MAIN_APP_BUNDLE_IDENTIFIER)";
INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
@ -5197,7 +5199,7 @@
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "xDrip-Watch-App-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = xDrip4iO5;
INFOPLIST_KEY_CFBundleDisplayName = "$(MAIN_APP_DISPLAY_NAME)";
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(MAIN_APP_BUNDLE_IDENTIFIER)";
INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;

View File

@ -9,18 +9,10 @@
import Foundation
enum ConstantsLiveActivity {
// warn that live activity will soon end (in minutes)
static let warnLiveActivityAfterMinutes: Double = 7.25 * 60 * 60
// end live activity after time in (minutes) we give a bit of margin
// in case there is a missed reading (and therefore no update cycle) towards the end
static let endLiveActivityAfterMinutes: Double = 7.75 * 60 * 60
// what time should the automatic standBy live activity view be started from?
static let showClockAtNightFromHour: Int = 23
// what time should the automatic standBy live activity view end at?
static let showClockAtNightToHour: Int = 8
}

View File

@ -4,4 +4,8 @@ extension Bundle {
var appGroupSuiteName: String {
return object(forInfoDictionaryKey: "AppGroupIdentifier") as! String
}
var mainAppBundleIdentifier: String {
return object(forInfoDictionaryKey: "MainAppBundleIdentifier") as! String
}
}

View File

@ -75,8 +75,6 @@ extension UserDefaults {
case liveActivityType = "liveActivityType"
/// which size should the live activities be shown?
case liveActivitySize = "liveActivitySize"
/// should the live activity be automatically configured for stand-by mode at night?
case liveActivityShowClockAtNight = "liveActivityShowClockAtNight"
// Home Screen and main chart settings
@ -652,20 +650,6 @@ extension UserDefaults {
}
}
/// should the live activity be configured for the best stand-by mode during night hours?
/// if true (and if the live activity is started/updated after 22hrs and before 08hrs), the big chart view with a clock will be shown
/// if false (and if the live activity is started/updated before 22hrs or after 08hrs) then just show the normal live activity type the user has selected
@objc dynamic var liveActivityShowClockAtNight: Bool {
// default value for bool in userdefaults is false, as default we want this to be disabled
get {
return !bool(forKey: Key.liveActivityShowClockAtNight.rawValue)
}
set {
set(!newValue, forKey: Key.liveActivityShowClockAtNight.rawValue)
}
}
// MARK: Home Screen Settings
/// the amount of hours to show in the mini-chart. Usually 24 hours but can be set to 48 hours by the user

View File

@ -8,6 +8,7 @@
import Foundation
import UIKit
import SwiftUI
/// types of background keep-alive
public enum FollowerBackgroundKeepAliveType: Int, CaseIterable {
@ -42,7 +43,8 @@ public enum FollowerBackgroundKeepAliveType: Int, CaseIterable {
}
}
var keepAliveImage: UIImage {
// return the keep-alive image for UIKit views
var keepAliveUIImage: UIImage {
switch self {
case .disabled:
return UIImage(systemName: "d.circle") ?? UIImage()
@ -53,4 +55,16 @@ public enum FollowerBackgroundKeepAliveType: Int, CaseIterable {
}
}
// return the keep-alive image for SwiftUI views
var keepAliveImage: Image {
switch self {
case .disabled:
return Image(systemName: "d.circle")
case .normal:
return Image(systemName: "n.circle")
case .aggressive:
return Image(systemName: "a.circle")
}
}
}

View File

@ -163,9 +163,9 @@ class NightScoutFollowManager: NSObject {
trace(" last reading is less than 30 seconds old, will not download now", log: self.log, category: ConstantsLog.categoryNightScoutFollowManager, type: .info)
// schedule new download, only if followerBackgroundKeepAliveType != disabled
if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled {
//if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled {
self.scheduleNewDownload()
}
//}
return
}
@ -205,9 +205,9 @@ class NightScoutFollowManager: NSObject {
}
// schedule new download, only if followerBackgroundKeepAliveType != disabled
if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled {
//if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled {
self.scheduleNewDownload()
}
//}
}
@ -417,14 +417,14 @@ class NightScoutFollowManager: NSObject {
if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled {
enableSuspensionPrevention()
// do initial download, this will also schedule future downloads
download()
} else {
disableSuspensionPrevention()
}
// do initial download, this will also schedule future downloads
download()
} else {
// disable the suspension prevention

View File

@ -8,6 +8,7 @@
import Foundation
import WatchConnectivity
import WidgetKit
public final class WatchManager: NSObject, ObservableObject {
@ -16,6 +17,9 @@ public final class WatchManager: NSObject, ObservableObject {
/// a watch connectivity session instance
private let session: WCSession
// dispatch queue for async processing operations
private let processQueue = DispatchQueue(label: "WatchManager.processQueue")
/// a BgReadingsAccessor instance
private var bgReadingsAccessor: BgReadingsAccessor
@ -46,20 +50,8 @@ public final class WatchManager: NSObject, ObservableObject {
}
private func processWatchState() {
// check if the watch is connected and active before doing anything
guard session.isReachable else { return }
DispatchQueue.main.async {
// get 2 last Readings, with a calculatedValue
//let lastReading = self.bgReadingsAccessor.get2LatestBgReadings(minimumTimeIntervalInMinutes: 0)
// there should be at least one reading
// guard lastReading.count > 0 else {
// print("exiting processWatchState(), no recent BG readings returned")
// return
// }
// 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
@ -96,16 +88,25 @@ public final class WatchManager: NSObject, ObservableObject {
self.watchState.showAppleWatchDebug = UserDefaults.standard.showAppleWatchDebug
self.watchState.activeSensorDescription = UserDefaults.standard.activeSensorDescription
self.watchState.timeStampOfLastFollowerConnection = UserDefaults.standard.timeStampOfLastFollowerConnection ?? Date()
self.watchState.secondsUntilFollowerDisconnectWarning = UserDefaults.standard.followerDataSourceType.secondsUntilFollowerDisconnectWarning ?? 90
self.watchState.secondsUntilFollowerDisconnectWarning = UserDefaults.standard.followerDataSourceType.secondsUntilFollowerDisconnectWarning
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
// check when the last follower connection was and compare that to the actual time
if let timeStampOfLastFollowerConnection = UserDefaults.standard.timeStampOfLastFollowerConnection, Calendar.current.dateComponents([.second], from: timeStampOfLastFollowerConnection, to: Date()).second! >= UserDefaults.standard.followerDataSourceType.secondsUntilFollowerDisconnectWarning {
self.watchState.followerConnectionIsStale = true
} else {
self.watchState.followerConnectionIsStale = false
}
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
self.sendToWatch()
@ -113,12 +114,14 @@ public final class WatchManager: NSObject, ObservableObject {
}
private func sendToWatch() {
private func sendToWatch() {
guard let data = try? JSONEncoder().encode(watchState) else {
print("Watch state JSON encoding error")
return
}
guard session.isReachable else { return }
session.sendMessageData(data, replyHandler: nil) { error in
print("Cannot send data message to watch")
}
@ -148,8 +151,8 @@ extension WatchManager: WCSessionDelegate {
// 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.processWatchState()
processQueue.async {
self.sendToWatch()
}
}
}
@ -158,8 +161,8 @@ extension WatchManager: WCSessionDelegate {
public func sessionReachabilityDidChange(_ session: WCSession) {
if session.isReachable {
DispatchQueue.main.async {
self.processWatchState()
processQueue.async {
self.sendToWatch()
}
}
}

View File

@ -26,6 +26,8 @@ struct WatchState: Codable {
var isMaster: Bool?
var followerDataSourceTypeRawValue: Int?
var followerBackgroundKeepAliveTypeRawValue: Int?
var followerConnectionIsStale: Bool?
var timeStampOfLastFollowerConnection: Date?
var secondsUntilFollowerDisconnectWarning: Int?
var disableComplications: Bool?
}

View File

@ -8,7 +8,7 @@
import Foundation
/// model of the data we'll store in the shared app group to pass from the watch app to the watch complication widget extension
/// model of the data we'll store in the shared app group to pass from the watch app to widgetkit
struct WidgetSharedUserDefaultsModel: Codable {
var bgReadingValues: [Double]
var bgReadingDatesAsDouble: [Double]

View File

@ -35,6 +35,8 @@
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MainAppBundleIdentifier</key>
<string>$(MAIN_APP_BUNDLE_IDENTIFIER)</string>
<key>NFCReaderUsageDescription</key>
<string>xDrip4iO5 uses NFC to scan Libre sensors.</string>
<key>NSAppTransportSecurity</key>
@ -51,6 +53,8 @@
<string>Store bloodglucose readings</string>
<key>NSHealthUpdateUsageDescription</key>
<string>Store bloodglucose readings</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
@ -96,7 +100,5 @@
<string>Light</string>
<key>view controller-based status bar</key>
<false/>
<key>NSSupportsLiveActivities</key>
<true/>
</dict>
</plist>

View File

@ -93,10 +93,6 @@ class Texts_SettingsView {
return NSLocalizedString("settingsviews_liveActivitySizeLarge", tableName: filename, bundle: Bundle.main, value: "Large", comment: "notification settings, live activity size large")
}()
static let liveActivityShowClockAtNight: String = {
return NSLocalizedString("settingsviews_liveActivityShowClockAtNight", tableName: filename, bundle: Bundle.main, value: "Automatically configure for StandBy", comment: "notification settings, live activities will be automatically switch for standby mode at night")
}()
// MARK: - Section Data Source

View File

@ -3223,7 +3223,7 @@ final class RootViewController: UIViewController, ObservableObject {
setFollowerConnectionStatus()
// set the keep-alive image, then make it visible if in follower mode
dataSourceKeepAliveImageOutlet.image = UserDefaults.standard.followerBackgroundKeepAliveType.keepAliveImage
dataSourceKeepAliveImageOutlet.image = UserDefaults.standard.followerBackgroundKeepAliveType.keepAliveUIImage
dataSourceKeepAliveImageOutlet.isHidden = isMaster
// let's go through the specific cases for follower modes
@ -3591,7 +3591,7 @@ final class RootViewController: UIViewController, ObservableObject {
if UserDefaults.standard.isMaster && showLiveActivity {
// create the contentState that will update the dynamic attributes of the Live Activity Widget
let contentState = XDripWidgetAttributes.ContentState( bgReadingValues: bgReadingValues, bgReadingDates: bgReadingDates, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, slopeOrdinal: slopeOrdinal, deltaChangeInMgDl: deltaChangeInMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, showClockAtNight: UserDefaults.standard.liveActivityShowClockAtNight, liveActivitySize: UserDefaults.standard.liveActivitySize)
let contentState = XDripWidgetAttributes.ContentState( bgReadingValues: bgReadingValues, bgReadingDates: bgReadingDates, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, slopeOrdinal: slopeOrdinal, deltaChangeInMgDl: deltaChangeInMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, liveActivitySize: UserDefaults.standard.liveActivitySize)
LiveActivityManager.shared.runActivity(contentState: contentState, forceRestart: forceRestart)
@ -3603,9 +3603,11 @@ final class RootViewController: UIViewController, ObservableObject {
}
let widgetSharedUserDefaultsModel = WidgetSharedUserDefaultsModel(bgReadingValues: bgReadingValues, bgReadingDatesAsDouble: bgReadingDatesAsDouble, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, slopeOrdinal: slopeOrdinal, deltaChangeInMgDl: deltaChangeInMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue)
// 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 data
if let widgetData = try? JSONEncoder().encode(widgetSharedUserDefaultsModel) {
UserDefaults.storeInSharedUserDefaults(value: widgetData, forKey: "widgetSharedUserDefaults")
UserDefaults.storeInSharedUserDefaults(value: widgetData, forKey: "widgetSharedUserDefaults.\(Bundle.main.mainAppBundleIdentifier)")
}
WidgetCenter.shared.reloadAllTimelines()

View File

@ -17,14 +17,11 @@ fileprivate enum Setting:Int, CaseIterable {
/// live activity size
case liveActivitySize = 3
/// live activity will be automatically configured for night/stand-by use?
case liveActivityShowClockAtNight = 4
/// show reading in app badge
case showReadingInAppBadge = 5
case showReadingInAppBadge = 4
/// if reading is shown in app badge, should value be multiplied with 10 yes or no
case multipleAppBadgeValueWith10 = 6
case multipleAppBadgeValueWith10 = 5
}
@ -56,7 +53,7 @@ class SettingsViewNotificationsSettingsViewModel: SettingsViewModelProtocol {
switch setting {
case .showReadingInNotification, .showReadingInAppBadge, .multipleAppBadgeValueWith10, .liveActivityShowClockAtNight:
case .showReadingInNotification, .showReadingInAppBadge, .multipleAppBadgeValueWith10:
return .nothing
case .notificationInterval:
@ -207,9 +204,6 @@ class SettingsViewNotificationsSettingsViewModel: SettingsViewModelProtocol {
case .liveActivitySize:
return Texts_SettingsView.labelliveActivitySize
case .liveActivityShowClockAtNight:
return Texts_SettingsView.liveActivityShowClockAtNight
case .showReadingInAppBadge:
return Texts_SettingsView.labelShowReadingInAppBadge
@ -224,7 +218,7 @@ class SettingsViewNotificationsSettingsViewModel: SettingsViewModelProtocol {
switch setting {
case .showReadingInNotification, .showReadingInAppBadge, .multipleAppBadgeValueWith10, .liveActivityShowClockAtNight:
case .showReadingInNotification, .showReadingInAppBadge, .multipleAppBadgeValueWith10:
return .none
case .notificationInterval:
@ -264,13 +258,6 @@ class SettingsViewNotificationsSettingsViewModel: SettingsViewModelProtocol {
} else {
return "iOS 16.2 needed"
}
case .liveActivityShowClockAtNight:
if #available(iOS 16.2, *) {
return UserDefaults.standard.isMaster ? nil : Texts_SettingsView.liveActivityDisabledInFollowerMode
} else {
return "iOS 16.2 needed"
}
}
}
@ -290,9 +277,6 @@ class SettingsViewNotificationsSettingsViewModel: SettingsViewModelProtocol {
case .notificationInterval, .liveActivityType, .liveActivitySize:
return nil
case .liveActivityShowClockAtNight:
return UISwitch(isOn: UserDefaults.standard.liveActivityShowClockAtNight, action: {(isOn:Bool) in UserDefaults.standard.liveActivityShowClockAtNight = isOn})
}
}
}