Просмотр исходного кода

Implement RestartLiveActivityIntent
* Adds RestartLiveActivityIntent and RestartLiveActivityIntentRequest to Shortscuts
* Adds functionality to force a restart via AppIntent to LiveActivityManager
* Rename LiveActivityBridge to LiveActivityManager
* Adds docstrings to LiveActivityManager

Deniz Cengiz 1 год назад
Родитель
Сommit
c680be678c

+ 23 - 10
Trio.xcodeproj/project.pbxproj

@@ -262,7 +262,7 @@
 		6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */; };
 		6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6B1A8D232B14D91700E76752 /* Assets.xcassets */; };
 		6B1A8D282B14D91700E76752 /* LiveActivityExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
-		6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */; };
+		6B1A8D2E2B156EEF00E76752 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */; };
 		6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A708CDB546692C2230B385 /* SnoozeDataFlow.swift */; };
 		6BCF84DD2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */; };
 		6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */; };
@@ -294,11 +294,11 @@
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
 		BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */; };
+		BD432CA12D2F4E3600D1EB79 /* WatchMessageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */; };
+		BD432CA22D2F4E4000D1EB79 /* WatchMessageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */; };
 		BD4D738D2D15A4080052227B /* TDDStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4D738B2D15A4080052227B /* TDDStored+CoreDataClass.swift */; };
 		BD4D738E2D15A4080052227B /* TDDStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4D738C2D15A4080052227B /* TDDStored+CoreDataProperties.swift */; };
 		BD4D73A22D15A42A0052227B /* TDDStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4D73A12D15A4220052227B /* TDDStorage.swift */; };
-		BD432CA12D2F4E3600D1EB79 /* WatchMessageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */; };
-		BD432CA22D2F4E4000D1EB79 /* WatchMessageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */; };
 		BD4E1A7A2D3681B700D21626 /* GlucoseTargetSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */; };
 		BD4E1A7C2D3686D900D21626 /* StartEndMarkerSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */; };
 		BD4ED4FD2CF9D5E8000EDC9C /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */; };
@@ -486,6 +486,8 @@
 		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
+		DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */; };
+		DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */; };
 		DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */; };
 		DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */; };
 		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
@@ -967,7 +969,7 @@
 		6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivity.swift; sourceTree = "<group>"; };
 		6B1A8D232B14D91700E76752 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		6B1A8D252B14D91700E76752 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-		6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = "<group>"; };
+		6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
 		6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyAttributes.swift; sourceTree = "<group>"; };
 		715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationsOption.swift; sourceTree = "<group>"; };
 		71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = "<group>"; };
