Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions Strand/Screens/JournalReminderCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,21 @@ struct JournalReminderCard: View {
private static let stripDays = 7

var body: some View {
Group {
if reminderEnabled, let logged = loggedDays {
card(logged)
if reminderEnabled {
// A VStack, not a Group: a Group hands its modifiers to its children, and before the first load
// there is no child, so the `.task` below never ran, `loggedDays` stayed nil and the card never
// appeared. The stack is a view of its own even while empty, so the load always starts.
VStack(spacing: 0) {
if let logged = loggedDays {
card(logged)
}
}
// Re-read whenever a sync bumps refreshSeq, so the strip and the "logged today" state stay current
// after the user logs and comes back. Flipping the toggle removes and recreates this view, which
// starts a fresh task on its own, so the toggle needs no place in the id.
.task(id: repo.refreshSeq) {
await reload()
}
}
// Re-read whenever a sync bumps refreshSeq or the toggle flips (mirrors AutoWorkoutCard's task id),
// so the strip and the "logged today" state stay current after the user logs and comes back.
.task(id: JournalReminderLoadKey(seq: repo.refreshSeq, enabled: reminderEnabled)) {
await reload()
}
}

Expand Down Expand Up @@ -141,9 +147,3 @@ struct JournalReminderCard: View {
}
}
}

/// Reload key: a sync (seq) or toggle flip re-reads completion. Mirrors `AutoWorkoutLoadKey`.
private struct JournalReminderLoadKey: Equatable {
let seq: Int
let enabled: Bool
}
42 changes: 37 additions & 5 deletions Strand/Screens/SkinTempCardsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,25 @@ struct MenstrualCycleHomeCard: View {
/// The user's "not for me" opt-out (#hide-cycle): when set, the card is suppressed entirely,
/// reversibly from Automations. USER-controlled, never age-based.
@AppStorage(AppModel.cycleAwarenessHiddenKey) private var cycleHidden = false
@State private var starts: [String] = []
/// Logged period starts, loaded outside the view tree (see `PeriodStartsLoader` for why a `.task` here
/// could never start while the card was hidden).
@StateObject private var loader = PeriodStartsLoader()
@State private var showingTracker = false

private var starts: [String] { loader.starts }

private var shouldShow: Bool {
!cycleHidden && (profile.cycleAwarenessApplies || cycleEnabled || !starts.isEmpty)
}

var body: some View {
// Re-read whenever a period start is logged or deleted. The body re-runs on that (it observes `repo`),
// and the loader ignores an unchanged seq.
let _ = loader.request(seq: repo.cycleTrackingSeq) { [repo, model, cycleEnabled] in
let starts = await repo.periodStarts()
if cycleEnabled, model.cyclePhase == nil { await model.refreshV5Signals() }
return starts
}
Group {
if shouldShow {
NoopCard(tint: StrandPalette.restColor) {
Expand Down Expand Up @@ -375,10 +386,6 @@ struct MenstrualCycleHomeCard: View {
.accessibilityElement(children: .contain)
}
}
.task(id: repo.cycleTrackingSeq) {
starts = await repo.periodStarts()
if cycleEnabled, model.cyclePhase == nil { await model.refreshV5Signals() }
}
.sheet(isPresented: $showingTracker) {
Group {
if let result = model.cyclePhase {
Expand Down Expand Up @@ -430,6 +437,31 @@ struct MenstrualCycleHomeCard: View {
}
}

/// Loads the logged period starts for `MenstrualCycleHomeCard` outside the view tree. One reason to show
/// the card is that starts exist, and SwiftUI never runs a `.task` attached to a `Group` with no child, so
/// with cycle awareness off and the toggle off the load never started and the card never appeared. That
/// empty state is permanent for most users, so an always-present placeholder would hold a slot in the
/// Today stack and add its spacing; the view only reports the current seq here and this object loads.
@MainActor
final class PeriodStartsLoader: ObservableObject {
@Published private(set) var starts: [String] = []
private var lastSeq: Int?
private var load: Task<Void, Never>?

/// Called from the card's body on every pass; starts a load only when the seq changed. Publishes
/// later, from the task, never during the view update.
func request(seq: Int, load fetch: @escaping @MainActor () async -> [String]) {
guard seq != lastSeq else { return }
lastSeq = seq
load?.cancel()
load = Task { [weak self] in
let next = await fetch()
guard !Task.isCancelled, let self else { return }
self.starts = next
}
}
}

// MARK: - Cycle tracker detail

/// Local-only logged-period detail. It deliberately records only cycle day 1: the engine needs an
Expand Down
Loading