Fix stale LA dismissed by iOS incorrectly blocking auto-restart#577
Merged
bjorkert merged 2 commits intoloopandlearn:live-activityfrom Mar 26, 2026
Merged
Conversation
startIfNeeded() unconditionally reused any existing activity, which meant that on cold start (app killed while stale overlay was showing) willEnterForeground is never sent, handleForeground never runs, and viewDidAppear → startFromCurrentState → startIfNeeded just rebinds to the stale activity — leaving the overlay visible. Fix: before reusing an existing activity in startIfNeeded(), check whether its staleDate has passed or the renewal window is open. If so, end it (awaited) and call startIfNeeded() again so a fresh activity with a new 7.5h deadline is started. Also add cancelRenewalFailedNotification() to handleForeground() so the "Live Activity Expiring" system notification is dismissed whenever the foreground restart path fires, not only via forceRestart(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When iOS dismisses a Live Activity because its staleDate passed (background stale overlay case), laRenewalFailed is false, so the state observer's else branch fired and set dismissedByUser=true — permanently blocking all auto-restart paths (startFromCurrentState has guard !dismissedByUser). Fix 1: attachStateObserver now checks staleDatePassed alongside laRenewalFailed; both are iOS-initiated dismissals that should allow auto-restart. Fix 2: handleForeground() Task resets dismissedByUser=false before calling startFromCurrentState(), guarding against the race where the state observer fires .dismissed during our own end() call before its Task cancellation takes effect. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.
Problem
When iOS dismisses a Live Activity because its
staleDatepassed (the stale overlay scenario),laRenewalFailedisfalse. The state observer'selsebranch then fires and setsdismissedByUser = true, which permanently blocks all auto-restart paths —startFromCurrentState()hasguard !dismissedByUserat the top.Result: foregrounding the app after a stale overlay appeared did nothing; the user had to manually press Restart Live Activity in Settings.
Root cause
In
attachStateObserver, the.dismissedcase only distinguished user-swipes from iOS-forced dismissals by checkinglaRenewalFailed. But there's a second iOS-initiated path: iOS silently removes the activity whenstaleDatepasses, without settinglaRenewalFailed.Fix
1.
attachStateObserver— also checkstaleDatePassed(activity.content.staleDate <= Date()). BothrenewalFailedandstaleDatePassedare iOS-initiated dismissals; only a fresh-activity swipe should block auto-restart.2.
handleForeground()Task — resetdismissedByUser = falsebefore callingstartFromCurrentState(), guarding against a race where the state observer fires.dismissedduring our ownend()call (before its Task cancellation takes effect) and incorrectly sets the flag.Testing
🤖 Generated with Claude Code