LiveActivityBridge.swift 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import ActivityKit
  2. import Foundation
  3. import Swinject
  4. import UIKit
  5. extension LiveActivityAttributes.ContentState {
  6. static func formatGlucose(_ value: Int, mmol: Bool) -> String {
  7. let formatter = NumberFormatter()
  8. formatter.numberStyle = .decimal
  9. formatter.maximumFractionDigits = 0
  10. if mmol {
  11. formatter.minimumFractionDigits = 1
  12. formatter.maximumFractionDigits = 1
  13. }
  14. formatter.roundingMode = .halfUp
  15. return formatter
  16. .string(from: mmol ? value.asMmolL as NSNumber : NSNumber(value: value))!
  17. }
  18. init?(new bg: BloodGlucose, prev: BloodGlucose?, mmol: Bool) {
  19. guard let glucose = bg.glucose,
  20. bg.dateString.timeIntervalSinceNow > -TimeInterval(minutes: 6)
  21. else {
  22. return nil
  23. }
  24. let formattedBG = Self.formatGlucose(glucose, mmol: mmol)
  25. let trentString: String?
  26. switch bg.direction {
  27. case .doubleUp,
  28. .singleUp,
  29. .tripleUp:
  30. trentString = "arrow.up"
  31. case .fortyFiveUp:
  32. trentString = "arrow.up.right"
  33. case .flat:
  34. trentString = "arrow.right"
  35. case .fortyFiveDown:
  36. trentString = "arrow.down.right"
  37. case .doubleDown,
  38. .singleDown,
  39. .tripleDown:
  40. trentString = "arrow.down"
  41. case .notComputable,
  42. Optional.none,
  43. .rateOutOfRange,
  44. .some(.none):
  45. trentString = nil
  46. }
  47. let change = prev?.glucose.map({ glucose - $0 })
  48. self.init(bg: formattedBG, trendSystemImage: trentString, change: change, date: bg.dateString)
  49. }
  50. }
  51. @available(iOS 16.2, *) private struct ActiveActivity {
  52. let activity: Activity<LiveActivityAttributes>
  53. let startDate: Date
  54. }
  55. @available(iOS 16.2, *) final class LiveActivityBridge: Injectable {
  56. @Injected() private var settingsManager: SettingsManager!
  57. @Injected() private var glucoseStorage: GlucoseStorage!
  58. @Injected() private var broadcaster: Broadcaster!
  59. private var settings: FreeAPSSettings {
  60. settingsManager.settings
  61. }
  62. private var currentActivity: ActiveActivity?
  63. private var latestGlucose: BloodGlucose?
  64. init(resolver: Resolver) {
  65. injectServices(resolver)
  66. broadcaster.register(GlucoseObserver.self, observer: self)
  67. Foundation.NotificationCenter.default.addObserver(
  68. forName: UIApplication.didEnterBackgroundNotification,
  69. object: nil,
  70. queue: nil
  71. ) { _ in
  72. // just before app resigns active, show a new activity
  73. // only do this if there is no current activity or the current activity is older than 1h
  74. if self.settings.useLiveActivity {
  75. if (self.currentActivity?.startDate).map({ -$0.timeIntervalSinceNow >
  76. TimeInterval(60 * 60) }) ?? true
  77. {
  78. self.forceActivityUpdate()
  79. }
  80. } else {
  81. Task {
  82. await self.endActivity()
  83. }
  84. }
  85. }
  86. }
  87. /// creates and tries to present a new activity update from the current GlucoseStorage values
  88. private func forceActivityUpdate() {
  89. glucoseDidUpdate(glucoseStorage.recent())
  90. }
  91. /// attempts to present this live activity state, creating a new activity if none exists yet
  92. private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
  93. // hide duplicate/unknown activities
  94. for unknownActivity in Activity<LiveActivityAttributes>.activities
  95. .filter({ self.currentActivity?.activity.id != $0.id })
  96. {
  97. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  98. }
  99. let content = ActivityContent(state: state, staleDate: state.date.addingTimeInterval(TimeInterval(6 * 60)))
  100. if let currentActivity {
  101. switch currentActivity.activity.activityState {
  102. case .dismissed,
  103. .ended:
  104. // activity is no longer visible. End it and try to push the update again
  105. await endActivity()
  106. await pushUpdate(state)
  107. case .active,
  108. .stale: await currentActivity.activity.update(content)
  109. @unknown default:
  110. await currentActivity.activity.update(content)
  111. }
  112. } else {
  113. do {
  114. let activity = try Activity.request(
  115. attributes: LiveActivityAttributes(startDate: Date.now),
  116. content: content,
  117. pushType: nil
  118. )
  119. currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
  120. } catch {
  121. print("activity creation error: \(error)")
  122. }
  123. }
  124. }
  125. /// ends all live activities immediateny
  126. private func endActivity() async {
  127. if let currentActivity {
  128. await currentActivity.activity.end(nil, dismissalPolicy: ActivityUIDismissalPolicy.immediate)
  129. self.currentActivity = nil
  130. }
  131. // end any other activities
  132. for unknownActivity in Activity<LiveActivityAttributes>.activities {
  133. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  134. }
  135. }
  136. }
  137. @available(iOS 16.2, *)
  138. extension LiveActivityBridge: GlucoseObserver {
  139. func glucoseDidUpdate(_ glucose: [BloodGlucose]) {
  140. // backfill latest glucose if contained in this update
  141. if glucose.count > 1 {
  142. latestGlucose = glucose[glucose.count - 2]
  143. }
  144. defer {
  145. self.latestGlucose = glucose.last
  146. }
  147. guard let bg = glucose.last, let content = LiveActivityAttributes.ContentState(
  148. new: bg,
  149. prev: latestGlucose,
  150. mmol: settings.units == .mmolL
  151. ) else {
  152. // no bg or value stale. Don't update the activity if there already is one, just let it turn stale so that it can still be used once current bg is available again
  153. return
  154. }
  155. Task {
  156. await self.pushUpdate(content)
  157. }
  158. }
  159. }