|
|
@@ -4,7 +4,7 @@ import Swinject
|
|
|
import UIKit
|
|
|
|
|
|
extension LiveActivityAttributes.ContentState {
|
|
|
- static func formatGlucose(_ value: Int, mmol: Bool) -> String {
|
|
|
+ static func formatGlucose(_ value: Int, mmol: Bool, forceSign: Bool) -> String {
|
|
|
let formatter = NumberFormatter()
|
|
|
formatter.numberStyle = .decimal
|
|
|
formatter.maximumFractionDigits = 0
|
|
|
@@ -12,6 +12,9 @@ extension LiveActivityAttributes.ContentState {
|
|
|
formatter.minimumFractionDigits = 1
|
|
|
formatter.maximumFractionDigits = 1
|
|
|
}
|
|
|
+ if forceSign {
|
|
|
+ formatter.positivePrefix = formatter.plusSign
|
|
|
+ }
|
|
|
formatter.roundingMode = .halfUp
|
|
|
|
|
|
return formatter
|
|
|
@@ -25,45 +28,62 @@ extension LiveActivityAttributes.ContentState {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
- let formattedBG = Self.formatGlucose(glucose, mmol: mmol)
|
|
|
+ let formattedBG = Self.formatGlucose(glucose, mmol: mmol, forceSign: false)
|
|
|
|
|
|
- let trentString: String?
|
|
|
+ let trendString: String?
|
|
|
switch bg.direction {
|
|
|
case .doubleUp,
|
|
|
.singleUp,
|
|
|
.tripleUp:
|
|
|
- trentString = "arrow.up"
|
|
|
+ trendString = "arrow.up"
|
|
|
|
|
|
case .fortyFiveUp:
|
|
|
- trentString = "arrow.up.right"
|
|
|
+ trendString = "arrow.up.right"
|
|
|
|
|
|
case .flat:
|
|
|
- trentString = "arrow.right"
|
|
|
+ trendString = "arrow.right"
|
|
|
|
|
|
case .fortyFiveDown:
|
|
|
- trentString = "arrow.down.right"
|
|
|
+ trendString = "arrow.down.right"
|
|
|
|
|
|
case .doubleDown,
|
|
|
.singleDown,
|
|
|
.tripleDown:
|
|
|
- trentString = "arrow.down"
|
|
|
+ trendString = "arrow.down"
|
|
|
|
|
|
case .notComputable,
|
|
|
Optional.none,
|
|
|
.rateOutOfRange,
|
|
|
.some(.none):
|
|
|
- trentString = nil
|
|
|
+ trendString = nil
|
|
|
}
|
|
|
|
|
|
- let change = prev?.glucose.map({ glucose - $0 })
|
|
|
+ let change = prev?.glucose.map({
|
|
|
+ Self.formatGlucose(glucose - $0, mmol: mmol, forceSign: true)
|
|
|
+ }) ?? ""
|
|
|
|
|
|
- self.init(bg: formattedBG, trendSystemImage: trentString, change: change, date: bg.dateString)
|
|
|
+ self.init(bg: formattedBG, trendSystemImage: trendString, change: change, date: bg.dateString)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@available(iOS 16.2, *) private struct ActiveActivity {
|
|
|
let activity: Activity<LiveActivityAttributes>
|
|
|
let startDate: Date
|
|
|
+
|
|
|
+ func needsRecreation() -> Bool {
|
|
|
+ switch activity.activityState {
|
|
|
+ case .dismissed,
|
|
|
+ .ended:
|
|
|
+ return true
|
|
|
+ case .active,
|
|
|
+ .stale: break
|
|
|
+ @unknown default:
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ return -startDate.timeIntervalSinceNow >
|
|
|
+ TimeInterval(60 * 60)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@available(iOS 16.2, *) final class LiveActivityBridge: Injectable {
|
|
|
@@ -87,29 +107,37 @@ extension LiveActivityAttributes.ContentState {
|
|
|
object: nil,
|
|
|
queue: nil
|
|
|
) { _ in
|
|
|
- // just before app resigns active, show a new activity
|
|
|
- // only do this if there is no current activity or the current activity is older than 1h
|
|
|
- if self.settings.useLiveActivity {
|
|
|
- if (self.currentActivity?.startDate).map({ -$0.timeIntervalSinceNow >
|
|
|
- TimeInterval(60 * 60) }) ?? true
|
|
|
- {
|
|
|
- self.forceActivityUpdate()
|
|
|
- }
|
|
|
- } else {
|
|
|
- Task {
|
|
|
- await self.endActivity()
|
|
|
- }
|
|
|
- }
|
|
|
+ self.forceActivityUpdate()
|
|
|
+ }
|
|
|
+
|
|
|
+ Foundation.NotificationCenter.default.addObserver(
|
|
|
+ forName: UIApplication.didBecomeActiveNotification,
|
|
|
+ object: nil,
|
|
|
+ queue: nil
|
|
|
+ ) { _ in
|
|
|
+ self.forceActivityUpdate()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// creates and tries to present a new activity update from the current GlucoseStorage values
|
|
|
+ /// creates and tries to present a new activity update from the current GlucoseStorage values if live activities are enabled in settings
|
|
|
+ /// Ends existing live activities if live activities are not enabled in settings
|
|
|
private func forceActivityUpdate() {
|
|
|
- glucoseDidUpdate(glucoseStorage.recent())
|
|
|
+ // just before app resigns active, show a new activity
|
|
|
+ // only do this if there is no current activity or the current activity is older than 1h
|
|
|
+ if settings.useLiveActivity {
|
|
|
+ if currentActivity?.needsRecreation() ?? true
|
|
|
+ {
|
|
|
+ glucoseDidUpdate(glucoseStorage.recent())
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ Task {
|
|
|
+ await self.endActivity()
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// attempts to present this live activity state, creating a new activity if none exists yet
|
|
|
- private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
|
|
|
+ @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
|
|
|
// hide duplicate/unknown activities
|
|
|
for unknownActivity in Activity<LiveActivityAttributes>.activities
|
|
|
.filter({ self.currentActivity?.activity.id != $0.id })
|
|
|
@@ -120,18 +148,13 @@ extension LiveActivityAttributes.ContentState {
|
|
|
let content = ActivityContent(state: state, staleDate: state.date.addingTimeInterval(TimeInterval(6 * 60)))
|
|
|
|
|
|
if let currentActivity {
|
|
|
- switch currentActivity.activity.activityState {
|
|
|
- case .dismissed,
|
|
|
- .ended:
|
|
|
- // activity is no longer visible. End it and try to push the update again
|
|
|
+ if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
|
|
|
+ // activity is no longer visible or old. End it and try to push the update again
|
|
|
await endActivity()
|
|
|
await pushUpdate(state)
|
|
|
- case .active,
|
|
|
- .stale: await currentActivity.activity.update(content)
|
|
|
- @unknown default:
|
|
|
+ } else {
|
|
|
await currentActivity.activity.update(content)
|
|
|
}
|
|
|
-
|
|
|
} else {
|
|
|
do {
|
|
|
let activity = try Activity.request(
|