Explorar o código

Merge pull request #72 from nightscout/live_activities_alpha

Live activities alpha
bjornoleh %!s(int64=2) %!d(string=hai) anos
pai
achega
16599a47de

+ 2 - 2
.github/workflows/create_certs.yml

@@ -15,8 +15,8 @@ jobs:
     runs-on: macos-13
     steps:
       # Uncomment to manually select Xcode version if needed
-      #- name: Select Xcode version
-      #  run: "sudo xcode-select --switch /Applications/Xcode_14.1.app/Contents/Developer"
+      - name: Select Xcode version
+        run: "sudo xcode-select --switch /Applications/Xcode_15.0.1.app/Contents/Developer"
       
       # Checks-out the repo
       - name: Checkout Repo

+ 1 - 0
Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -105,6 +105,7 @@
     </entity>
     <entity name="Readings" representedClassName="Readings" syncable="YES" codeGenerationType="class">
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="direction" optional="YES" attributeType="String"/>
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="id" optional="YES" attributeType="String"/>
     </entity>

+ 201 - 1
FreeAPS.xcodeproj/project.pbxproj

@@ -22,6 +22,7 @@
 		1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; };
 		1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Oref2_variables.swift */; };
 		193F6CDD2A512C8F001240FD /* Loops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193F6CDC2A512C8F001240FD /* Loops.swift */; };
+		1956FB212AFF79E200C7B4FF /* CoreDataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */; };
 		1967DFBE29D052C200759F30 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBD29D052C200759F30 /* Icons.swift */; };
 		1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBF29D053AC00759F30 /* IconSelection.swift */; };
 		1967DFC229D053D300759F30 /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFC129D053D300759F30 /* IconImage.swift */; };
@@ -246,8 +247,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 */; };
 		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
@@ -272,6 +282,7 @@
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
+		BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
@@ -393,6 +404,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 */
@@ -435,6 +453,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 */
@@ -475,6 +504,7 @@
 		193F1E3B2B44C14800525770 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		193F1E3C2B44C14800525770 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
 		193F6CDC2A512C8F001240FD /* Loops.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loops.swift; sourceTree = "<group>"; };
+		1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStorage.swift; sourceTree = "<group>"; };
 		1967DFBD29D052C200759F30 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = "<group>"; };
 		1967DFBF29D053AC00759F30 /* IconSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelection.swift; sourceTree = "<group>"; };
 		1967DFC129D053D300759F30 /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = "<group>"; };
@@ -738,6 +768,16 @@
 		64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorStateModel.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>"; };
@@ -766,6 +806,7 @@
 		B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMDataFlow.swift; sourceTree = "<group>"; };
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
+		BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
@@ -901,6 +942,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 */
@@ -1231,6 +1281,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				6B1A8D2C2B156EC100E76752 /* LiveActivity */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
 				F90692A8274B7A980037068D /* HealthKit */,
 				38E8754D275556E100975559 /* WatchManager */,
@@ -1402,6 +1453,9 @@
 				3818AA56274C26A300843DB3 /* RileyLinkKit.framework */,
 				3818AA57274C26A300843DB3 /* RileyLinkKitUI.framework */,
 				3818AA49274C267000843DB3 /* CGMBLEKit.framework */,
+				6B1A8D012B14D88B00E76752 /* UniformTypeIdentifiers.framework */,
+				6B1A8D182B14D91600E76752 /* WidgetKit.framework */,
+				6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -1472,6 +1526,7 @@
 				3818AA44274C229000843DB3 /* Packages */,
 				38E8751D27554D5500975559 /* FreeAPSWatch */,
 				38E8752827554D5700975559 /* FreeAPSWatch WatchKit Extension */,
+				6B1A8D1C2B14D91600E76752 /* LiveActivity */,
 				388E595925AD948C0019842D /* Products */,
 				3818AA48274C267000843DB3 /* Frameworks */,
 				192F0FF5276AC36D0085BE4D /* Recovered References */,
@@ -1485,6 +1540,7 @@
 				38FCF3ED25E9028E0078B0D1 /* FreeAPSTests.xctest */,
 				38E8751C27554D5500975559 /* FreeAPSWatch.app */,
 				38E8752427554D5700975559 /* FreeAPSWatch WatchKit Extension.appex */,
+				6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -1550,6 +1606,7 @@
 				19A910352A24D6D700C8951B /* DateFilter.swift */,
 				193F6CDC2A512C8F001240FD /* Loops.swift */,
 				CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */,
+				BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -1595,6 +1652,7 @@
 				38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */,
 				38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */,
 				CE82E02428E867BA00473A9C /* AlertStorage.swift */,
