Skip to content

Add bedtime Live Activity - #37

Draft
gsbernstein wants to merge 77 commits into
masterfrom
cursor/live-bedtime-activity-43cf
Draft

Add bedtime Live Activity#37
gsbernstein wants to merge 77 commits into
masterfrom
cursor/live-bedtime-activity-43cf

Conversation

@gsbernstein

@gsbernstein gsbernstein commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Adds a Live Activity for tonight's bedtime countdown, shown on the Lock Screen and in the Dynamic Island (compact, expanded, and minimal presentations), plus in-app controls to start/update/end it.

Highlights:

  • LiveActivityManager starts, updates, schedules, and ends the activity, and auto-syncs with the current recommendation on foreground.
  • Lock Screen / expanded Dynamic Island show a countdown ("in 8 hours"), a wind-down/sleep progress bar, and bedtime/wake/target times.
  • Compact Dynamic Island shows a short duration under a "Sleep"/"Wake" label on the leading side and a countdown with a linear progress bar on the trailing side; minimal presentation (when sharing space with another Live Activity) shows a circular progress ring with the phase icon.
  • BedtimePlanStore persists the latest recommendation so a background App Intent (StartBedtimeCountdownIntent / EndBedtimeCountdownIntent, exposed via AppShortcutsProvider) can start/stop the countdown from a Shortcuts automation without launching the app.
  • New BedtimeLiveActivity WidgetKit extension target wired into the Xcode project, deploying to iOS 18 (gated behind LiveActivityManager.isSupported in the app UI, which stays on iOS 17.6).

Latest changes: extracted the progress-bar layout-direction handling into a shared PhaseProgressBar view, and added a thin linear progress bar under the compact trailing countdown text so it mirrors the wind-down/sleep progress shown elsewhere.

Open in Web Open in Cursor 

cursoragent and others added 30 commits August 7, 2026 14:06
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
…-activity-43cf

# Conflicts:
#	Bedtime/Bedtime/ContentView.swift
#	Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift
#	README.md

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
cursoragent and others added 16 commits August 13, 2026 16:56
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
# Conflicts:
#	Bedtime/Bedtime.xcodeproj/xcshareddata/xcschemes/Bedtime.xcscheme
@gsbernstein
gsbernstein force-pushed the cursor/live-bedtime-activity-43cf branch from 6195876 to 389b9fb Compare August 14, 2026 15:04
cursoragent and others added 13 commits August 15, 2026 05:00
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
HealthKit background delivery and a BGProcessingTask backup call
LiveActivityManager.markAwakeIfNeeded(), which flips a new isAwake
flag and clears staleDate on the existing active/stale activity
without recomputing the recommendation. The widget adds a third
phase for this, showing a link to the Oura app (or a generic open-app
prompt) instead of a countdown once the person wakes up.

staleDate is a one-shot latch already spent on the wind-down->sleeping
transition, so a second transition needs the app to actually run and
push a fresh update rather than a second local staleness flip.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Removes SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor from the project's
Debug/Release configs (shared by both targets) and the nonisolated
markers that only existed to counteract it (Constants, SleepWindow,
the TimeInterval extension, BedtimeActivityAttributes/
BedtimeSourceAppLink).

Adds the two things that were actually relying on the implicit
default rather than an explicit annotation: @mainactor on
SourcePreferences (an ObservableObject, like HealthKitManager and
LiveActivityManager), and nonisolated on
LiveActivityManager.registerBackgroundTask() so it can still be
called synchronously from BedtimeApp.init(), which is no longer
MainActor-isolated by default.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
refreshBedtimePlan() only ran on initial launch and returning from
background, so adjusting the sleep bank range, sleep goal, or wake
time while the app stayed foregrounded updated the on-screen
recommendation but never told the Live Activity about it.

Makes BedtimeRecommendation Equatable and adds .onChange(of:) so any
foreground change to the computed recommendation re-syncs it.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
activeActivityID was cached from Activity.activities.first, which
isn't guaranteed to be the on-screen activity once the multi-night
queue can add several .pending future nights to that array. If it
latched onto a pending night instead of tonight's active/stale one,
startOrUpdate would look it up, find it .pending, end it (destroying
a correctly queued future night), and create a brand-new separate
activity — leaving the one actually on screen untouched. That's why
neither the auto-sync nor the manual Update button appeared to do
anything: they were updating something you couldn't see.

Adds currentlyVisibleActivity(), which finds whatever's .active or
.stale (on screen) directly rather than trusting the cached id, and
uses it in init(), startOrUpdate(), and markAwakeIfNeeded(). Each of
those also now writes activeActivityID back afterward, so the cache
self-heals instead of staying wrong once it's drifted.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
startOrUpdate only ended whichever single activity 'existingActivity'
happened to resolve to before falling through to Activity.request().
Once the multi-night queue can leave several .pending nights
scheduled at once, that left the others still counting against the
5-concurrent-activity budget, so request() failed with
targetMaximumExceeded ('Maximum number of activities for target
already exists') even though nothing was visibly on screen.

Now ends every .pending activity before requesting a new one:
starting/updating right now always takes priority over a background
pre-scheduled queue, which syncWithSchedule rebuilds fresh anyway.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
- registerBackgroundTask's launch handler captured the non-Sendable
  BGTask into a nested Task { @mainactor in }; nonisolated(unsafe)
  matches the existing HealthKit completionHandler workaround.
- startOrUpdate's shared 'existingActivity' let was read at two
  separate points (computing startTime, then the if-let/await), which
  taints it for sending under strict concurrency. Replaced with a
  local function that re-derives it fresh at each call site, same
  fix as the earlier scheduleUpcomingNights issue.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
The previous fix used a local nested function, but local functions
referencing self still inherit the enclosing method's MainActor
isolation at their call site, which taints the returned Activity for
the region just like a stored let did. Added
existingActivity(cachedID:) as a real nonisolated static function
(taking the cached ID as a parameter instead of reading self),
matching the existing activity(withID:)/currentlyVisibleActivity()
pattern, and call it fresh at each use site in startOrUpdate.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants