Kaynağa Gözat

live activity

10nas 2 yıl önce
ebeveyn
işleme
c8b5cc332a

+ 193 - 1
FreeAPS.xcodeproj/project.pbxproj

@@ -271,8 +271,17 @@
 		6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */; };
 		69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */; };
 		69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */; };
+		6B1A8D192B14D91600E76752 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D182B14D91600E76752 /* WidgetKit.framework */; };
+		6B1A8D1B2B14D91600E76752 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */; };
+		6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */; };
+		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 */; };
 		6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A708CDB546692C2230B385 /* SnoozeDataFlow.swift */; };
 		6B9625766B697D1C98E455A2 /* PumpSettingsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72778B68C3004F71F6E79BDC /* PumpSettingsEditorStateModel.swift */; };
+		6BCF84DD2B16843A003AD46E /* LiveActitiyShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */; };
+		6BCF84DE2B16843A003AD46E /* LiveActitiyShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */; };
 		6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */; };
 		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
@@ -424,6 +433,13 @@
 			remoteGlobalIDString = 388E595725AD948C0019842D;
 			remoteInfo = FreeAPS;
 		};
+		6B1A8D262B14D91700E76752 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 388E595025AD948C0019842D /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 6B1A8D162B14D91500E76752;
+			remoteInfo = LiveActivityExtension;
+		};
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXCopyFilesBuildPhase section */
@@ -481,6 +497,17 @@
 			name = "Embed App Extensions";
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		6B1A8D122B14D88E00E76752 /* Embed Foundation Extensions */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 13;
+			files = (
+				6B1A8D282B14D91700E76752 /* LiveActivityExtension.appex in Embed Foundation Extensions */,
+			);
+			name = "Embed Foundation Extensions";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
@@ -792,6 +819,16 @@
 		66A5B83E7967C38F7CBD883C /* LibreConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigDataFlow.swift; sourceTree = "<group>"; };
 		67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.swift; sourceTree = "<group>"; };
 		680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalProvider.swift; sourceTree = "<group>"; };
+		6B1A8D012B14D88B00E76752 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
+		6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LiveActivityExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+		6B1A8D182B14D91600E76752 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
+		6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
+		6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBundle.swift; sourceTree = "<group>"; };
+		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>"; };
+		6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyShared.swift; sourceTree = "<group>"; };
 		6F8BA8533F56BC55748CA877 /* PreferencesEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorProvider.swift; sourceTree = "<group>"; };
 		72778B68C3004F71F6E79BDC /* PumpSettingsEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorStateModel.swift; sourceTree = "<group>"; };
 		79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorDataFlow.swift; sourceTree = "<group>"; };
@@ -961,6 +998,15 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		6B1A8D142B14D91500E76752 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6B1A8D1B2B14D91600E76752 /* SwiftUI.framework in Frameworks */,
+				6B1A8D192B14D91600E76752 /* WidgetKit.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
@@ -1291,6 +1337,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				6B1A8D2C2B156EC100E76752 /* LiveActivity */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
 				F90692A8274B7A980037068D /* HealthKit */,
 				38E8754D275556E100975559 /* WatchManager */,
@@ -1458,6 +1505,9 @@
 				3818AA56274C26A300843DB3 /* RileyLinkKit.framework */,
 				3818AA57274C26A300843DB3 /* RileyLinkKitUI.framework */,
 				3818AA49274C267000843DB3 /* CGMBLEKit.framework */,
+				6B1A8D012B14D88B00E76752 /* UniformTypeIdentifiers.framework */,
+				6B1A8D182B14D91600E76752 /* WidgetKit.framework */,
+				6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -1538,6 +1588,7 @@
 				3818AA44274C229000843DB3 /* Packages */,
 				38E8751D27554D5500975559 /* FreeAPSWatch */,
 				38E8752827554D5700975559 /* FreeAPSWatch WatchKit Extension */,
+				6B1A8D1C2B14D91600E76752 /* LiveActivity */,
 				388E595925AD948C0019842D /* Products */,
 				3818AA48274C267000843DB3 /* Frameworks */,
 				192F0FF5276AC36D0085BE4D /* Recovered References */,
