feat: Live Activity including Lock screen + Dynamic Island, APNs self-push#534
feat: Live Activity including Lock screen + Dynamic Island, APNs self-push#534MtlPhil wants to merge 5 commits intoloopandlearn:devfrom
Conversation
…f-push) Implements a lock screen and Dynamic Island Live Activity for LoopFollow displaying real-time glucose data updated via APNs self-push. ## What's included - Lock screen card: glucose + trend arrow, delta, IOB, COB, projected, last update time, threshold-driven background color (green/orange/red) - Dynamic Island: compact, expanded, and minimal presentations - Not Looping overlay: red banner when Loop hasn't reported in 15+ min - APNs self-push: app sends push to itself for reliable background updates without interference from background audio session - Single source of truth: all data flows from Storage/Observable - Source-agnostic: IOB/COB/projected are optional, safe for Dexcom-only users - Dynamic App Group ID: derived from bundle identifier, no hardcoded team IDs - APNs key injected via xcconfig/Info.plist — never bundled, never committed ## Files added - LoopFollow/LiveActivity/: APNSClient, APNSJWTGenerator, AppGroupID, GlucoseLiveActivityAttributes, GlucoseSnapshot, GlucoseSnapshotBuilder, GlucoseSnapshotStore, GlucoseUnitConversion, LAAppGroupSettings, LAThresholdSync, LiveActivityManager, PreferredGlucoseUnit, StorageCurrentGlucoseStateProvider - LoopFollowLAExtension/: LoopFollowLiveActivity, LoopFollowLABundle - docs/LiveActivity.md (architecture + APNs setup guide) ## Files modified - Storage: added lastBgReadingTimeSeconds, lastDeltaMgdl, lastTrendCode, lastIOB, lastCOB, projectedBgMgdl - Observable: added isNotLooping - BGData, DeviceStatusLoop, DeviceStatusOpenAPS: write canonical values to Storage - DeviceStatus: write isNotLooping to Observable - BackgroundTaskAudio: cleanup - MainViewController: wired LiveActivityManager.refreshFromCurrentState() - Info.plist: added APNSKeyID, APNSTeamID, APNSKeyContent build settings - fastlane/Fastfile: added extension App ID and provisioning profile - build_LoopFollow.yml: inject APNs key from GitHub secret
|
Thanks for the updated PR! I'll have a look. |
Code ReviewAgain, thanks for the updated PR and documentation. It seems the APNs self-push approach is the correct solution for the background audio session limitation so I think we should follow that path. A few things need to be addressed before we can proceed with testing further review. Critical — Must fix1. Hardcoded Team ID and Bundle Identifiers The PR description states "No Hardcoded Identifiers", but the diff replaces dynamic variables with a personal team ID (
These must all use the existing dynamic variable pattern ( 2. Removed iPad and Mac Catalyst support The PR silently changes platform support settings that are unrelated to Live Activity:
These should be reverted to their original values. 3. Info.plist:
LoopFollow has the app_suffix in order so support multiple apps running at the same time, they should each have their own LiveActivity. Important — Should fix4. Both direct
Minor — Nice to fix5. This file has cosmetic changes (reordered copyright header, |
|
Ready to review again. Two issues came up overnight (other than what came out of your PR review):
|
|
Thanks, I think we are getting close! There are still a few things I'd like to clean up — hardcoded team IDs, some Xcode artifacts, and minor diff noise. Would you be ok if I take it from here? I can do the final fixes a bit faster than going back and forth :) Does your PR allow edits from maintainers? Otherwise I can create a new branch at loopandlearn from where we are. |
|
No issue at all.
Where are the hard-coded references? Could have sworn the greps came back
blank!
I’m sorry about the diff noise - made a couple of attempts to fix the
background audio blocker, guessing I didn’t catch all of them. Should have
diffed every file not related to the LA before opening the PR.
I’ll check the repo when I get home tonight. No issue giving you access.
Anyone else ?
Thanks for your patience !
…On Mon, Mar 9, 2026 at 15:33 Jonas Björkert ***@***.***> wrote:
*bjorkert* left a comment (loopandlearn/LoopFollow#534)
<#534 (comment)>
Thanks, I think we are getting close! There are still a few things I'd
like to clean up — hardcoded team IDs, some Xcode artifacts, and minor diff
noise. Would you be ok if I take it from here? I can do the final fixes a
bit faster than going back and forth :) Does your PR allow edits from
maintainers? Otherwise I can create a new branch at loopandlearn from where
we are.
—
Reply to this email directly, view it on GitHub
<#534 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ASINOG7IOLADWS3Q2Q5VYJ34P4MBRAVCNFSM6AAAAACWKTM72KVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DAMRWGI2DGNBTHA>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
Not sure how to fix the remaining hardcoded bundleIDs. I can remove them in project.pbxproj, but when I rebuild in Xcode and add a team, the values get hard-coded again. |
|
No worries, I'll se what I can do and I'll get back to you if there are any questions, thanks! |
Adds a lock screen and Dynamic Island Live Activity to LoopFollow displaying
real-time glucose data, IOB, COB and projected blood glucose. Updates are driven by LoopFollow's existing refresh engine via APNs self-push.
See docs/LiveActivity.md for full architecture, setup instructions, and the reasoning behind APNs self-push over direct activity.update().
LiveActivity.md contents:
LoopFollow Live Activity — Architecture & Design Decisions
Author: Philippe Achkar (supported by Claude)
Date: 2026-03-07
What Is the Live Activity?
The Live Activity displays real-time glucose data on the iPhone lock screen and in the Dynamic Island. It shows:
It updates every 5 minutes, driven by LoopFollow's existing refresh engine. No separate data pipeline exists — the Live Activity is a rendering surface only.
Core Principles
1. Single Source of Truth
The Live Activity never fetches data directly from Nightscout or Dexcom. It reads exclusively from LoopFollow's internal storage layer (
Storage.shared,Observable.shared). All glucose values, thresholds, IOB, COB, and loop status flow through the same path as the rest of the app.This means:
2. Source-Agnostic Design
LoopFollow supports both Nightscout and Dexcom. IOB, COB, or predicted glucose are modeled as optional (
Double?) inGlucoseSnapshotand the UI renders a dash (—) when they are absent. The Live Activity never assumes these values exist.3. No Hardcoded Identifiers
The App Group ID is derived dynamically at runtime: group.`. No team-specific bundle IDs or App Group IDs are hardcoded anywhere. This ensures the project is safe to fork, clone, and submit as a pull request by any contributor.
Update Architecture — Why APNs Self-Push?
This is the most important architectural decision in Phase 1. Understanding it will help you maintain and extend this feature correctly.
What We Tried First — Direct
activity.update()The obvious approach to updating a Live Activity is to call
activity.update()directly from the app. This works reliably when the app is in the foreground.The problem appears when the app is in the background. LoopFollow uses a background audio session (
.playbackcategory, silent WAV file) to stay alive in the background and continue fetching glucose data. We discovered that liveactivitiesd (the iOS system daemon responsible for rendering Live Activities) refuses to processactivity.update()calls from processes that hold an active background audio session. The update call either hangs indefinitely or is silently dropped. The Live Activity freezes on the lock screen while the app continues running normally.We attempted several workarounds; none of these approaches were reliable or production-safe:
activity.update()while audio is playing | Updates hang or are droppedAVAudioSession.setActive(false)before updating | Intermittently worked, but introduced a race condition and broke the audio session unpredictablyThe Solution — APNs Self-Push
Our solution is for LoopFollow to send an APNs (Apple Push Notification service) push notification to itself.
Here is how it works:
activity.pushTokenUpdates.liveactivitiesdon the device.liveactivitiesdupdates the Live Activity directly — the app process is never involved in the rendering path.Because
liveactivitiesdreceives the update via APNs rather than via an inter-process call from LoopFollow, it does not care that LoopFollow holds a background audio session. The update is processed reliably every time.Why This Is Safe and Appropriate
P256.Signing) and cached for 55 minutes (APNs tokens are valid for 60 minutes).File Map
Main App Target
LiveActivityManager.swiftGlucoseSnapshotBuilder.swiftGlucoseSnapshotfrom storageStorageCurrentGlucoseStateProvider.swiftStorage.sharedandObservable.sharedGlucoseSnapshotStore.swiftLAThresholdSync.swiftPreferredGlucoseUnit.swiftAPNSClient.swiftAPNSJWTGenerator.swiftShared (App + Extension)
GlucoseLiveActivityAttributes.swiftGlucoseSnapshot.swiftGlucoseUnitConversion.swiftLAAppGroupSettings.swiftAppGroupID.swiftExtension Target
LoopFollowLiveActivity.swiftLoopFollowLABundle.swiftUpdate Flow
APNs Setup — Required for Contributors
To build and run the Live Activity locally or via CI, you need an APNs authentication key. The key content is injected at build time via
LoopFollowConfigOverride.xcconfigand is never stored in the repository.What you need
.p8file) with the Apple Push Notifications service (APNs) capability enabledLocal Build Setup
.p8key from developer.apple.com → Certificates, Identifiers & Profiles → Keys.-----BEGIN PRIVATE KEY-----and-----END PRIVATE KEY-----. Join all lines into a single unbroken string with no spaces or line breaks.LoopFollowConfigOverride.xcconfigin the project root (this file is gitignored):Info.plistwhich resolves$(APNS_KEY_CONTENT)from the xcconfig.CI / GitHub Actions Setup
Add two repository secrets under Settings → Secrets and variables → Actions:
APNS_KEY_IDAPNS_KEY.p8file including PEM headersThe build workflow strips the PEM headers automatically and injects the content into
LoopFollowConfigOverride.xcconfigbefore building.