+				1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */,
 			);
 			path = Storage;
 			sourceTree = "<group>";
@@ -1883,6 +1941,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 = (
@@ -2197,12 +2275,14 @@
 				388E595625AD948C0019842D /* Resources */,
 				3821ECD025DC703C00BC42AD /* Embed Frameworks */,
 				38E8753D27554D5900975559 /* Embed Watch Content */,
+				6B1A8D122B14D88E00E76752 /* Embed Foundation Extensions */,
 				CE95BF582BA5F8F300DC3DE3 /* Install plugins */,
 			);
 			buildRules = (
 			);
 			dependencies = (
 				38E8753B27554D5900975559 /* PBXTargetDependency */,
+				6B1A8D272B14D91700E76752 /* PBXTargetDependency */,
 			);
 			name = FreeAPS;
 			packageProductDependencies = (
@@ -2272,13 +2352,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 = {
@@ -2343,6 +2439,7 @@
 				38FCF3EC25E9028E0078B0D1 /* FreeAPSTests */,
 				38E8751B27554D5500975559 /* FreeAPSWatch */,
 				38E8752327554D5700975559 /* FreeAPSWatch WatchKit Extension */,
+				6B1A8D162B14D91500E76752 /* LiveActivityExtension */,
 			);
 		};
 /* End PBXProject section */
@@ -2390,6 +2487,14 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		6B1A8D152B14D91500E76752 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXShellScriptBuildPhase section */
@@ -2470,6 +2575,7 @@
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
+				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
@@ -2598,6 +2704,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 */,
@@ -2743,6 +2850,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 */,
@@ -2750,6 +2858,7 @@
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,
 				E3A08AAE59538BC8A8ABE477 /* NotificationsConfigDataFlow.swift in Sources */,
+				1956FB212AFF79E200C7B4FF /* CoreDataStorage.swift in Sources */,
 				0F7A65FBD2CD8D6477ED4539 /* NotificationsConfigProvider.swift in Sources */,
 				3171D2818C7C72CD1584BB5E /* NotificationsConfigStateModel.swift in Sources */,
 				CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */,
@@ -2794,6 +2903,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 */
@@ -2812,6 +2931,11 @@
 			target = 388E595725AD948C0019842D /* FreeAPS */;
 			targetProxy = 38FCF3F225E9028E0078B0D1 /* PBXContainerItemProxy */;
 		};
+		6B1A8D272B14D91700E76752 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 6B1A8D162B14D91500E76752 /* LiveActivityExtension */;
+			targetProxy = 6B1A8D262B14D91700E76752 /* PBXContainerItemProxy */;
+		};
 /* End PBXTargetDependency section */
 
 /* Begin PBXVariantGroup section */
@@ -3278,6 +3402,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 = 16.2;
+				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 = 16.2;
+				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 */
@@ -3326,6 +3517,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 */

+ 2 - 1
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -41,5 +41,6 @@
   "oneDimensionalGraph" : false,
   "rulerMarks" : false,
   "maxCarbs": 1000,
-  "displayFatAndProteinOnWatch": false
+  "displayFatAndProteinOnWatch": false,
+  "lockScreenView": "simple"
 }

+ 34 - 0
FreeAPS/Sources/APS/Storage/CoreDataStorage.swift

@@ -0,0 +1,34 @@
+import CoreData
+import Foundation
+import SwiftDate
+import Swinject
+
+final class CoreDataStorage {
+    let coredataContext = CoreDataStack.shared.persistentContainer.viewContext // newBackgroundContext()
+
+    func fetchGlucose(interval: NSDate) -> [Readings] {
+        var fetchGlucose = [Readings]()
+        coredataContext.performAndWait {
+            let requestReadings = Readings.fetchRequest() as NSFetchRequest<Readings>
+            let sort = NSSortDescriptor(key: "date", ascending: false)
+            requestReadings.sortDescriptors = [sort]
+            requestReadings.predicate = NSPredicate(
+                format: "glucose > 0 AND date > %@", interval
+            )
+            try? fetchGlucose = self.coredataContext.fetch(requestReadings)
+        }
+        return fetchGlucose
+    }
+
+    func fetchLatestOverride() -> [Override] {
+        var overrideArray = [Override]()
+        coredataContext.performAndWait {
+            let requestOverrides = Override.fetchRequest() as NSFetchRequest<Override>
+            let sortOverride = NSSortDescriptor(key: "date", ascending: false)
+            requestOverrides.sortDescriptors = [sortOverride]
+            requestOverrides.fetchLimit = 1
+            try? overrideArray = self.coredataContext.fetch(requestOverrides)
+        }
+        return overrideArray
+    }
+}