@@ -1551,6 +1602,7 @@
 				38FCF3ED25E9028E0078B0D1 /* FreeAPSTests.xctest */,
 				38E8751C27554D5500975559 /* FreeAPSWatch.app */,
 				38E8752427554D5700975559 /* FreeAPSWatch WatchKit Extension.appex */,
+				6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -1956,6 +2008,26 @@
 			path = AutotuneConfig;
 			sourceTree = "<group>";
 		};
+		6B1A8D1C2B14D91600E76752 /* LiveActivity */ = {
+			isa = PBXGroup;
+			children = (
+				6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */,
+				6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */,
+				6B1A8D232B14D91700E76752 /* Assets.xcassets */,
+				6B1A8D252B14D91700E76752 /* Info.plist */,
+			);
+			path = LiveActivity;
+			sourceTree = "<group>";
+		};
+		6B1A8D2C2B156EC100E76752 /* LiveActivity */ = {
+			isa = PBXGroup;
+			children = (
+				6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */,
+				6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */,
+			);
+			path = LiveActivity;
+			sourceTree = "<group>";
+		};
 		6DC5D590658EF8B8DF94F9F5 /* AddCarbs */ = {
 			isa = PBXGroup;
 			children = (
@@ -2272,11 +2344,13 @@
 				388E595625AD948C0019842D /* Resources */,
 				3821ECD025DC703C00BC42AD /* Embed Frameworks */,
 				38E8753D27554D5900975559 /* Embed Watch Content */,
+				6B1A8D122B14D88E00E76752 /* Embed Foundation Extensions */,
 			);
 			buildRules = (
 			);
 			dependencies = (
 				38E8753B27554D5900975559 /* PBXTargetDependency */,
+				6B1A8D272B14D91700E76752 /* PBXTargetDependency */,
 			);
 			name = FreeAPS;
 			packageProductDependencies = (
@@ -2346,13 +2420,29 @@
 			productReference = 38FCF3ED25E9028E0078B0D1 /* FreeAPSTests.xctest */;
 			productType = "com.apple.product-type.bundle.unit-test";
 		};
+		6B1A8D162B14D91500E76752 /* LiveActivityExtension */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 6B1A8D292B14D91800E76752 /* Build configuration list for PBXNativeTarget "LiveActivityExtension" */;
+			buildPhases = (
+				6B1A8D132B14D91500E76752 /* Sources */,
+				6B1A8D142B14D91500E76752 /* Frameworks */,
+				6B1A8D152B14D91500E76752 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = LiveActivityExtension;
+			productName = LiveActivityExtension;
+			productReference = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */;
+			productType = "com.apple.product-type.app-extension";
+		};
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
 		388E595025AD948C0019842D /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastSwiftUpdateCheck = 1310;
 				LastUpgradeCheck = 1240;
 				TargetAttributes = {
 					388E595725AD948C0019842D = {
@@ -2416,6 +2506,7 @@
 				38FCF3EC25E9028E0078B0D1 /* FreeAPSTests */,
 				38E8751B27554D5500975559 /* FreeAPSWatch */,
 				38E8752327554D5700975559 /* FreeAPSWatch WatchKit Extension */,
+				6B1A8D162B14D91500E76752 /* LiveActivityExtension */,
 			);
 		};
 /* End PBXProject section */
@@ -2462,6 +2553,14 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		6B1A8D152B14D91500E76752 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXShellScriptBuildPhase section */
@@ -2647,6 +2746,7 @@
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* DecimalTextField.swift in Sources */,
+				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
 				38DAB28A260D349500F74C1A /* FetchGlucoseManager.swift in Sources */,
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
@@ -2792,6 +2892,7 @@
 				1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */,
 				19F95FFA29F1102A00314DDC /* StatRootView.swift in Sources */,
 				0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */,
+				6BCF84DD2B16843A003AD46E /* LiveActitiyShared.swift in Sources */,
 				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
 				F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */,
@@ -2849,6 +2950,16 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		6B1A8D132B14D91500E76752 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6BCF84DE2B16843A003AD46E /* LiveActitiyShared.swift in Sources */,
+				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */,
+				6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXSourcesBuildPhase section */
 
 /* Begin PBXTargetDependency section */
@@ -2867,6 +2978,11 @@
 			target = 388E595725AD948C0019842D /* FreeAPS */;
 			targetProxy = 38FCF3F225E9028E0078B0D1 /* PBXContainerItemProxy */;
 		};
