Bug fixes and SmallFamilyView configurable slot#575
Closed
MtlPhil wants to merge 67 commits intoloopandlearn:live-activityfrom
Closed
Bug fixes and SmallFamilyView configurable slot#575MtlPhil wants to merge 67 commits intoloopandlearn:live-activityfrom
MtlPhil wants to merge 67 commits intoloopandlearn:live-activityfrom
Conversation
Registers com.loopfollow.audiorefresh with BGTaskScheduler so iOS can wake the app every ~15 min to check if the silent audio session is still alive and restart it if not. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 'fetch' to UIBackgroundModes so BGTaskScheduler.submit() doesn't
throw notPermitted on every background transition
- Call stopBackgroundTask() before startBackgroundTask() in the refresh
handler to prevent accumulating duplicate AVAudioSession observers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- startBackgroundTask() now removes the old observer before adding, making it idempotent and preventing duplicate interrupt callbacks - Add 'audio restart initiated' log after restart so success is visible without debug mode - Temporarily make 'Silent audio playing' log always visible for testing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Forgotten stub.
- When renewIfNeeded fails in the background (app can't start a new LA because it's not visible), schedule a local notification on the first failure: "Live Activity Expiring — Open LoopFollow to restart." Subsequent failures in the same cycle are suppressed. Notification is cancelled if renewal later succeeds or forceRestart is called. - In attachStateObserver, distinguish iOS force-dismiss (laRenewalFailed == true) from user swipe (laRenewalFailed == false). OS-dismissed LAs no longer set dismissedByUser, so opening the app triggers auto-restart as expected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Force-quitting an app kills its Live Activities, so cold-launch via LA tap only occurs when iOS terminates the app — in which case scene(_:openURLContexts:) already handles navigation correctly via DispatchQueue.main.async. The flag was never set and never needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BackgroundRefreshManager: all logs → .taskScheduler - AppDelegate: APNs registration/notification logs → .apns - APNSClient: all logs → .apns - BackgroundTaskAudio: restore isDebug:true on silent audio log; fix double blank line - LiveActivityManager: fix trailing whitespace; remove double blank line; SwiftFormat - GlucoseSnapshotBuilder: fix file header (date → standard LoopFollow header) - LoopFollowLiveActivity: remove dead commented-out activityID property - SwiftFormat applied across all reviewed LiveActivity/, Storage/, extension files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents truncation toward zero (e.g. 179.9 → 179); now correctly rounds to nearest integer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BackgroundRefreshManager: guard against double setTaskCompleted if the expiration handler fires while the main-queue block is in-flight. Apple documents calling setTaskCompleted more than once as a programming error. LiveActivityManager.renewIfNeeded: write laRenewBy to Storage only after Activity.request succeeds, eliminating the narrow window where a crash between the write and the request could leave the deadline permanently stuck in the future. No rollback needed on failure. The fresh snapshot is built via withRenewalOverlay(false) directly rather than re-running the builder, since the caller already has a current snapshot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Derive BGTask IDs, notification IDs, URL schemes, and notification categories from Bundle.main.bundleIdentifier so that LoopFollow, LoopFollow_Second, and LoopFollow_Third each get isolated identifiers and don't interfere with each other's background tasks, notifications, or Live Activities. Also show the configured display name in the Live Activity footer (next to the update time) when the existing "Show Display Name" toggle is enabled, so users can identify which instance a LA belongs to.
Users upgrading from the old hardcoded identifiers would have orphaned pending notifications that the new bundle-ID-scoped code can't cancel. This one-time migration cleans them up on first launch.
The `bg` and `loopingResumed` refresh triggers fire ~10s apart. With a 5s debounce, `loopingResumed` arrives after the debounce has already executed, causing two APNs pushes per BG cycle instead of one. Widening the window to 20s ensures both events are coalesced into a single push containing the most up-to-date post-loop-cycle state (fresh IOB, predicted BG, etc.). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When BGAppRefreshTask fires after a reboot (before the user has unlocked the device), UserDefaults files are still encrypted (Before First Unlock state). Reading migrationStep returns 0, causing all migrations to re-run. migrateStep1 reads old_url from the also-locked App Group suite, gets "", and writes "" to url — wiping Nightscout and other settings. Fix: skip the entire migration block when the app is in background state. Migrations will run correctly on the next foreground open. This is safe since no migration is time-critical and all steps are guarded by version checks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous fix used guard+return which skipped the entire viewDidLoad when the app launched in background (BGAppRefreshTask). viewDidLoad only runs once per VC lifecycle, so the UI was never initialized when the user later foregrounded the app — causing a blank screen. Fix: wrap only the migration block in an if-check, so UI setup always runs. Migrations are still skipped in background to avoid BFU corruption. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…reground During BFU viewDidLoad, all tasks fire with url="" and reschedule 60s out. checkTasksNow() on first foreground finds nothing overdue. Fix: call scheduleAllTasks() after reloadAll() so tasks reset to their normal 2-5s initial delay, displacing the stale 60s BFU schedule. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After BFU reloadAll(), viewDidLoad left isInitialLoad=false and no overlay. Reset loading state and show the overlay so the user sees the same spinner they see on a normal cold launch, rather than blank charts for 2-5 seconds. The overlay auto-hides via the normal markLoaded() path when data arrives. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-column layout: BG + trend arrow + delta/unit on the left (colored by glucose threshold), projected BG + unit label on the right in white. Dynamic Island and lock screen views are unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LoopFollowLiveActivityWidgetWithCarPlay is declared with .supplementalActivityFamilies([.small]) so it is only ever rendered in .small contexts (CarPlay, Watch Smart Stack). Use SmallFamilyView directly instead of routing through LockScreenFamilyAdaptiveView, which was falling through to LockScreenLiveActivityView when activityFamily wasn't detected as .small. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two ActivityConfiguration widgets for the same attributes type were registered simultaneously. The system used the primary widget for all contexts, ignoring the supplemental one. On iOS 18+: register only LoopFollowLiveActivityWidgetWithCarPlay (with .supplementalActivityFamilies([.small]) and family-adaptive routing via LockScreenFamilyAdaptiveView for all contexts). On iOS <18: register only LoopFollowLiveActivityWidget (lock screen and Dynamic Island only). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Revert bundle to if #available without else (WidgetBundleBuilder does not support if/else with #available) - Make primary widget also use LockScreenFamilyAdaptiveView on iOS 18+ so SmallFamilyView renders correctly regardless of which widget the system selects for .small contexts (CarPlay / Watch Smart Stack) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Scope all notification/task identifiers to Bundle.main.bundleIdentifier so multiple LoopFollow instances don't collide (BackgroundAlertManager, BackgroundRefreshManager, LiveActivityManager renewal notification) - Derive URL scheme dynamically via AppGroupID.urlScheme (loopfollow → loopfollow2/3/etc. for additional instances) - Update Info.plist: BGTask identifier and URL scheme use app_suffix var - AppGroupID: extract baseBundleID computed var, add urlScheme computed var - LAAppGroupSettings: add displayName/showDisplayName support for multi-instance LA footer (off by default, no behaviour change for single) - Migration step 7: cancel legacy hardcoded notification identifiers - LoopFollowLiveActivity.swift: apply urlScheme to all Link/widgetURL targets; add optional display name prefix to lock screen footer Conflict on LoopFollowLAExtension/LoopFollowLiveActivity.swift resolved by keeping our single-widget structure (SmallFamilyView / supplemental family) and applying the URL scheme and display name changes from pr-branch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The LiveActivity.md doc file should not be part of this PR. https://claude.ai/code/session_01WaUhT8PoPNKumX9ZK9jeBy
The overlay was missing because isNotLooping in the snapshot was sourced from Observable.shared.isNotLooping, which is only set by evaluateNotLooping() in MainViewController. In background, BG refreshes could fire and build a snapshot before evaluateNotLooping() had run, always producing isNotLooping=false. Fix: persist the last known loop time to Storage (lastLoopTime) whenever the pump clock is updated from device status. StorageCurrentGlucoseStateProvider now computes isNotLooping directly from this stored timestamp instead of reading the Observable flag, so every refresh — BG, device status, or audio failure — independently produces the correct value regardless of whether the UI code path has executed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the renewal overlay (or renewal-failed flag) caused handleForeground() to end the old Live Activity and start a fresh one, handleDidBecomeActive() was racing with it: it called startFromCurrentState() before the old activity was fully ended, found it in Activity.activities, bound to it, and started observing its push token. The new activity created by handleForeground()'s async end+restart then had no token observer, leaving pushToken nil — so background APNs updates stopped until the user manually hit Restart LA. Fix: handleForeground() sets skipNextDidBecomeActive = true before starting the async restart. handleDidBecomeActive() checks and consumes the flag, skipping its competing startFromCurrentState() call entirely. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the right side of the small widget (CarPlay / Watch Smart Stack) was hardcoded to show projected BG. Replace it with a user-configurable slot that can be any of the same options available in the lock-screen grid. Changes: - LAAppGroupSettings: add smallWidgetSlot() / setSmallWidgetSlot() backed by a new App Group key (la.smallWidgetSlot), defaulting to .iob - LoopFollowLiveActivity: extract slotFormattedValue(option:snapshot:) as a shared file-private function used by both SlotView and SmallFamilyView; update SmallFamilyView right side to show the configured slot's gridLabel and value (trailing-aligned); hide the right side entirely when slot = .none - LiveActivitySettingsView: add "Small widget (CarPlay / Watch)" section with a Right slot picker Unit handling confirmed correct — LAFormat.glucose/delta/projected all go through formatGlucoseValue(unit:) which respects snapshot.unit. No hardcoding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Glucose-based slots (projectedBG, delta, minMax, target, isf) now show mg/dL or mmol/L below the value in the SmallFamilyView right slot. Added isGlucoseUnit property to LiveActivitySlotOption to identify which slots carry a glucose measurement. SmallFamilyView reads unitLabel from snapshot.unit (already unit-aware) and renders it at 11pt below the value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Glucose-based slots (projectedBG, delta, minMax, target, isf) now show the unit label (mg/dL / mmol/L) only when vertical space allows — CarPlay Dashboard has enough room, Watch Smart Stack drops it gracefully. Non-glucose slots are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'Grid slots' → 'Grid Slots - Live Activity' 'Small widget (CarPlay / Watch)' → 'Grid Slot - CarPlay / Watch' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reduce right slot value font 24pt → 20pt to prevent crowding the left side - Add .layoutPriority(1) to left VStack so glucose/delta are never compressed - Fix ISF unit label: rightSlotUnitLabel() now returns "mmol/L/U" or "mg/dL/U" for .isf instead of the plain glucose unit; other glucose slots unchanged Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GlucoseSnapshot.Unit.displayName returns "mg/dL" or "mmol/L" — the string was previously duplicated inline wherever needed. SmallFamilyView now uses snapshot.unit.displayName directly, removing the local glucoseUnitLabel/deltaUnitLabel vars. The rightSlotUnitLabel helper is simplified to a single guard + one ISF-specific branch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bjorkert
added a commit
that referenced
this pull request
Mar 25, 2026
- Make SmallFamilyView right slot configurable via Live Activity settings - Add Unit.displayName to GlucoseSnapshot for consistent unit labelling - Use ViewThatFits for adaptive CarPlay vs Watch Smart Stack layout - Fix APNs push token lost after renewal-overlay foreground restart - Fix Not Looping overlay not showing when app is backgrounded - Rename Live Activity settings section headers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bug fixes
Not Looping overlay missing when backgrounded —
isNotLoopingin the snapshot was sourced fromObservable.shared.isNotLooping, which is only set byevaluateNotLooping()inMainViewController. Background BG refreshes built snapshots before that method ran, always producingisNotLooping = false. Fix: persist the last known loop time toStorage(lastLoopTime) whenever the pump clock is updated from device status.StorageCurrentGlucoseStateProvidernow computesisNotLoopingdirectly from this stored timestamp, so every refresh path (foreground, background, device status, audio recovery) produces the correct value.APNs push token lost after renewal-overlay foreground restart —
willEnterForegroundNotificationanddidBecomeActiveNotificationalways fire back-to-back. When the renewal overlay triggeredhandleForeground()to do an async end+restart,handleDidBecomeActive()fired a fraction later, found the old (dying) activity still inActivity.activities, bound to it, and startedobservePushToken()on it. The old activity's token had already been issued and wouldn't re-emit — sopushTokenstayednil. Fix:handleForeground()setsskipNextDidBecomeActive = truebefore starting the async restart, sohandleDidBecomeActive()yields entirely.SmallFamilyView improvements (CarPlay / Watch Smart Stack)
LiveActivitySlotOptionvalues available in the lock-screen grid. Defaults to Projected BG. Persisted via a newla.smallWidgetSlotApp Group key.ViewThatFits(in: .vertical)— CarPlay shows the label, Watch omits it if space is tight. ISF correctly showsmmol/L/U/mg/dL/Urather than the bare glucose unit.GlucoseSnapshot.Unitgains adisplayNameproperty ("mg/dL"/"mmol/L") as a single source of truth, replacing inline string literals..layoutPriority(1)to prevent glucose/delta from being compressed by wide right-slot values.Test plan
mmol/L/Uormg/dL/Uon CarPlay and bare value on Watch🤖 Generated with Claude Code