+ 3 - 0
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -61,11 +61,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 var bg_ = 0
                 var bgDate = Date()
                 var id = ""
+                var direction = ""
 
                 if glucose.isNotEmpty {
                     bg_ = glucose[0].glucose ?? 0
                     bgDate = glucose[0].dateString
                     id = glucose[0].id
+                    direction = glucose[0].direction?.symbol ?? "↔︎"
                 }
 
                 if bg_ != 0 {
@@ -74,6 +76,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         dataForForStats.date = bgDate
                         dataForForStats.glucose = Int16(bg_)
                         dataForForStats.id = id
+                        dataForForStats.direction = direction
                         try? self.coredataContext.save()
                     }
                 }

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

@@ -1,3 +1,4 @@
+import ActivityKit
 import CoreData
 import Foundation
 import SwiftUI
@@ -46,6 +47,9 @@ import Swinject
         _ = resolver.resolve(HealthKitManager.self)!
         _ = resolver.resolve(BluetoothStateManager.self)!
         _ = resolver.resolve(PluginManager.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)
+            }
+        }
     }
 }

+ 1 - 0
FreeAPS/Sources/Models/DateFilter.swift

@@ -2,6 +2,7 @@
 import Foundation
 
 struct DateFilter {
+    var twoHours = Date().addingTimeInterval(-2.hours.timeInterval) as NSDate
     var today = Calendar.current.startOfDay(for: Date()) as NSDate
     var day = Date().addingTimeInterval(-24.hours.timeInterval) as NSDate
     var week = Date().addingTimeInterval(-7.days.timeInterval) as NSDate

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

@@ -44,6 +44,8 @@ struct FreeAPSSettings: JSON, Equatable {
     var maxCarbs: Decimal = 1000
     var displayFatAndProteinOnWatch: Bool = false
     var onlyAutotuneBasals: Bool = false
+    var useLiveActivity: Bool = false
+    var lockScreenView: LockScreenView = .simple
 }
 
 extension FreeAPSSettings: Decodable {
@@ -229,6 +231,13 @@ extension FreeAPSSettings: Decodable {
             settings.onlyAutotuneBasals = onlyAutotuneBasals
         }
 
+        if let useLiveActivity = try? container.decode(Bool.self, forKey: .useLiveActivity) {
+            settings.useLiveActivity = useLiveActivity
+        }
+        if let lockScreenView = try? container.decode(LockScreenView.self, forKey: .lockScreenView) {
+            settings.lockScreenView = lockScreenView
+        }
+
         self = settings
     }
 }

+ 15 - 0
FreeAPS/Sources/Models/LockScreenView.swift

@@ -0,0 +1,15 @@
+import Foundation
+
+enum LockScreenView: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+    case simple
+    case detailed
+    var displayName: String {
+        switch self {
+        case .simple:
+            return NSLocalizedString("Simple", comment: "")
+        case .detailed:
+            return NSLocalizedString("Detailed", comment: "")
+        }
+    }
+}

+ 4 - 1
FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigStateModel.swift

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

+ 81 - 4
FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift

@@ -1,3 +1,5 @@
+import ActivityKit
+import Combine
 import SwiftUI
 import Swinject
 
@@ -6,6 +8,14 @@ extension NotificationsConfig {
         let resolver: Resolver
         @StateObject var state = StateModel()
 
+        @State private var systemLiveActivitySetting: Bool = {
+            if #available(iOS 16.1, *) {
+                ActivityAuthorizationInfo().areActivitiesEnabled
+            } else {
+                false
+            }
+        }()
+
         private var glucoseFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
@@ -24,6 +34,71 @@ extension NotificationsConfig {
             return formatter
         }
 
