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