+		6B1A8D272B14D91700E76752 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 6B1A8D162B14D91500E76752 /* LiveActivityExtension */;
+			targetProxy = 6B1A8D262B14D91700E76752 /* PBXContainerItemProxy */;
+		};
 /* End PBXTargetDependency section */
 
 /* Begin PBXVariantGroup section */
@@ -3333,6 +3449,73 @@
 			};
 			name = Release;
 		};
+		6B1A8D2A2B14D91800E76752 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = LiveActivity/Info.plist;
+				INFOPLIST_KEY_CFBundleDisplayName = LiveActivity;
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).LiveActivity";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		6B1A8D2B2B14D91800E76752 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = LiveActivity/Info.plist;
+				INFOPLIST_KEY_CFBundleDisplayName = LiveActivity;
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).LiveActivity";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
@@ -3381,6 +3564,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Debug;
 		};
+		6B1A8D292B14D91800E76752 /* Build configuration list for PBXNativeTarget "LiveActivityExtension" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				6B1A8D2A2B14D91800E76752 /* Debug */,
+				6B1A8D2B2B14D91800E76752 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Debug;
+		};
 /* End XCConfigurationList section */
 
 /* Begin XCRemoteSwiftPackageReference section */

+ 21 - 67
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,70 +1,24 @@
 {
-  "object": {
-    "pins": [
-      {
-        "package": "CryptoSwift",
-        "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift",
-        "state": {
-          "branch": null,
-          "revision": "19b3c3ceed117c5cc883517c4e658548315ba70b",
-          "version": "1.6.0"
-        }
-      },
-      {
-        "package": "swift-algorithms",
-        "repositoryURL": "https://github.com/apple/swift-algorithms",
-        "state": {
-          "branch": null,
-          "revision": "2327673b0e9c7e90e6b1826376526ec3627210e4",
-          "version": "0.2.1"
-        }
-      },
-      {
-        "package": "swift-numerics",
-        "repositoryURL": "https://github.com/apple/swift-numerics",
-        "state": {
-          "branch": null,
-          "revision": "6583ac70c326c3ee080c1d42d9ca3361dca816cd",
-          "version": "0.1.0"
-        }
-      },
-      {
-        "package": "SwiftCharts",
-        "repositoryURL": "https://github.com/ivanschuetz/SwiftCharts.git",
-        "state": {
-          "branch": "master",
-          "revision": "c354c1945bb35a1f01b665b22474f6db28cba4a2",
-          "version": null
-        }
-      },
-      {
-        "package": "SwiftDate",
-        "repositoryURL": "https://github.com/malcommac/SwiftDate",
-        "state": {
-          "branch": null,
-          "revision": "6190d0cefff3013e77ed567e6b074f324e5c5bf5",
-          "version": "6.3.1"
-        }
-      },
-      {
-        "package": "SwiftMessages",
-        "repositoryURL": "https://github.com/SwiftKickMobile/SwiftMessages",
-        "state": {
-          "branch": null,
-          "revision": "b29dd21090b708aa0ae9ecbaf6e2d0487028dc3f",
-          "version": "9.0.6"
-        }
-      },
-      {
-        "package": "Swinject",
-        "repositoryURL": "https://github.com/Swinject/Swinject",
-        "state": {
-          "branch": null,
-          "revision": "8bc503e60965298984fb58cf47b71c541449fe2a",
-          "version": "2.8.3"
-        }
+  "originHash" : "3bbb1091cfb3f1b8b58a093a985268a65a760f0d86e2d024e00ccab5721c0b89",
+  "pins" : [
+    {
+      "identity" : "cryptoswift",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/krzyzanowskim/CryptoSwift",
+      "state" : {
+        "revision" : "19b3c3ceed117c5cc883517c4e658548315ba70b",
+        "version" : "1.6.0"
       }
-    ]
-  },
-  "version": 1
+    },
+    {
+      "identity" : "swiftcharts",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/ivanschuetz/SwiftCharts",
+      "state" : {
+        "branch" : "master",
+        "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2"
+      }
+    }
+  ],
+  "version" : 3
 }

+ 4 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -1,3 +1,4 @@
+import ActivityKit
 import CoreData
 import Foundation
 import SwiftUI
@@ -45,6 +46,9 @@ import Swinject
         _ = resolver.resolve(WatchManager.self)!
         _ = resolver.resolve(HealthKitManager.self)!
         _ = resolver.resolve(BluetoothStateManager.self)!
+        if #available(iOS 16.2, *) {
+            _ = resolver.resolve(LiveActivityBridge.self)!
+        }
     }
 
     init() {

+ 6 - 0
FreeAPS/Sources/Assemblies/ServiceAssembly.swift

@@ -20,5 +20,11 @@ final class ServiceAssembly: Assembly {
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
         container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
         container.register(GarminManager.self) { r in BaseGarminManager(resolver: r) }
+
+        if #available(iOS 16.2, *) {
+            container.register(LiveActivityBridge.self) { r in
+                LiveActivityBridge(resolver: r)
+            }
+        }
     }
 }