@@ -997,10 +999,10 @@
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
+		BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchMessageKeys.swift; sourceTree = "<group>"; };
 		BD4D738B2D15A4080052227B /* TDDStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TDDStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		BD4D738C2D15A4080052227B /* TDDStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TDDStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		BD4D73A12D15A4220052227B /* TDDStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDDStorage.swift; sourceTree = "<group>"; };
-		BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchMessageKeys.swift; sourceTree = "<group>"; };
 		BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseTargetSetup.swift; sourceTree = "<group>"; };
 		BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartEndMarkerSetup.swift; sourceTree = "<group>"; };
 		BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
@@ -1190,6 +1192,8 @@
 		DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Helpers.swift"; sourceTree = "<group>"; };
 		DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+Extensions.swift"; sourceTree = "<group>"; };
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
+		DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntent.swift; sourceTree = "<group>"; };
+		DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntentRequest.swift; sourceTree = "<group>"; };
 		DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminWatchState.swift; sourceTree = "<group>"; };
 		DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+Overrides.swift"; sourceTree = "<group>"; };
 		DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+TempTargets.swift"; sourceTree = "<group>"; };
@@ -2153,12 +2157,9 @@
 		38A0362725ECF05300FCBB52 /* Storage */ = {
 			isa = PBXGroup;
 			children = (
-				CE82E02428E867BA00473A9C /* AlertStorage.swift */,
 				38AEE75625F0F18E0013F05B /* CarbsStorage.swift */,
 				DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */,
-				5864E8582C42CFAE00294306 /* DeterminationStorage.swift */,
 				38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */,
-				BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */,
 				38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */,
 				38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */,
 				CE82E02428E867BA00473A9C /* AlertStorage.swift */,
@@ -2428,7 +2429,7 @@
 		6B1A8D2C2B156EC100E76752 /* LiveActivity */ = {
 			isa = PBXGroup;
 			children = (
-				6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */,
+				6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */,
 				6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */,
 				BDF34F922C10D0E100D51995 /* LiveActivityAttributes+Helper.swift */,
 				BDF34F882C10C65E00D51995 /* Data */,
@@ -2663,6 +2664,7 @@
 		CE7CA3422A064973004BE681 /* Shortcuts */ = {
 			isa = PBXGroup;
 			children = (
+				DD4C57A42D73ADDA001BFF2C /* LiveActivity */,
 				118DF7692C5ECBC60067FEB7 /* Override */,
 				110AEDE22C5193D100615CC9 /* Bolus */,
 				CE1856F32ADC4835007E39C7 /* Carbs */,
@@ -2931,6 +2933,15 @@
 			path = Helper;
 			sourceTree = "<group>";
 		};
+		DD4C57A42D73ADDA001BFF2C /* LiveActivity */ = {
+			isa = PBXGroup;
+			children = (
+				DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */,
+				DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */,
+			);
+			path = LiveActivity;
+			sourceTree = "<group>";
+		};
 		DD5DC9EF2CF3D95400AB8703 /* AdjustmentsStateModel+Extensions */ = {
 			isa = PBXGroup;
 			children = (
@@ -3599,6 +3610,7 @@
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
+				DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */,
 				DD1745402C55BFC100211FAC /* AlgorithmAdvancedSettingsRootView.swift in Sources */,
 				58645BA52CA2D347008AFCE7 /* ForecastSetup.swift in Sources */,
 				110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */,
@@ -3782,7 +3794,7 @@
 				DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
-				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
+				6B1A8D2E2B156EEF00E76752 /* LiveActivityManager.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */,
 				DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */,
@@ -3938,6 +3950,7 @@
 				389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */,
 				110AEDEC2C51A0AE00615CC9 /* ShortcutsConfigDataFlow.swift in Sources */,
 				CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */,
+				DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */,
 				19E1F7EA29D082ED005C8D20 /* IconConfigProvider.swift in Sources */,
 				DD09D4822C5986F6003FEA5D /* CalendarEventSettingsRootView.swift in Sources */,
 				CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */,

+ 1 - 1
Trio/Sources/Application/TrioApp.swift

@@ -58,7 +58,7 @@ import Swinject
         _ = resolver.resolve(PluginManager.self)!
         _ = resolver.resolve(AlertPermissionsChecker.self)!
         if #available(iOS 16.2, *) {
-            _ = resolver.resolve(LiveActivityBridge.self)!
+            _ = resolver.resolve(LiveActivityManager.self)!
         }
     }
 

+ 2 - 2
Trio/Sources/Assemblies/ServiceAssembly.swift

@@ -24,8 +24,8 @@ final class ServiceAssembly: Assembly {
         container.register(ContactImageManager.self) { r in BaseContactImageManager(resolver: r) }
         container.register(AlertPermissionsChecker.self) { r in AlertPermissionsChecker(resolver: r) }
         if #available(iOS 16.2, *) {
-            container.register(LiveActivityBridge.self) { r in
-                LiveActivityBridge(resolver: r)
+            container.register(LiveActivityManager.self) { r in
+                LiveActivityManager(resolver: r)
             }
         }
     }

+ 6 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -146718,6 +146718,9 @@
         }
       }
     },
+    "Submodules" : {
+
+    },
     "Subtract IOB" : {
       "localizations" : {
         "ar" : {
@@ -167880,6 +167883,9 @@
         }
       }
     },
+    "Trio Live Activity restarted successfully." : {
+
+    },
     "Trio Not Active" : {
       "comment" : "Trio Not Active",
       "localizations" : {

+ 1 - 1
Trio/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift

@@ -155,7 +155,7 @@ extension LiveActivitySettings {
                 }
             }
             .listSectionSpacing(sectionSpacing)
-            .onReceive(resolver.resolve(LiveActivityBridge.self)!.$systemEnabled, perform: {
+            .onReceive(resolver.resolve(LiveActivityManager.self)!.$systemEnabled, perform: {
                 self.systemLiveActivitySetting = $0
             })
             .sheet(isPresented: $shouldDisplayHint) {

+ 1 - 1
Trio/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -3,7 +3,7 @@ import Foundation
 // Fetch Data for Glucose and Determination from Core Data and map them to the Structs in order to pass them thread safe to the glucoseDidUpdate/ pushUpdate function
 
 @available(iOS 16.2, *)
-extension LiveActivityBridge {
+extension LiveActivityManager {
     func fetchAndMapGlucose() async throws -> [GlucoseData] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,

+ 92 - 3
Trio/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -9,6 +9,9 @@ import UIKit
     let activity: Activity<LiveActivityAttributes>
     let startDate: Date
 
+    /// Determines if the current activity needs to be recreated.
+    ///
+    /// - Returns: `true` if the activity is dismissed, ended, stale, or has been active for more than 60 minutes; otherwise, `false`.
     func needsRecreation() -> Bool {
         switch activity.activityState {
         case .dismissed,
@@ -24,34 +27,54 @@ import UIKit
     }
 }
 
+/// A service managing live activity updates and state management.
+///
+/// This class handles the creation, update, and termination of live activities based on various data sources
+/// (e.g. Core Data notifications, glucose updates, settings changes). It integrates with system notifications,
+/// dependency injection, and user defaults to ensure that the live activity reflects the current app state.
+///
+/// Additionally, it supports a restart functionality (via `restartActivityFromLiveActivityIntent()`)
+/// via iOS shortcuts, similar to other iOS apps like xDrip4iOS or Sweat Dreams.
 @available(iOS 16.2, *)
-final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
+final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var storage: FileStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
 
     private let activityAuthorizationInfo = ActivityAuthorizationInfo()
+    /// Indicates whether system live activities are enabled.
     @Published private(set) var systemEnabled: Bool
 
+    /// Returns the current Trio settings.
     private var settings: TrioSettings {
         settingsManager.settings
     }
 
+    /// Determination data used to update live activity state.
     var determination: DeterminationData?
+    /// The current active live activity.
     private var currentActivity: ActiveActivity?
+    /// The most recent glucose reading.
     private var latestGlucose: GlucoseData?
+    /// Array of glucose readings fetched from persistent storage.
     var glucoseFromPersistence: [GlucoseData]?
+    /// The current override data (if any).
     var override: OverrideData?
+    /// The widget items displayed within the live activity.
     var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
 
+    /// A Core Data task context.
     let context = CoreDataStack.shared.newTaskContext()
 
-    // Queue for handling Core Data change notifications
+    /// A dispatch queue for handling Core Data change notifications.
     private let queue = DispatchQueue(label: "LiveActivityBridge.queue", qos: .userInitiated)
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
+    /// Initializes a new instance of `LiveActivityBridge` and sets up observers, subscribers, and notifications.
+    ///
+    /// - Parameter resolver: The dependency injection resolver.
     init(resolver: Resolver) {
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
@@ -69,6 +92,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         broadcaster.register(SettingsObserver.self, observer: self)
     }
 
+    /// Sets up application notifications that trigger live activity updates when the app state changes.
     private func setupNotifications() {
         let notificationCenter = Foundation.NotificationCenter.default
         notificationCenter
@@ -91,12 +115,17 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         )
     }
 
+    /// Called when the app settings change.
+    ///
+    /// This method triggers an update to the live activity content state based on the new settings.
+    /// - Parameter _: The updated `TrioSettings`.
     func settingsDidChange(_: TrioSettings) {
         Task {
             await updateContentState(determination)
         }
     }
 
+    /// Registers handlers for Core Data changes related to overrides, glucose readings, and determinations.
     private func registerHandler() {
         coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
@@ -116,6 +145,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
             }.store(in: &subscriptions)
     }
 
+    /// Registers subscribers for updates from the glucose storage.
     private func registerSubscribers() {
         glucoseStorage.updatePublisher
             .receive(on: queue)
@@ -126,6 +156,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
             .store(in: &subscriptions)
     }
 
+    /// Fetches and maps new determination data and updates the live activity content state.
     private func cobOrIobDidUpdate() {
         Task { @MainActor in
             do {
@@ -142,6 +173,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Fetches and maps override data and updates the live activity content state.
     private func overridesDidUpdate() {
         Task { @MainActor in
             do {
@@ -155,6 +187,9 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Handles changes to the live activity order.
+    ///
+    /// Loads widget items from user defaults and triggers an update to the live activity order.
     @objc private func handleLiveActivityOrderChange() {
         Task {
             self.widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
@@ -163,6 +198,9 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Updates the live activity content state based on new determination or override data.
+    ///
+    /// - Parameter update: An object representing new `DeterminationData` or `OverrideData`.
     @MainActor private func updateContentState<T>(_ update: T) async {
         guard let latestGlucose = latestGlucose else {
             return
@@ -201,12 +239,16 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Triggers an update of the live activity order.
+    ///
+    /// This method refreshes the activity’s content state to reflect any changes in the widget order.
     @MainActor private func updateLiveActivityOrder() async {
         Task {
             await updateContentState(determination)
         }
     }
 
+    /// Sets up the array of glucose data from persistent storage and triggers an update to the live activity.
     private func setupGlucoseArray() {
         Task { @MainActor in
             do {
@@ -218,6 +260,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Monitors live activity authorization changes and updates the `systemEnabled` flag.
     private func monitorForLiveActivityAuthorizationChanges() {
         Task {
             for await activityState in activityAuthorizationInfo.activityEnablementUpdates {
@@ -230,6 +273,10 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Forces an update to the live activity.
+    ///
+    /// If live activities are enabled and the current activity requires recreation, this method triggers a new glucose update.
+    /// Otherwise, it ends the current live activity.
     @MainActor private func forceActivityUpdate() {
         if settings.useLiveActivity {
             if currentActivity?.needsRecreation() ?? true {
@@ -242,6 +289,12 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Pushes an update to the live activity with the specified content state.
+    ///
+    /// If an existing activity requires recreation or is outdated, this method ends it and starts a new one.
+    /// Otherwise, it updates the current live activity.
+    ///
+    /// - Parameter state: The new content state to push to the live activity.
     @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
         for unknownActivity in Activity<LiveActivityAttributes>.activities
             .filter({ self.currentActivity?.activity.id != $0.id })
@@ -297,6 +350,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Ends the current live activity and ensures that all unknown activities are terminated.
     private func endActivity() async {
         if let currentActivity {
             await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
@@ -307,10 +361,45 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
         }
     }
+
+    /// Restarts the live activity from a Live Activity Intent.
+    ///
+    /// This method mimics xdrip’s `restartActivityFromLiveActivityIntent()` behavior by verifying that a valid content state exists,
+    /// ending the current live activity, and starting a new one using the current state.
+    @MainActor func restartActivityFromLiveActivityIntent() async {
+        guard let latestGlucose = latestGlucose,
+              let determination = determination
+        else {
+            debug(.default, "Cannot restart live activity because required persistent state is not available")
+            return
+        }
+
+        guard let contentState = LiveActivityAttributes.ContentState(
+            new: latestGlucose,
+            prev: latestGlucose,
+            units: settings.units,
+            chart: glucoseFromPersistence ?? [],
+            settings: settings,
+            determination: determination,
+            override: override,
+            widgetItems: widgetItems
+        ) else {
+            debug(.default, "Cannot restart live activity because content state cannot be created")
+            return
+        }
+
+        await endActivity()
+        await pushUpdate(contentState)
+        debug(.default, "Restarted Live Activity from LiveActivityIntent (via iOS Shortcut)")
+    }
 }
 
 @available(iOS 16.2, *)
-extension LiveActivityBridge {
+extension LiveActivityManager {
+    /// Updates the live activity when new glucose data is available.
+    ///
+    /// This function adjusts the live activity content based on new glucose readings and triggers an update to the live activity.
+    /// - Parameter glucose: An array of `GlucoseData` objects.
     @MainActor func glucoseDidUpdate(_ glucose: [GlucoseData]) {
         guard settings.useLiveActivity else {
             if currentActivity != nil {

+ 1 - 0
Trio/Sources/Shortcuts/BaseIntentsRequest.swift

@@ -14,6 +14,7 @@ import Swinject
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var apsManager: APSManager!
     @Injected() var overrideStorage: OverrideStorage!
+    @Injected() var liveActivityManager: LiveActivityManager!
 
     let resolver: Resolver
 

+ 29 - 0
Trio/Sources/Shortcuts/LiveActivity/RestartLiveActivityIntent.swift

@@ -0,0 +1,29 @@
+import AppIntents
+import Foundation
+
+/// App Intent used to restart the live activity via Apple Shortcuts automation.
+/// When invoked, this intent instantiates a RestartLiveActivityIntentRequest, which has its
+/// dependencies injected via Swinject, and calls the restart functionality.
+@available(iOS 16.2, *) struct RestartLiveActivityIntent: LiveActivityIntent {
+    /// Title of the action in the Shortcuts app.
+    static var title = LocalizedStringResource("Restart Live Activity", table: "ShortcutsDetail")
+
+    /// Description of the action in the Shortcuts app.
+    static var description = IntentDescription(.init("Restarts Trio's Live Activity", table: "ShortcutsDetail"))
+
+    /// Performs the intent by triggering the live activity restart.
+    ///
+    /// This method creates an instance of RestartLiveActivityIntentRequest (which inherits from BaseIntentsRequest)
+    /// so that dependency injection provides the required services, then calls its restart functionality.
+    ///
+    /// - Returns: An intent result indicating success.
+    @MainActor func perform() async throws -> some ReturnsValue<String> {
+        let request = RestartLiveActivityIntentRequest()
+        do {
+            try await request.performRestart()
+        } catch {
+            debug(.default, "Error restarting Live Activity: \(error)")
+        }
+        return .result(value: String(localized: "Trio Live Activity restarted successfully."))
+    }
+}

+ 35 - 0
Trio/Sources/Shortcuts/LiveActivity/RestartLiveActivityIntentRequest.swift

@@ -0,0 +1,35 @@
+import AppIntents
+import Foundation
+import UIKit
+
+/// Request object that uses dependency injection to perform a live activity restart.
+/// This class inherits from BaseIntentsRequest so that its dependencies (including liveActivityManager)
+/// are automatically injected.
+@available(iOS 16.2, *) final class RestartLiveActivityIntentRequest: BaseIntentsRequest {
+    /// Triggers the live activity restart via the injected LiveActivityManager.
+    ///
+    /// - Throws: An error if the restart process fails.
+    /// - Returns: Void upon successful restart.
+    @MainActor func performRestart() async throws {
+        // Start background task
+        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+        backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Restart Live Activity") {
+            guard backgroundTaskID != .invalid else { return }
+            Task {
+                UIApplication.shared.endBackgroundTask(backgroundTaskID)
+            }
+            backgroundTaskID = .invalid
+        }
+
+        defer {
+            if backgroundTaskID != .invalid {
+                Task {
+                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
+                }
+                backgroundTaskID = .invalid
+            }
+        }
+
+        await liveActivityManager.restartActivityFromLiveActivityIntent()
+    }
+}