From f8d4c137bd8bba709ad67635cf83f1f1029b0f1e Mon Sep 17 00:00:00 2001 From: skimaniac <44727542+skimaniac@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:14:08 -0400 Subject: [PATCH 1/3] Add App Intents to enable/disable BG speech via Shortcuts --- LoopFollow.xcodeproj/project.pbxproj | 26 +++++++++++-------- LoopFollow/Controllers/SpeakBGIntents.swift | 24 +++++++++++++++++ .../RestartLiveActivityIntent.swift | 14 +++++++++- 3 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 LoopFollow/Controllers/SpeakBGIntents.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 560cd2f8e..36c627e5d 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -97,6 +97,10 @@ exceptions = ( D736FE5E8494B2581A4EA95F /* Exceptions for "LoopFollow" folder in "LoopFollow" target */, ); + explicitFileTypes = { + }; + explicitFolders = ( + ); path = LoopFollow; sourceTree = ""; }; @@ -104,6 +108,10 @@ isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( ); + explicitFileTypes = { + }; + explicitFolders = ( + ); path = Shared; sourceTree = ""; }; @@ -258,8 +266,8 @@ isa = PBXNativeTarget; buildConfigurationList = FC97882D2485969C00A7906C /* Build configuration list for PBXNativeTarget "LoopFollow" */; buildPhases = ( - DD7F4BEA2DD48B9600D449E9 /* Swiftformat */, B038D39450A1F9A97D2B8BA4 /* [CP] Check Pods Manifest.lock */, + DD7F4BEA2DD48B9600D449E9 /* Swiftformat */, FC9788102485969B00A7906C /* Sources */, FC9788112485969B00A7906C /* Frameworks */, FC9788122485969B00A7906C /* Resources */, @@ -366,14 +374,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh\"\n"; @@ -490,7 +494,7 @@ CODE_SIGN_ENTITLEMENTS = LoopFollowLAExtensionExtension.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = 4MFAGT7CVA; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -542,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = LoopFollowLAExtensionExtension.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = 4MFAGT7CVA; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -591,7 +595,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = 4MFAGT7CVA; ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -622,7 +626,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = 4MFAGT7CVA; ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -772,7 +776,7 @@ CODE_SIGN_ENTITLEMENTS = "LoopFollow/Loop Follow.entitlements"; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = 4MFAGT7CVA; INFOPLIST_FILE = LoopFollow/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -797,7 +801,7 @@ CODE_SIGN_ENTITLEMENTS = "LoopFollow/Loop Follow.entitlements"; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = 4MFAGT7CVA; INFOPLIST_FILE = LoopFollow/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/LoopFollow/Controllers/SpeakBGIntents.swift b/LoopFollow/Controllers/SpeakBGIntents.swift new file mode 100644 index 000000000..575e0323a --- /dev/null +++ b/LoopFollow/Controllers/SpeakBGIntents.swift @@ -0,0 +1,24 @@ +// LoopFollow +// SpeakBGIntents.swift + +import AppIntents + +struct EnableSpeakBGIntent: AppIntent { + static var title: LocalizedStringResource = "Enable BG Speech" + static var description = IntentDescription("Turns on spoken glucose readings in LoopFollow.") + + func perform() async throws -> some IntentResult & ProvidesDialog { + Storage.shared.speakBG.value = true + return .result(dialog: "BG speech enabled.") + } +} + +struct DisableSpeakBGIntent: AppIntent { + static var title: LocalizedStringResource = "Disable BG Speech" + static var description = IntentDescription("Turns off spoken glucose readings in LoopFollow.") + + func perform() async throws -> some IntentResult & ProvidesDialog { + Storage.shared.speakBG.value = false + return .result(dialog: "BG speech disabled.") + } +} diff --git a/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift b/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift index e92ee5778..fb50fee15 100644 --- a/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift +++ b/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift @@ -32,7 +32,7 @@ } } - struct LoopFollowAppShortcuts: AppShortcutsProvider { +struct LoopFollowAppShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { AppShortcut( intent: RestartLiveActivityIntent(), @@ -40,6 +40,18 @@ shortTitle: "Restart Live Activity", systemImageName: "dot.radiowaves.left.and.right" ) + AppShortcut( + intent: EnableSpeakBGIntent(), + phrases: ["Enable BG speech in \(.applicationName)"], + shortTitle: "Enable BG Speech", + systemImageName: "speaker.wave.2" + ) + AppShortcut( + intent: DisableSpeakBGIntent(), + phrases: ["Disable BG speech in \(.applicationName)"], + shortTitle: "Disable BG Speech", + systemImageName: "speaker.slash" + ) } } #endif From bbe8ca102693568d8d3f2e9c3656dba4162eecda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 17 Sep 2026 10:10:07 +0200 Subject: [PATCH 2/3] Keep project signing settings on the shared team variable --- LoopFollow.xcodeproj/project.pbxproj | 26 ++++++++----------- .../RestartLiveActivityIntent.swift | 2 +- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 36c627e5d..560cd2f8e 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -97,10 +97,6 @@ exceptions = ( D736FE5E8494B2581A4EA95F /* Exceptions for "LoopFollow" folder in "LoopFollow" target */, ); - explicitFileTypes = { - }; - explicitFolders = ( - ); path = LoopFollow; sourceTree = ""; }; @@ -108,10 +104,6 @@ isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( ); - explicitFileTypes = { - }; - explicitFolders = ( - ); path = Shared; sourceTree = ""; }; @@ -266,8 +258,8 @@ isa = PBXNativeTarget; buildConfigurationList = FC97882D2485969C00A7906C /* Build configuration list for PBXNativeTarget "LoopFollow" */; buildPhases = ( - B038D39450A1F9A97D2B8BA4 /* [CP] Check Pods Manifest.lock */, DD7F4BEA2DD48B9600D449E9 /* Swiftformat */, + B038D39450A1F9A97D2B8BA4 /* [CP] Check Pods Manifest.lock */, FC9788102485969B00A7906C /* Sources */, FC9788112485969B00A7906C /* Frameworks */, FC9788122485969B00A7906C /* Resources */, @@ -374,10 +366,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh\"\n"; @@ -494,7 +490,7 @@ CODE_SIGN_ENTITLEMENTS = LoopFollowLAExtensionExtension.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4MFAGT7CVA; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -546,7 +542,7 @@ CODE_SIGN_ENTITLEMENTS = LoopFollowLAExtensionExtension.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4MFAGT7CVA; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -595,7 +591,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4MFAGT7CVA; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -626,7 +622,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4MFAGT7CVA; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -776,7 +772,7 @@ CODE_SIGN_ENTITLEMENTS = "LoopFollow/Loop Follow.entitlements"; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 4MFAGT7CVA; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; INFOPLIST_FILE = LoopFollow/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -801,7 +797,7 @@ CODE_SIGN_ENTITLEMENTS = "LoopFollow/Loop Follow.entitlements"; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 4MFAGT7CVA; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; INFOPLIST_FILE = LoopFollow/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift b/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift index fb50fee15..3a89d6ebe 100644 --- a/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift +++ b/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift @@ -32,7 +32,7 @@ } } -struct LoopFollowAppShortcuts: AppShortcutsProvider { + struct LoopFollowAppShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { AppShortcut( intent: RestartLiveActivityIntent(), From 81786170446bd8622d1cdcd5428e240b316543b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 17 Sep 2026 10:19:32 +0200 Subject: [PATCH 3/3] Run Speak BG intents on the main actor, check storage readiness, and use the Speak BG name --- LoopFollow/Controllers/SpeakBGIntents.swift | 41 +++++++++++++++---- .../RestartLiveActivityIntent.swift | 8 ++-- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/LoopFollow/Controllers/SpeakBGIntents.swift b/LoopFollow/Controllers/SpeakBGIntents.swift index 575e0323a..995df2071 100644 --- a/LoopFollow/Controllers/SpeakBGIntents.swift +++ b/LoopFollow/Controllers/SpeakBGIntents.swift @@ -4,21 +4,46 @@ import AppIntents struct EnableSpeakBGIntent: AppIntent { - static var title: LocalizedStringResource = "Enable BG Speech" - static var description = IntentDescription("Turns on spoken glucose readings in LoopFollow.") + static var title: LocalizedStringResource = "Turn On Speak BG" + static var description = IntentDescription("Turns on Speak BG so LoopFollow reads glucose values aloud.") + @MainActor func perform() async throws -> some IntentResult & ProvidesDialog { - Storage.shared.speakBG.value = true - return .result(dialog: "BG speech enabled.") + try SpeakBGIntentSupport.set(true) + return .result(dialog: "Speak BG is now on.") } } struct DisableSpeakBGIntent: AppIntent { - static var title: LocalizedStringResource = "Disable BG Speech" - static var description = IntentDescription("Turns off spoken glucose readings in LoopFollow.") + static var title: LocalizedStringResource = "Turn Off Speak BG" + static var description = IntentDescription("Turns off Speak BG so LoopFollow stops reading glucose values aloud.") + @MainActor func perform() async throws -> some IntentResult & ProvidesDialog { - Storage.shared.speakBG.value = false - return .result(dialog: "BG speech disabled.") + try SpeakBGIntentSupport.set(false) + return .result(dialog: "Speak BG is now off.") + } +} + +enum SpeakBGIntentSupport { + /// Storage writes are memory-only during a suspected before-first-unlock + /// launch, so the change would be lost on hydration. + @MainActor + static func set(_ enabled: Bool) throws { + guard !StorageReadiness.isSuppressingWrites else { + throw SpeakBGIntentError.storageUnavailable + } + Storage.shared.speakBG.value = enabled + } +} + +enum SpeakBGIntentError: Error, CustomLocalizedStringResourceConvertible { + case storageUnavailable + + var localizedStringResource: LocalizedStringResource { + switch self { + case .storageUnavailable: + return "LoopFollow can't change Speak BG until your iPhone has been unlocked once after restarting." + } } } diff --git a/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift b/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift index 3a89d6ebe..5a25afdae 100644 --- a/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift +++ b/LoopFollow/LiveActivity/RestartLiveActivityIntent.swift @@ -42,14 +42,14 @@ ) AppShortcut( intent: EnableSpeakBGIntent(), - phrases: ["Enable BG speech in \(.applicationName)"], - shortTitle: "Enable BG Speech", + phrases: ["Turn on Speak BG in \(.applicationName)"], + shortTitle: "Turn On Speak BG", systemImageName: "speaker.wave.2" ) AppShortcut( intent: DisableSpeakBGIntent(), - phrases: ["Disable BG speech in \(.applicationName)"], - shortTitle: "Disable BG Speech", + phrases: ["Turn off Speak BG in \(.applicationName)"], + shortTitle: "Turn Off Speak BG", systemImageName: "speaker.slash" ) }