+        @Environment(\.colorScheme) var colorScheme
+
+        var color: LinearGradient {
+            colorScheme == .dark ? LinearGradient(
+                gradient: Gradient(colors: [
+                    Color("Background_1"),
+                    Color("Background_1"),
+                    Color("Background_2")
+                    // Color("Background_1")
+                ]),
+                startPoint: .top,
+                endPoint: .bottom
+            )
+                :
+                LinearGradient(
+                    gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
+                    startPoint: .top,
+                    endPoint: .bottom
+                )
+        }
+
+        @ViewBuilder private func liveActivitySection() -> some View {
+            if #available(iOS 16.2, *) {
+                Section(
+                    header: Text("Live Activity"),
+                    footer: Text(
+                        liveActivityFooterText()
+                    ),
+                    content: {
+                        if !systemLiveActivitySetting {
+                            Button("Open Settings App") {
+                                UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
+                            }
+                        } else {
+                            Toggle("Show Live Activity", isOn: $state.useLiveActivity)
+                        }
+                        Picker(
+                            selection: $state.lockScreenView,
+                            label: Text("Lock screen widget")
+                        ) {
+                            ForEach(LockScreenView.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }
+                    }
+                )
+                .onReceive(resolver.resolve(LiveActivityBridge.self)!.$systemEnabled, perform: {
+                    self.systemLiveActivitySetting = $0
+                })
+            }
+        }
+
+        private func liveActivityFooterText() -> String {
+            var footer =
+                "Live activity displays blood glucose live on the lock screen and on the dynamic island (if available)"
+
+            if !systemLiveActivitySetting {
+                footer =
+                    "Live activities are turned OFF in system settings. To enable live activities, go to Settings app -> iAPS -> Turn live Activities ON.\n\n" +
+                    footer
+            }
+
+            return footer
+        }
+
         var body: some View {
             Form {
                 Section(header: Text("Glucose")) {
@@ -55,10 +130,12 @@ extension NotificationsConfig {
                         Text("g").foregroundColor(.secondary)
                     }
                 }
-            }
-            .onAppear(perform: configureView)
-            .navigationBarTitle("Notifications")
-            .navigationBarTitleDisplayMode(.automatic)
+
+                liveActivitySection()
+            }.scrollContentBackground(.hidden).background(color)
+                .onAppear(perform: configureView)
+                .navigationBarTitle("Notifications")
+                .navigationBarTitleDisplayMode(.automatic)
         }
     }
 }

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

@@ -0,0 +1,21 @@
+import ActivityKit
+import Foundation
+
+struct LiveActivityAttributes: ActivityAttributes {
+    public struct ContentState: Codable, Hashable {
+        let bg: String
+        let direction: String?
+        let change: String
+        let date: Date
+        let chart: [Double]
+        let chartDate: [Date?]
+        let rotationDegrees: Double
+        let highGlucose: Double
+        let lowGlucose: Double
+        let cob: Decimal
+        let iob: Decimal
+        let lockScreenView: String
+    }
+
+    let startDate: Date
+}

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