+ 5 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -43,6 +43,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var maxCarbs: Decimal = 1000
     var displayFatAndProteinOnWatch: Bool = false
     var onlyAutotuneBasals: Bool = false
+    var useLiveActivity: Bool = true
 }
 
 extension FreeAPSSettings: Decodable {
@@ -224,6 +225,10 @@ extension FreeAPSSettings: Decodable {
             settings.onlyAutotuneBasals = onlyAutotuneBasals
         }
 
+        if let useLiveActivity = try? container.decode(Bool.self, forKey: .useLiveActivity) {
+            settings.useLiveActivity = useLiveActivity
+        }
+
         self = settings
     }
 }

+ 2 - 0
FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigStateModel.swift

@@ -9,6 +9,7 @@ extension NotificationsConfig {
         @Published var lowGlucose: Decimal = 0
         @Published var highGlucose: Decimal = 0
         @Published var carbsRequiredThreshold: Decimal = 0
+        @Published var useLiveActivity = true
         var units: GlucoseUnits = .mmolL
 
         override func subscribe() {
@@ -20,6 +21,7 @@ extension NotificationsConfig {
             subscribeSetting(\.useAlarmSound, on: $useAlarmSound) { useAlarmSound = $0 }
             subscribeSetting(\.addSourceInfoToGlucoseNotifications, on: $addSourceInfoToGlucoseNotifications) {
                 addSourceInfoToGlucoseNotifications = $0 }
+            subscribeSetting(\.useLiveActivity, on: $useLiveActivity) { useLiveActivity = $0 }
 
             subscribeSetting(\.lowGlucose, on: $lowGlucose, initial: {
                 let value = max(min($0, 400), 40)

+ 11 - 0
FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift

@@ -55,6 +55,17 @@ extension NotificationsConfig {
                         Text("g").foregroundColor(.secondary)
                     }
                 }
+
+                if #available(iOS 16.2, *) {
+                    Section(
+                        header: Text("Live Activity"),
+                        footer: Text(
+                            "Live activity displays blood gluocse live on the lock screen and on the dynamic island (if available)"
+                        )
+                    ) {
+                        Toggle("Show live activity", isOn: $state.useLiveActivity)
+                    }
+                }
             }
             .onAppear(perform: configureView)
             .navigationBarTitle("Notifications")

+ 13 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActitiyShared.swift

@@ -0,0 +1,13 @@
+import ActivityKit
+import Foundation
+
+struct LiveActivityAttributes: ActivityAttributes {
+    public struct ContentState: Codable, Hashable {
+        let bg: String
+        let trendSystemImage: String?
+        let change: Int?
+        let date: Date
+    }
+
+    let startDate: Date
+}

+ 187 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -0,0 +1,187 @@
+import ActivityKit
+import Foundation
+import Swinject
+import UIKit
+
+extension LiveActivityAttributes.ContentState {
+    static func formatGlucose(_ value: Int, mmol: Bool) -> String {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        if mmol {
+            formatter.minimumFractionDigits = 1
+            formatter.maximumFractionDigits = 1
+        }
+        formatter.roundingMode = .halfUp
+
+        return formatter
+            .string(from: mmol ? value.asMmolL as NSNumber : NSNumber(value: value))!
+    }
+
+    init?(new bg: BloodGlucose, prev: BloodGlucose?, mmol: Bool) {
+        guard let glucose = bg.glucose,
+              bg.dateString.timeIntervalSinceNow > -TimeInterval(minutes: 6)
+        else {
+            return nil
+        }
+
+        let formattedBG = Self.formatGlucose(glucose, mmol: mmol)
+
+        let trentString: String?
+        switch bg.direction {
+        case .doubleUp,
+             .singleUp,
+             .tripleUp:
+            trentString = "arrow.up"
+
+        case .fortyFiveUp:
+            trentString = "arrow.up.right"
+
+        case .flat:
+            trentString = "arrow.right"
+
+        case .fortyFiveDown:
+            trentString = "arrow.down.right"
+
+        case .doubleDown,
+             .singleDown,
+             .tripleDown:
+            trentString = "arrow.down"
+
+        case .notComputable,
+             Optional.none,
+             .rateOutOfRange,
+             .some(.none):
+            trentString = nil
+        }
+
+        let change = prev?.glucose.map({ glucose - $0 })
+
+        self.init(bg: formattedBG, trendSystemImage: trentString, change: change, date: bg.dateString)
+    }
+}
+
+@available(iOS 16.2, *) private struct ActiveActivity {
+    let activity: Activity<LiveActivityAttributes>
+    let startDate: Date
+}
+
+@available(iOS 16.2, *) final class LiveActivityBridge: Injectable {
+    @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() private var broadcaster: Broadcaster!
+
+    private var settings: FreeAPSSettings {
+        settingsManager.settings
+    }
+
+    private var currentActivity: ActiveActivity?
+    private var latestGlucose: BloodGlucose?
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+        broadcaster.register(GlucoseObserver.self, observer: self)
+
+        Foundation.NotificationCenter.default.addObserver(
+            forName: UIApplication.didEnterBackgroundNotification,
+            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()
+                }
+            }
+        }
+    }
+
+    /// creates and tries to present a new activity update from the current GlucoseStorage values
+    private func forceActivityUpdate() {
+        glucoseDidUpdate(glucoseStorage.recent())
+    }
+
+    /// attempts to present this live activity state, creating a new activity if none exists yet
+    private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
+        // hide duplicate/unknown activities
+        for unknownActivity in Activity<LiveActivityAttributes>.activities
+            .filter({ self.currentActivity?.activity.id != $0.id })
+        {
+            await unknownActivity.end(nil, dismissalPolicy: .immediate)
+        }
+
+        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
+                await endActivity()
+                await pushUpdate(state)
+            case .active,
+                 .stale: await currentActivity.activity.update(content)
+            @unknown default:
+                await currentActivity.activity.update(content)
+            }
+
+        } else {
+            do {
+                let activity = try Activity.request(
+                    attributes: LiveActivityAttributes(startDate: Date.now),
+                    content: content,
+                    pushType: nil
+                )
+                currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
+            } catch {
+                print("activity creation error: \(error)")
+            }
+        }
+    }
+
+    /// ends all live activities immediateny
+    private func endActivity() async {
+        if let currentActivity {
+            await currentActivity.activity.end(nil, dismissalPolicy: ActivityUIDismissalPolicy.immediate)
+            self.currentActivity = nil
+        }
+
+        // end any other activities
+        for unknownActivity in Activity<LiveActivityAttributes>.activities {
+            await unknownActivity.end(nil, dismissalPolicy: .immediate)
+        }
+    }
+}
+
+@available(iOS 16.2, *)
+extension LiveActivityBridge: GlucoseObserver {
+    func glucoseDidUpdate(_ glucose: [BloodGlucose]) {
+        // backfill latest glucose if contained in this update
+        if glucose.count > 1 {
+            latestGlucose = glucose[glucose.count - 2]
+        }
+        defer {
+            self.latestGlucose = glucose.last
+        }
+
+        guard let bg = glucose.last, let content = LiveActivityAttributes.ContentState(
+            new: bg,
+            prev: latestGlucose,
+            mmol: settings.units == .mmolL
+        ) else {
+            // 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
+            return
+        }
+
+        Task {
+            await self.pushUpdate(content)
+        }
+    }
+}

