From a6f741b1b308ac976b95a27a21d24bd8afc0436e Mon Sep 17 00:00:00 2001 From: alexanderschmidt2 <67632384+alexanderschmidt2@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:18:29 +0200 Subject: [PATCH 1/2] Reuse Trends calendar dates and stable heat-strip identities --- .../Sources/StrandDesign/YearHeatStrip.swift | 8 ++++---- Strand/Screens/TrendsView.swift | 19 ++++++++----------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Packages/StrandDesign/Sources/StrandDesign/YearHeatStrip.swift b/Packages/StrandDesign/Sources/StrandDesign/YearHeatStrip.swift index 5ed28aa69f..aff13a6406 100644 --- a/Packages/StrandDesign/Sources/StrandDesign/YearHeatStrip.swift +++ b/Packages/StrandDesign/Sources/StrandDesign/YearHeatStrip.swift @@ -12,7 +12,7 @@ import SwiftUI /// A day's recovery datum for the heat strip. public struct RecoveryDay: Identifiable, Sendable { - public let id = UUID() + public var id: Date { date } public var date: Date /// Recovery 0...100, or nil if no data for that day. public var score: Double? @@ -79,7 +79,7 @@ public struct YearHeatStrip: View { // Group days into week columns. weekday 0 = Monday ... 6 = Sunday. private struct Week: Identifiable { - let id = UUID() + let id: Date var cells: [RecoveryDay?] // length 7, indexed by weekday row var monthLabel: String? } @@ -89,7 +89,7 @@ public struct YearHeatStrip: View { private static func buildWeeks(from days: [RecoveryDay]) -> [Week] { guard let first = days.first?.date else { return [] } var weeks: [Week] = [] - var current = Week(cells: Array(repeating: nil, count: 7), monthLabel: nil) + var current = Week(id: first, cells: Array(repeating: nil, count: 7), monthLabel: nil) var lastMonth = -1 // Pad the first week so the first day lands on its weekday row. let firstRow = weekdayRow(first) @@ -100,7 +100,7 @@ public struct YearHeatStrip: View { let row = weekdayRow(day.date) if row == 0 && filledThisWeek > 0 { weeks.append(current) - current = Week(cells: Array(repeating: nil, count: 7), monthLabel: nil) + current = Week(id: day.date, cells: Array(repeating: nil, count: 7), monthLabel: nil) filledThisWeek = 0 } current.cells[row] = day diff --git a/Strand/Screens/TrendsView.swift b/Strand/Screens/TrendsView.swift index 8bf18e41b2..734f730263 100644 --- a/Strand/Screens/TrendsView.swift +++ b/Strand/Screens/TrendsView.swift @@ -116,10 +116,11 @@ struct TrendsView: View { let rhr: ResolvedMetric let strain: ResolvedMetric let rest: ResolvedMetric + let recoveryDays: [RecoveryDay] } /// Repository status publications can redraw Trends without changing its historical data. - /// Retain the five resolved windows across those redraws; invalidate on the data generation, + /// Retain the resolved windows and calendar dates; invalidate on the data generation, /// range, Rest-series revision, local day, or locale so readings and captions remain current. /// This reference does not publish changes of its own, avoiding a body-to-state update loop. @MainActor private final class ResolvedCache { @@ -154,7 +155,10 @@ struct TrendsView: View { hrv: resolve { $0.avgHrv }, rhr: resolve { $0.restingHr.map(Double.init) }, strain: resolve { $0.strain }, - rest: resolve { sleepPerfByDay[$0.day] }) + rest: resolve { sleepPerfByDay[$0.day] }, + recoveryDays: repo.days.suffix(max(range.days ?? repo.days.count, 365)).compactMap { d in + date(d.day).map { RecoveryDay(date: $0, score: d.recovery) } + }) } } @@ -326,7 +330,7 @@ struct TrendsView: View { // chart behind an honest "needs N more days" state until enough history exists. TrainingLoadCard(days: repo.days) .staggeredAppear(index: 5) - yearStrip + yearStrip(recoveryDays: metrics.recoveryDays) .staggeredAppear(index: 6) exportReportRow .staggeredAppear(index: 7) @@ -772,14 +776,7 @@ struct TrendsView: View { // MARK: Year heat-strip - private var yearStrip: some View { - // Always show at least a full year for context; expand to all history on ALL. - let stripDays = max(range.days ?? repo.days.count, 365) - let recent = repo.days.suffix(stripDays) - let recoveryDays: [RecoveryDay] = recent.compactMap { d in - guard let dt = date(d.day) else { return nil } - return RecoveryDay(date: dt, score: d.recovery) - } + private func yearStrip(recoveryDays: [RecoveryDay]) -> some View { let title = (range == .all && repo.days.count > 365) ? String(localized: "Charge (all history)") : String(localized: "Charge (past year)") return NoopCard { VStack(alignment: .leading, spacing: NoopMetrics.cardInnerSpacing) { From 3b09b85fd264fa827fdb93ae56836631b5216db4 Mon Sep 17 00:00:00 2001 From: alexanderschmidt2 <67632384+alexanderschmidt2@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:49:45 +0200 Subject: [PATCH 2/2] Keep cached Trends sections compatible with lazy materialization --- Strand/Screens/TrendsView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Strand/Screens/TrendsView.swift b/Strand/Screens/TrendsView.swift index 734f730263..f7c1680d0e 100644 --- a/Strand/Screens/TrendsView.swift +++ b/Strand/Screens/TrendsView.swift @@ -329,9 +329,7 @@ struct TrendsView: View { // range window — chronic load is inherently a 42-day horizon. Self-hides its // chart behind an honest "needs N more days" state until enough history exists. TrainingLoadCard(days: repo.days) - .staggeredAppear(index: 5) yearStrip(recoveryDays: metrics.recoveryDays) - .staggeredAppear(index: 6) exportReportRow .staggeredAppear(index: 7) }