@@ -0,0 +1,313 @@
+import ActivityKit
+import Foundation
+import Swinject
+import UIKit
+
+extension LiveActivityAttributes.ContentState {
+    static func formatGlucose(_ value: Int, mmol: Bool, forceSign: Bool) -> String {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        if mmol {
+            formatter.minimumFractionDigits = 1
+            formatter.maximumFractionDigits = 1
+        }
+        if forceSign {
+            formatter.positivePrefix = formatter.plusSign
+        }
+        formatter.roundingMode = .halfUp
+
+        return formatter
+            .string(from: mmol ? value.asMmolL as NSNumber : NSNumber(value: value))!
+    }
+
+    init?(
+        new bg: BloodGlucose,
+        prev: BloodGlucose?,
+        mmol: Bool,
+        chart: [Readings],
+        settings: FreeAPSSettings,
+        suggestion: Suggestion
+    ) {
+        guard let glucose = bg.glucose else {
+            return nil
+        }
+
+        let formattedBG = Self.formatGlucose(glucose, mmol: mmol, forceSign: false)
+
+        var rotationDegrees: Double = 0.0
+
+        switch bg.direction {
+        case .doubleUp,
+             .singleUp,
+             .tripleUp:
+            rotationDegrees = -90
+        case .fortyFiveUp:
+            rotationDegrees = -45
+        case .flat:
+            rotationDegrees = 0
+        case .fortyFiveDown:
+            rotationDegrees = 45
+        case .doubleDown,
+             .singleDown,
+             .tripleDown:
+            rotationDegrees = 90
+        case .notComputable,
+             Optional.none,
+             .rateOutOfRange,
+             .some(.none):
+            rotationDegrees = 0
+        }
+
+        let trendString = bg.direction?.symbol
+
+        let change = prev?.glucose.map({
+            Self.formatGlucose(glucose - $0, mmol: mmol, forceSign: true)
+        }) ?? ""
+
+        let chartBG = chart.map(\.glucose)
+
+        let conversionFactor: Double = settings.units == .mmolL ? 18.0 : 1.0
+        let convertedChartBG = chartBG.map { Double($0) / conversionFactor }
+
+        let chartDate = chart.map(\.date)
+
+        /// glucose limits from UI settings
+        let highGlucose = settings.high / Decimal(conversionFactor)
+        let lowGlucose = settings.low / Decimal(conversionFactor)
+
+        let cob = suggestion.cob ?? 0
+        let iob = suggestion.iob ?? 0
+
+        let lockScreenView = settings.lockScreenView.displayName
+
+        self.init(
+            bg: formattedBG,
+            direction: trendString,
+            change: change,
+            date: bg.dateString,
+            chart: convertedChartBG,
+            chartDate: chartDate,
+            rotationDegrees: rotationDegrees,
+            highGlucose: Double(highGlucose),
+            lowGlucose: Double(lowGlucose),
+            cob: cob,
+            iob: iob,
+            lockScreenView: lockScreenView
+        )
+    }
+}
+
+@available(iOS 16.2, *) private struct ActiveActivity {
+    let activity: Activity<LiveActivityAttributes>
+    let startDate: Date
+
+    func needsRecreation() -> Bool {
+        switch activity.activityState {
+        case .dismissed,
+             .ended,
+             .stale:
+            return true
+        case .active: break
+        @unknown default:
+            return true
+        }
+
+        return -startDate.timeIntervalSinceNow >
+            TimeInterval(60 * 60)
+    }
+}
+
+@available(iOS 16.2, *) final class LiveActivityBridge: Injectable, ObservableObject {
+    @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var storage: FileStorage!
+
+    private let activityAuthorizationInfo = ActivityAuthorizationInfo()
+    @Published private(set) var systemEnabled: Bool
+
+    private var settings: FreeAPSSettings {
+        settingsManager.settings
+    }
+
+    var suggestion: Suggestion? {
+        storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)
+    }
+
+    private var currentActivity: ActiveActivity?
+    private var latestGlucose: BloodGlucose?
+
+    init(resolver: Resolver) {
+        systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
+        injectServices(resolver)
+        broadcaster.register(GlucoseObserver.self, observer: self)
+
+        Foundation.NotificationCenter.default.addObserver(
+            forName: UIApplication.didEnterBackgroundNotification,
+            object: nil,
+            queue: nil
+        ) { _ in
+            self.forceActivityUpdate()
+        }
+
+        Foundation.NotificationCenter.default.addObserver(
+            forName: UIApplication.didBecomeActiveNotification,
+            object: nil,
+            queue: nil
+        ) { _ in
+            self.forceActivityUpdate()
+        }
+
+        monitorForLiveActivityAuthorizationChanges()
+    }
+
+    private func monitorForLiveActivityAuthorizationChanges() {
+        Task {
+            for await activityState in activityAuthorizationInfo.activityEnablementUpdates {
+                if activityState != systemEnabled {
+                    await MainActor.run {
+                        systemEnabled = activityState
+                    }
+                }
+            }
+        }
+    }
+
+    /// 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() {
+        // 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
+    @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 })
+        {
+            await unknownActivity.end(nil, dismissalPolicy: .immediate)
+        }
+
+        if let currentActivity {
+            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)
+            } else {
+                let content = ActivityContent(
+                    state: state,
+                    staleDate: min(state.date, Date.now).addingTimeInterval(TimeInterval(6 * 60))
+                )
+                await currentActivity.activity.update(content)
+            }
+        } else {
+            do {
+                // always push a non-stale content as the first update
+                // pushing a stale content as the frst content results in the activity not being shown at all
+                // we want it shown though even if it is iniially stale, as we expect new BG readings to become available soon, which should then be displayed
+                let nonStale = ActivityContent(
+                    state: LiveActivityAttributes.ContentState(
+                        bg: "--",
+                        direction: nil,
+                        change: "--",
+                        date: Date.now,
+                        chart: [],
+                        chartDate: [],
+                        rotationDegrees: 0,
+                        highGlucose: Double(180),
+                        lowGlucose: Double(70),
+                        cob: 0,
+                        iob: 0,
+                        lockScreenView: "Simple"
+                    ),
+                    staleDate: Date.now.addingTimeInterval(60)
+                )
+
+                let activity = try Activity.request(
+                    attributes: LiveActivityAttributes(startDate: Date.now),
+                    content: nonStale,
+                    pushType: nil
+                )
+                currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
+
+                // then show the actual content
+                await pushUpdate(state)
+            } catch {
+                print("activity creation error: \(error)")
+            }
+        }
+    }
+
+    /// ends all live activities immediateny
+    private func endActivity() async {
+        if let currentActivity {
+            await currentActivity.activity.end(nil, dismissalPolicy: .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]) {
+        guard settings.useLiveActivity else {
+            if currentActivity != nil {
+                Task {
+                    await self.endActivity()
+                }
+            }
+            return
+        }
+
+        // backfill latest glucose if contained in this update
+        if glucose.count > 1 {
+            latestGlucose = glucose[glucose.count - 2]
+        }
+        defer {
+            self.latestGlucose = glucose.last
+        }
+
+        // fetch glucose for chart from Core Data
+        let coreDataStorage = CoreDataStorage()
+        let sixHoursAgo = Calendar.current.date(byAdding: .hour, value: -6, to: Date()) ?? Date()
+        let fetchGlucose = coreDataStorage.fetchGlucose(interval: sixHoursAgo as NSDate)
+
+        guard let bg = glucose.last else {
+            return
+        }
+
+        if let suggestion = suggestion {
+            let content = LiveActivityAttributes.ContentState(
+                new: bg,
+                prev: latestGlucose,
+                mmol: settings.units == .mmolL,
+                chart: fetchGlucose,
+                settings: settings,
+                suggestion: suggestion
+            )
+
+            if let content = content {
+                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
+  }
+}

+ 298 - 0
LiveActivity/LiveActivity.swift

@@ -0,0 +1,298 @@
+import ActivityKit
+import Charts
+import SwiftUI
+import WidgetKit
+
+private enum Size {
+    case minimal
+    case compact
+    case expanded
+}
+
+struct LiveActivity: Widget {
+    private let dateFormatter: DateFormatter = {
+        var f = DateFormatter()
+        f.dateStyle = .none
+        f.timeStyle = .short
+        return f
+    }()
+
+    private var bolusFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 2
+        formatter.decimalSeparator = "."
+        return formatter
+    }
+
+    private var carbsFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        return formatter
+    }
+
+    @ViewBuilder private func changeLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        if !context.state.change.isEmpty {
+            if context.isStale {
+                Text(context.state.change).foregroundStyle(.primary.opacity(0.5))
+                    .strikethrough(pattern: .solid, color: .red.opacity(0.6))
+            } else {
+                Text(context.state.change)
+            }
+        } else {
+            Text("--")
+        }
+    }
+
+    @ViewBuilder func mealLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        VStack(alignment: .leading, spacing: 1, content: {
+            HStack {
+                Text("COB: ").font(.caption)
+                Text(
+                    (carbsFormatter.string(from: context.state.cob as NSNumber) ?? "--") +
+                        NSLocalizedString(" g", comment: "grams of carbs")
+                ).font(.caption).fontWeight(.bold)
+            }
+            HStack {
+                Text("IOB: ").font(.caption)
+                Text(
+                    (bolusFormatter.string(from: context.state.iob as NSNumber) ?? "--") +
+                        NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
+                ).font(.caption).fontWeight(.bold)
+            }
+        })
+    }
+
+    @ViewBuilder func trend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        if context.isStale {
+            Text("--")
+        } else {
+            if let trendSystemImage = context.state.direction {
+                Image(systemName: trendSystemImage)
+            }
+        }
+    }
+
+    private func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
+        let text = Text("Updated: \(dateFormatter.string(from: context.state.date))")
+        if context.isStale {
+            if #available(iOSApplicationExtension 17.0, *) {
+                return text.bold().foregroundStyle(.red)
+            } else {
+                return text.bold().foregroundColor(.red)
+            }
+        } else {
+            return text
+        }
+    }
+
+    private func bgLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
+        Text(context.state.bg)
+            .fontWeight(.bold)
+            .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+    }
+
+    private func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>, size: Size) -> (some View, Int) {
+        var characters = 0
+
+        let bgText = context.state.bg
+        characters += bgText.count
+
+        // narrow mode is for the minimal dynamic island view
+        // there is not enough space to show all three arrow there
+        // and everything has to be squeezed together to some degree
+        // only display the first arrow character and make it red in case there were more characters
+        var directionText: String?
+        var warnColor: Color?
+        if let direction = context.state.direction {
+            if size == .compact {
+                directionText = String(direction[direction.startIndex ... direction.startIndex])
+
+                if direction.count > 1 {
+                    warnColor = Color.red
+                }
+            } else {
+                directionText = direction
+            }
+
+            characters += directionText!.count
+        }
+
+        let spacing: CGFloat
+        switch size {
+        case .minimal: spacing = -1
+        case .compact: spacing = 0
+        case .expanded: spacing = 3
+        }
+
+        let stack = HStack(spacing: spacing) {
+            Text(bgText)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+            if let direction = directionText {
+                let text = Text(direction)
+                switch size {
+                case .minimal:
+                    let scaledText = text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading)
+                    if let warnColor {
+                        scaledText.foregroundStyle(warnColor)
+                    } else {
+                        scaledText
+                    }
+                case .compact:
+                    text.scaleEffect(x: 0.8, y: 0.8, anchor: .leading).padding(.trailing, -3)
+
+                case .expanded:
+                    text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading).padding(.trailing, -5)
+                }
+            }
+        }
+        .foregroundStyle(
+            context.state.lockScreenView == "Simple" ? (context.isStale ? Color.primary.opacity(0.5) : Color.primary) :
+                (context.isStale ? Color.white.opacity(0.5) : Color.white)
+        )
+
+        return (stack, characters)
+    }
+
+    @ViewBuilder func chart(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        if context.isStale {
+            Text("No data available")
+        } else {
+            Chart {
+                ForEach(context.state.chart.indices, id: \.self) { index in
+                    let currentValue = context.state.chart[index]
+                    if currentValue > context.state.highGlucose {
+                        PointMark(
+                            x: .value("Time", context.state.chartDate[index] ?? Date()),
+                            y: .value("Value", currentValue)
+                        ).foregroundStyle(Color.orange.gradient).symbolSize(12)
+                    } else if currentValue < context.state.lowGlucose {
+                        PointMark(
+                            x: .value("Time", context.state.chartDate[index] ?? Date()),
+                            y: .value("Value", currentValue)
+                        ).foregroundStyle(Color.red.gradient).symbolSize(12)
+                    } else {
+                        PointMark(
+                            x: .value("Time", context.state.chartDate[index] ?? Date()),
+                            y: .value("Value", currentValue)
+                        ).foregroundStyle(Color.green.gradient).symbolSize(12)
+                    }
+                }
+            }.chartPlotStyle { plotContent in
+                plotContent.background(.cyan.opacity(0.1))
+            }
+            .chartYAxis {
+                AxisMarks(position: .leading) { _ in
+                    AxisValueLabel().foregroundStyle(Color.white)
+                    AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])).foregroundStyle(Color.white)
+                }
+            }
+            .chartXAxis {
+                AxisMarks(position: .automatic) { _ in
+                    AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                        .foregroundStyle(Color.white)
+                    AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])).foregroundStyle(Color.white)
+                }
+            }
+        }
+    }
+
+    var body: some WidgetConfiguration {
+        ActivityConfiguration(for: LiveActivityAttributes.self) { context in
+            // Lock screen/banner UI goes here
+            if context.state.lockScreenView == "Simple" {
+                HStack(spacing: 3) {
+                    bgAndTrend(context: context, size: .expanded).0.font(.title)
+                    Spacer()
+                    VStack(alignment: .trailing, spacing: 5) {
+                        changeLabel(context: context).font(.title3)
+                        updatedLabel(context: context).font(.caption).foregroundStyle(.primary.opacity(0.7))
+                    }
+                }
+                .privacySensitive()
+                .padding(.all, 15)
+                // Semantic BackgroundStyle and Color values work here. They adapt to the given interface style (light mode, dark mode)
+                // Semantic UIColors do NOT (as of iOS 17.1.1). Like UIColor.systemBackgroundColor (it does not adapt to changes of the interface style)
+                // The colorScheme environment varaible that is usually used to detect dark mode does NOT work here (it reports false values)
+                .foregroundStyle(Color.primary)
+                .background(BackgroundStyle.background.opacity(0.4))
+                .activityBackgroundTint(Color.clear)
+            } else {
+                HStack(spacing: 2) {
+                    VStack {
+                        chart(context: context).frame(width: UIScreen.main.bounds.width / 1.8)
+                    }.padding(.all, 15)
+                    Divider().foregroundStyle(Color.white)
+                    VStack(alignment: .center) {
+                        Spacer()
+                        ZStack {
+                            VStack {
+                                bgAndTrend(context: context, size: .expanded).0.font(.largeTitle)
+                                changeLabel(context: context).font(.callout)
+                            }.frame(width: 130, height: 130)
+                        }.scaleEffect(0.85).offset(y: 30)
+                        mealLabel(context: context).padding(.bottom, 8)
+                        updatedLabel(context: context).font(.caption).padding(.bottom, 70)
+                    }
+                }
+                .privacySensitive()
+                .imageScale(.small)
+                .background(Color.white.opacity(0.2))
+                .foregroundColor(Color.white)
+                .activityBackgroundTint(Color.black.opacity(0.7))
+                .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) {
+                    bgAndTrend(context: context, size: .expanded).0.font(.title2).padding(.leading, 5)
+                }
+                DynamicIslandExpandedRegion(.trailing) {
+                    changeLabel(context: context).font(.title2).padding(.trailing, 5)
+                }
+                DynamicIslandExpandedRegion(.bottom) {
+                    if context.state.lockScreenView == "Simple" {
+                        Group {
+                            updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
+                        }
+                        .frame(
+                            maxHeight: .infinity,
+                            alignment: .bottom
+                        )
+                    } else {
+                        chart(context: context)
+                    }
+                }
+                DynamicIslandExpandedRegion(.center) {
+                    if context.state.lockScreenView == "Detailed" {
+                        updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
+                    }
+                }
+            } compactLeading: {
+                bgAndTrend(context: context, size: .compact).0.padding(.leading, 4)
+            } compactTrailing: {
+                changeLabel(context: context).padding(.trailing, 4)
+            } minimal: {
+                let (_label, characterCount) = bgAndTrend(context: context, size: .minimal)
+
+                let label = _label.padding(.leading, 7).padding(.trailing, 3)
+
+                if characterCount < 4 {
+                    label
+                } else if characterCount < 5 {
+                    label.fontWidth(.condensed)
+                } else {
+                    label.fontWidth(.compressed)
+                }
+            }
+            .widgetURL(URL(string: "freeaps-x://"))
+            .keylineTint(Color.purple)
+            .contentMargins(.horizontal, 0, for: .minimal)
+            .contentMargins(.trailing, 0, for: .compactLeading)
+            .contentMargins(.leading, 0, for: .compactTrailing)
+        }
+    }
+}

+ 8 - 0
LiveActivity/LiveActivityBundle.swift

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

+ 66 - 0
LiveActivity/WidgetBobble 2.swift

@@ -0,0 +1,66 @@
+import SwiftUI
+
+struct WidgetBobble: View {
+    @Environment(\.colorScheme) var colorScheme
+
+    let gradient: AngularGradient
+    let color: Color
+
+    var body: some View {
+        HStack(alignment: .center) {
+            ZStack {
+                Group {
+                    CircleShapeWidget(gradient: gradient)
+                    TriangleShapeWidget(color: color)
+                }
+                CircleShapeWidget(gradient: gradient)
+            }
+        }
+    }
+}
+
+struct CircleShapeWidget: View {
+    @Environment(\.colorScheme) var colorScheme
+
+    let gradient: AngularGradient
+
+    var body: some View {
+//        let colorBackground: Color = colorScheme == .dark ? Color(
+//            red: 0.05490196078,
+//            green: 0.05490196078,
+//            blue: 0.05490196078
+//        ) : .white
+
+        Circle()
+            .stroke(gradient, lineWidth: 10)
+            .background(Circle().fill(.clear))
+            .frame(width: 130, height: 130)
+    }
+}
+
+struct TriangleShapeWidget: View {
+    let color: Color
+
+    var body: some View {
+        TriangleWidget()
+            .fill(color)
+            .frame(width: 35, height: 35)
+            .rotationEffect(.degrees(90))
+            .offset(x: 78)
+    }
+}
+
+struct TriangleWidget: Shape {
+    func path(in rect: CGRect) -> Path {
+        var path = Path()
+
+        let cornerRadius: CGFloat = 2
+
+        path.move(to: CGPoint(x: rect.midX, y: rect.minY))
+        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
+        path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - cornerRadius), control: CGPoint(x: rect.midX, y: rect.maxY))
+        path.closeSubpath()
+
+        return path
+    }
+}