+ 11 - 0
LiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json

@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 13 - 0
LiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,13 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 0
LiveActivity/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 11 - 0
LiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json

@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 109 - 0
LiveActivity/LiveActivity.swift

@@ -0,0 +1,109 @@
+import ActivityKit
+import SwiftUI
+import WidgetKit
+
+struct LiveActivity: Widget {
+    let dateFormatter: DateFormatter = {
+        var f = DateFormatter()
+        f.dateStyle = .none
+        f.timeStyle = .short
+        return f
+    }()
+
+    func changeLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
+        if let change = context.state.change {
+            Text("\(change > 0 ? "+" : "")\(change)")
+        } else {
+            Text("--")
+        }
+    }
+
+    func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
+        Text("Updated: \(dateFormatter.string(from: context.state.date))")
+    }
+
+    func bgLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
+        if context.isStale {
+            Text("--")
+        } else {
+            Text("\(context.state.bg)")
+        }
+    }
+
+    @ViewBuilder func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        if context.isStale {
+            Text("--")
+        } else {
+            Text("\(context.state.bg)")
+            if let trendSystemImage = context.state.trendSystemImage {
+                Image(systemName: trendSystemImage)
+            }
+        }
+    }
+
+    var body: some WidgetConfiguration {
+        ActivityConfiguration(for: LiveActivityAttributes.self) { context in
+            // Lock screen/banner UI goes here
+            VStack(alignment: .trailing, spacing: 0) {
+                HStack(alignment: .top) {
+                    HStack(alignment: .center, spacing: 3) {
+                        bgAndTrend(context: context).font(.title)
+                    }
+                    Spacer()
+                    changeLabel(context: context).font(.title3)
+                }
+                .imageScale(.small)
+                updatedLabel(context: context).font(.caption).offset(y: -3)
+            }
+            .padding(.horizontal, 20)
+            .padding(.vertical, 10)
+            .activityBackgroundTint(Color.cyan.opacity(0.2))
+            .activitySystemActionForegroundColor(Color.white)
+
+        } dynamicIsland: { context in
+            DynamicIsland {
+                // Expanded UI goes here.  Compose the expanded UI through
+                // various regions, like leading/trailing/center/bottom
+                DynamicIslandExpandedRegion(.leading) {
+                    HStack(spacing: 3) {
+                        bgAndTrend(context: context)
+                    }.imageScale(.small).font(.title).padding(.leading, 5)
+                }
+                DynamicIslandExpandedRegion(.trailing) {
+                    changeLabel(context: context).font(.title).padding(.trailing, 5)
+                }
+                DynamicIslandExpandedRegion(.bottom) {
+                    updatedLabel(context: context).font(.caption)
+                }
+            } compactLeading: {
+                HStack(spacing: 1) {
+                    bgAndTrend(context: context)
+                }.bold().imageScale(.small)
+            } compactTrailing: {
+                changeLabel(context: context)
+            } minimal: {
+                bgLabel(context: context).bold()
+            }
+            .widgetURL(URL(string: "freeaps-x://"))
+            .keylineTint(Color.cyan.opacity(0.5))
+        }
+    }
+}
+
+private extension LiveActivityAttributes {
+    static var preview: LiveActivityAttributes {
+        LiveActivityAttributes(startDate: Date())
+    }
+}
+
+private extension LiveActivityAttributes.ContentState {
+    static var test: LiveActivityAttributes.ContentState {
+        LiveActivityAttributes.ContentState(bg: "100", trendSystemImage: "arrow.right", change: 2, date: Date())
+    }
+}
+
+#Preview("Notification", as: .content, using: LiveActivityAttributes.preview) {
+    LiveActivity()
+} contentStates: {
+    LiveActivityAttributes.ContentState.test
+}

+ 8 - 0
LiveActivity/LiveActivityBundle.swift

@@ -0,0 +1,8 @@
+import SwiftUI
+import WidgetKit
+
+@main struct LiveActivityBundle: WidgetBundle {
+    var body: some Widget {
+        LiveActivity()
+    }
+}