diff --git a/.github/workflows/create_certs.yml b/.github/workflows/create_certs.yml
index d418b51a2..2db64e50b 100644
--- a/.github/workflows/create_certs.yml
+++ b/.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
diff --git a/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents b/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
index 4b6271da7..575d66a48 100644
--- a/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
+++ b/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
@@ -105,6 +105,7 @@
+
diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj
index 038ce58df..05d9bd385 100644
--- a/FreeAPS.xcodeproj/project.pbxproj
+++ b/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 */; };
@@ -271,8 +272,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 */; };
@@ -301,6 +311,7 @@
BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500371C09F54F89A97D65FDB /* CalibrationsRootView.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 */; };
@@ -424,6 +435,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 +499,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 */
@@ -521,6 +550,7 @@
193F1E3B2B44C14800525770 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/InfoPlist.strings; sourceTree = ""; };
193F1E3C2B44C14800525770 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; };
193F6CDC2A512C8F001240FD /* Loops.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loops.swift; sourceTree = ""; };
+ 1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStorage.swift; sourceTree = ""; };
1967DFBD29D052C200759F30 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = ""; };
1967DFBF29D053AC00759F30 /* IconSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelection.swift; sourceTree = ""; };
1967DFC129D053D300759F30 /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = ""; };
@@ -792,6 +822,16 @@
66A5B83E7967C38F7CBD883C /* LibreConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigDataFlow.swift; sourceTree = ""; };
67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.swift; sourceTree = ""; };
680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalProvider.swift; sourceTree = ""; };
+ 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 = ""; };
+ 6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivity.swift; sourceTree = ""; };
+ 6B1A8D232B14D91700E76752 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 6B1A8D252B14D91700E76752 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = ""; };
+ 6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyShared.swift; sourceTree = ""; };
6F8BA8533F56BC55748CA877 /* PreferencesEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorProvider.swift; sourceTree = ""; };
72778B68C3004F71F6E79BDC /* PumpSettingsEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorStateModel.swift; sourceTree = ""; };
79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorDataFlow.swift; sourceTree = ""; };
@@ -821,6 +861,7 @@
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 = ""; };
BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = ""; };
+ BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenView.swift; sourceTree = ""; };
BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = ""; };
C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = ""; };
C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = ""; };
@@ -961,6 +1002,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 +1341,7 @@
3811DE9125C9D88200A708ED /* Services */ = {
isa = PBXGroup;
children = (
+ 6B1A8D2C2B156EC100E76752 /* LiveActivity */,
CEB434E128B8F9BC00B70274 /* Bluetooth */,
F90692A8274B7A980037068D /* HealthKit */,
38E8754D275556E100975559 /* WatchManager */,
@@ -1458,6 +1509,9 @@
3818AA56274C26A300843DB3 /* RileyLinkKit.framework */,
3818AA57274C26A300843DB3 /* RileyLinkKitUI.framework */,
3818AA49274C267000843DB3 /* CGMBLEKit.framework */,
+ 6B1A8D012B14D88B00E76752 /* UniformTypeIdentifiers.framework */,
+ 6B1A8D182B14D91600E76752 /* WidgetKit.framework */,
+ 6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -1538,6 +1592,7 @@
3818AA44274C229000843DB3 /* Packages */,
38E8751D27554D5500975559 /* FreeAPSWatch */,
38E8752827554D5700975559 /* FreeAPSWatch WatchKit Extension */,
+ 6B1A8D1C2B14D91600E76752 /* LiveActivity */,
388E595925AD948C0019842D /* Products */,
3818AA48274C267000843DB3 /* Frameworks */,
192F0FF5276AC36D0085BE4D /* Recovered References */,
@@ -1551,6 +1606,7 @@
38FCF3ED25E9028E0078B0D1 /* FreeAPSTests.xctest */,
38E8751C27554D5500975559 /* FreeAPSWatch.app */,
38E8752427554D5700975559 /* FreeAPSWatch WatchKit Extension.appex */,
+ 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */,
);
name = Products;
sourceTree = "";
@@ -1616,6 +1672,7 @@
19A910352A24D6D700C8951B /* DateFilter.swift */,
193F6CDC2A512C8F001240FD /* Loops.swift */,
CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */,
+ BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */,
);
path = Models;
sourceTree = "";
@@ -1661,6 +1718,7 @@
38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */,
38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */,
CE82E02428E867BA00473A9C /* AlertStorage.swift */,
+ 1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */,
);
path = Storage;
sourceTree = "";
@@ -1956,6 +2014,26 @@
path = AutotuneConfig;
sourceTree = "";
};
+ 6B1A8D1C2B14D91600E76752 /* LiveActivity */ = {
+ isa = PBXGroup;
+ children = (
+ 6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */,
+ 6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */,
+ 6B1A8D232B14D91700E76752 /* Assets.xcassets */,
+ 6B1A8D252B14D91700E76752 /* Info.plist */,
+ );
+ path = LiveActivity;
+ sourceTree = "";
+ };
+ 6B1A8D2C2B156EC100E76752 /* LiveActivity */ = {
+ isa = PBXGroup;
+ children = (
+ 6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */,
+ 6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */,
+ );
+ path = LiveActivity;
+ sourceTree = "";
+ };
6DC5D590658EF8B8DF94F9F5 /* AddCarbs */ = {
isa = PBXGroup;
children = (
@@ -2272,11 +2350,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 +2426,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 +2512,7 @@
38FCF3EC25E9028E0078B0D1 /* FreeAPSTests */,
38E8751B27554D5500975559 /* FreeAPSWatch */,
38E8752327554D5700975559 /* FreeAPSWatch WatchKit Extension */,
+ 6B1A8D162B14D91500E76752 /* LiveActivityExtension */,
);
};
/* End PBXProject section */
@@ -2462,6 +2559,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 6B1A8D152B14D91500E76752 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -2523,6 +2628,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 */,
@@ -2647,6 +2753,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 +2899,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 */,
@@ -2807,6 +2915,7 @@
E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */,
BA90041DC8991147E5C8C3AA /* CalibrationsRootView.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 */,
@@ -2849,6 +2958,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 +2986,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 +3457,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 */
@@ -3381,6 +3572,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 */
diff --git a/FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 2cfe78ebf..663d0612e 100644
--- a/FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/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
}
diff --git a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
index bec8a8989..db6a7a041 100644
--- a/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
+++ b/FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
@@ -40,5 +40,6 @@
"oneDimensionalGraph" : false,
"rulerMarks" : false,
"maxCarbs": 1000,
- "displayFatAndProteinOnWatch": false
+ "displayFatAndProteinOnWatch": false,
+ "lockScreenView": "simple"
}
diff --git a/FreeAPS/Sources/APS/Storage/CoreDataStorage.swift b/FreeAPS/Sources/APS/Storage/CoreDataStorage.swift
new file mode 100644
index 000000000..c0caa2fd7
--- /dev/null
+++ b/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
+ 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
+ let sortOverride = NSSortDescriptor(key: "date", ascending: false)
+ requestOverrides.sortDescriptors = [sortOverride]
+ requestOverrides.fetchLimit = 1
+ try? overrideArray = self.coredataContext.fetch(requestOverrides)
+ }
+ return overrideArray
+ }
+}
diff --git a/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift b/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
index 567aa71bc..35d27dbcd 100644
--- a/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
+++ b/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()
}
}
diff --git a/FreeAPS/Sources/Application/FreeAPSApp.swift b/FreeAPS/Sources/Application/FreeAPSApp.swift
index 907e4f010..39516ed5b 100644
--- a/FreeAPS/Sources/Application/FreeAPSApp.swift
+++ b/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() {
diff --git a/FreeAPS/Sources/Assemblies/ServiceAssembly.swift b/FreeAPS/Sources/Assemblies/ServiceAssembly.swift
index 778292731..f5a49697b 100644
--- a/FreeAPS/Sources/Assemblies/ServiceAssembly.swift
+++ b/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)
+ }
+ }
}
}
diff --git a/FreeAPS/Sources/Models/DateFilter.swift b/FreeAPS/Sources/Models/DateFilter.swift
index 42a8b7dbe..466b699f9 100644
--- a/FreeAPS/Sources/Models/DateFilter.swift
+++ b/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
diff --git a/FreeAPS/Sources/Models/FreeAPSSettings.swift b/FreeAPS/Sources/Models/FreeAPSSettings.swift
index 1c9a07c2c..6cc952f2e 100644
--- a/FreeAPS/Sources/Models/FreeAPSSettings.swift
+++ b/FreeAPS/Sources/Models/FreeAPSSettings.swift
@@ -43,6 +43,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 {
@@ -224,6 +226,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
}
}
diff --git a/FreeAPS/Sources/Models/LockScreenView.swift b/FreeAPS/Sources/Models/LockScreenView.swift
new file mode 100644
index 000000000..ac1bf8301
--- /dev/null
+++ b/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: "")
+ }
+ }
+}
diff --git a/FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigStateModel.swift b/FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigStateModel.swift
index abf82c5fe..121aa748e 100644
--- a/FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigStateModel.swift
+++ b/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
diff --git a/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift b/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift
index 8e2717c72..e1af19df3 100644
--- a/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift
+++ b/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)
}
}
}
diff --git a/FreeAPS/Sources/Services/LiveActivity/LiveActitiyShared.swift b/FreeAPS/Sources/Services/LiveActivity/LiveActitiyShared.swift
new file mode 100644
index 000000000..ad65a4804
--- /dev/null
+++ b/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
+}
diff --git a/FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift b/FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
new file mode 100644
index 000000000..dfe355056
--- /dev/null
+++ b/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
+ 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.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.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)
+ }
+ }
+ }
+ }
+}
diff --git a/LiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json b/LiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 000000000..eb8789700
--- /dev/null
+++ b/LiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/LiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json b/LiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..13613e3ee
--- /dev/null
+++ b/LiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/LiveActivity/Assets.xcassets/Contents.json b/LiveActivity/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/LiveActivity/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/LiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json b/LiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json
new file mode 100644
index 000000000..eb8789700
--- /dev/null
+++ b/LiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/LiveActivity/LiveActivity.swift b/LiveActivity/LiveActivity.swift
new file mode 100644
index 000000000..225e8eac7
--- /dev/null
+++ b/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) -> 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) -> 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) -> some View {
+ if context.isStale {
+ Text("--")
+ } else {
+ if let trendSystemImage = context.state.direction {
+ Image(systemName: trendSystemImage)
+ }
+ }
+ }
+
+ private func updatedLabel(context: ActivityViewContext) -> 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) -> Text {
+ Text(context.state.bg)
+ .fontWeight(.bold)
+ .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+ }
+
+ private func bgAndTrend(context: ActivityViewContext, 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) -> 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)
+ }
+ }
+}
diff --git a/LiveActivity/LiveActivityBundle.swift b/LiveActivity/LiveActivityBundle.swift
new file mode 100644
index 000000000..3a9ae4b64
--- /dev/null
+++ b/LiveActivity/LiveActivityBundle.swift
@@ -0,0 +1,8 @@
+import SwiftUI
+import WidgetKit
+
+@main struct LiveActivityBundle: WidgetBundle {
+ var body: some Widget {
+ LiveActivity()
+ }
+}
diff --git a/LiveActivity/WidgetBobble 2.swift b/LiveActivity/WidgetBobble 2.swift
new file mode 100644
index 000000000..17e5fefe5
--- /dev/null
+++ b/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
+ }
+}
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 171acb86e..ed1e88bb9 100644
--- a/fastlane/Fastfile
+++ b/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}.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