+ 13 - 1
fastlane/Fastfile

@@ -84,7 +84,8 @@ platform :ios do
       app_identifier: [
         "#{BUNDLE_ID}",
         "#{BUNDLE_ID}.watchkitapp",
-        "#{BUNDLE_ID}.watchkitapp.watchkitextension"
+        "#{BUNDLE_ID}.watchkitapp.watchkitextension",
+        "#{BUNDLE_ID}.FreeAPS.LiveActivity"
       ]
     )
 
@@ -124,6 +125,12 @@ platform :ios do
       code_sign_identity: "iPhone Distribution",
       targets: ["FreeAPSWatch"]
     )
+    update_code_signing_settings(
+      path: "#{GITHUB_WORKSPACE}/FreeAPS.xcodeproj",
+      profile_name: mapping["#{BUNDLE_ID}.LiveActivity"],
+      code_sign_identity: "iPhone Distribution",
+      targets: ["LiveActivityExtension"]
+    )
 
     gym(
       export_method: "app-store",
@@ -189,6 +196,10 @@ platform :ios do
     configure_bundle_id("FreeAPSWatch", "#{BUNDLE_ID}.watchkitapp", [
       Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS
     ])
+
+    configure_bundle_id("LiveActivityExtension", "#{BUNDLE_ID}.LiveActivity", [
+      Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS
+    ])
     
   end
 
@@ -212,6 +223,7 @@ platform :ios do
         "#{BUNDLE_ID}",
         "#{BUNDLE_ID}.watchkitapp.watchkitextension",
         "#{BUNDLE_ID}.watchkitapp",
+        "#{BUNDLE_ID}.LiveActivity"
       ]
     )
   end