Skip to content

fix(today): start the journal card's load, so the card can appear - #2765

Open
DX23876 wants to merge 2 commits into
ryanbr:mainfrom
DX23876:fix/journal-card-load-task
Open

DX23876 wants to merge 2 commits into
ryanbr:mainfrom
DX23876:fix/journal-card-load-task

Conversation

@DX23876

@DX23876 DX23876 commented Oct 10, 2026 •

Copy link
Copy Markdown

JournalReminderCard attaches its .task to a Group whose only child is the card itself, shown once loggedDays has loaded. A Group hands its modifiers to its children, and before the first load there is no child, so the task never runs, loggedDays stays nil and the card never appears on either Today screen, whatever the reminder toggle says.

The content now sits in a VStack that exists whenever the reminder is on, so the load always starts. With the reminder off the card still renders nothing.

Verification

  • iOS simulator, demo data, main at e580175: the Journal section is in the default order and not hidden, yet no card appears. With this change the card shows (strip plus "Log today's journal").
  • A standalone SwiftUI check on macOS: .task on a Group (or an EmptyView) with no child never runs; on an empty VStack it does.
  • macOS Strand and iOS NOOPiOS build.

Android is not affected: JournalReminderCard there loads in a LaunchedEffect after its early return, which always runs.

Also: the menstrual cycle card (same deadlock)

MenstrualCycleHomeCard shows when cycle awareness applies, when the toggle is on, or when period starts are logged. The starts were loaded by a .task on the same kind of childless Group, so with awareness and toggle off the third condition could never become true. Here the empty state is permanent for most users, so a container would leave a zero-size slot and its spacing in the Today stack. The load now runs in PeriodStartsLoader, the same shape as the loader in #2766.

The vestigial JournalReminderLoadKey is gone: the task only exists while the reminder is on, so it is keyed on refreshSeq alone.

  • Standalone SwiftUI check on macOS: the old shape never calls the load; the loader calls it once, the card appears (stack 48 → 80 pt), with no starts the stack keeps its height (48 pt), and a seq bump reloads once.
  • macOS Strand and iOS NOOPiOS build.

JournalReminderCard attached its .task to a Group whose only child is the card
itself, shown once `loggedDays` is loaded. A Group hands its modifiers to its
children, and before the first load there is no child, so the task never ran,
`loggedDays` stayed nil and the card never appeared on either Today screen,
whatever the reminder toggle said.

The content now sits in a VStack that exists whenever the reminder is on, so
the load always starts; with the reminder off the card still renders nothing.
Reproduced on the iOS simulator with demo data (card missing, then present
after the change). Android is not affected: JournalReminderCard there loads in
a LaunchedEffect after its early return, which always runs.
@ryanbr

ryanbr commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Swept the rest of the tree for this pattern, since the diagnosis generalises past the two cards: a Group hands its modifiers to its children, so with no child there is nothing for .task to attach to, and when the only child is gated on what the task loads, neither side can ever start.

Seven .task/.onAppear-on-Group sites in Strand/ and Packages/. Four cannot hit it, because an else branch guarantees a child on every pass:

  • Strand/Liquid/LiquidPrimitives.swift:353 (and filling starts true regardless)
  • Strand/Screens/TrainingLoadCard.swift:104
  • Strand/Screens/V5PillarHosts.swift:29
  • Strand/Screens/WeeklyDigestView.swift:540

Two are this card and the auto-detect one (#2766). The seventh has the identical deadlock:

Strand/Screens/SkinTempCardsView.swift:297

@State private var starts: [String] = []                                     // :289
private var shouldShow: Bool {                                               // :293
    !cycleHidden && (profile.cycleAwarenessApplies || cycleEnabled || !starts.isEmpty)
}
Group { if shouldShow { ... } }        // :297, no else
    .task(id: repo.cycleTrackingSeq) { starts = await repo.periodStarts() }   // :379

starts has exactly one writer and it sits inside that task, so the third disjunct !starts.isEmpty can never become true. That clause is there to keep the card visible for someone who has logged period starts but has cycle awareness off and the toggle off, and for that configuration the card never appears at all. Whenever either of the other two disjuncts is true the card renders, the task runs, and nothing looks wrong, which is presumably why it went unnoticed.

The same file carries its own control: :481 attaches the same .task(id: repo.cycleTrackingSeq) { starts = await repo.periodStarts() } to a view that always exists, and that one loads fine.

Happy to take it here if you want to extend this PR to cover it. Worth noting before you pick a remedy: the empty state at that site is permanent, not a brief pre-load window (cycle awareness off, toggle off, no logged starts is a steady state), so the container approach in this PR would leave a zero-size child holding a slot and its spacing in the Today stack. That puts it closer to the auto-detect card's situation than to this one.

One unrelated small thing on this PR: the task id is now built only when reminderEnabled is true, so the enabled field of JournalReminderLoadKey is constant for the task's whole lifetime, and flipping the toggle recreates the view rather than rekeying the task. Same behaviour either way, the field is just vestigial now.

…n show it

`MenstrualCycleHomeCard` is shown when cycle awareness applies, when the toggle is on, or when period
starts are logged. The starts were loaded by a `.task` on a `Group` whose only child is the card, so
with awareness off and the toggle off there was no child, the task never ran, `starts` stayed empty and
the third condition could never become true. That empty state is the steady state for most users, so a
placeholder would hold a slot and its spacing in the Today stack. The load now runs in
`PeriodStartsLoader`, the same shape as the auto-detect card's loader.

Also drop `JournalReminderLoadKey`: the task only exists while the reminder is on, so its `enabled`
field was constant and the seq alone is the id.

Analysis migration required: no
@DX23876

DX23876 commented Oct 13, 2026

Copy link
Copy Markdown
Author

Thanks for the sweep. Took both into this PR in 3875c37.

  • MenstrualCycleHomeCard: as you suggested, not the container. The period starts now load in a small PeriodStartsLoader, the same shape as the loader in fix(today): run the auto-detect scan, so "looks like a workout?" can appear #2766, so a user with logged starts but awareness and toggle off gets the card, and with no starts the Today stack keeps its height with no gap.
  • JournalReminderLoadKey is gone; the task is keyed on refreshSeq alone.

Checked with a standalone SwiftUI run on macOS (the old shape never calls the load; the loader loads once, the card appears, an empty result leaves the stack height unchanged, a seq bump reloads once) and both app builds. Not yet seen on a device with real logged starts.

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