Files
workouts/Workouts/Views/Progress/ExerciseTrendsView.swift
T
rzen 6e440317c4 Restructure into a three-tab app with Progress, goals, and Meditation
The UX redesign's first landing (spec in UX-REDESIGN.md): ContentView
becomes a Today / Progress / Settings TabView, "Routine" replaces
"Split" in every user-facing string and view name (code-level types
keep their names), and workout starting moves to shared
WorkoutStarter / StartedWorkoutNavigator plumbing.

- New Progress tab: weekly goal streaks, workout trends, per-exercise
  weight progression, achievements, and the full history list
  (WorkoutLogsView -> WorkoutHistoryView).
- Goals: stable categories workouts roll up to, managed from Settings.
- New Meditation exercise + starter routine; timed sits record to
  Apple Health as Mind & Body sessions.

Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
2026-07-11 07:53:01 -04:00

145 lines
5.0 KiB
Swift

//
// ExerciseTrendsView.swift
// Workouts
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
/// Per-exercise drill-down: every weighted exercise with completed history, most
/// recently trained first, each opening its weight-progression detail. This promotes
/// the progression chart to a first-class Progress surface (it also remains on the
/// library's reference pages).
struct ExerciseTrendsListView: View {
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
/// Completed weighted logs, newest first.
@Query private var logs: [WorkoutLog]
init() {
let completedRaw = WorkoutStatus.completed.rawValue
let weightRaw = LoadType.weight.rawValue
_logs = Query(
filter: #Predicate<WorkoutLog> {
$0.statusRaw == completedRaw && $0.loadType == weightRaw
},
sort: \WorkoutLog.date,
order: .reverse)
}
private struct ExerciseSummary: Identifiable {
var name: String
var sessions: Int
var best: Double
var last: Date
var id: String { name }
}
/// One row per exercise name: session count, best top-set weight, last trained.
/// `logs` is newest-first, so the first log seen per name fixes `last`.
private var summaries: [ExerciseSummary] {
var byName: [String: ExerciseSummary] = [:]
for log in logs {
let top = log.setEntries?.compactMap(\.weight).max() ?? log.weight
if var summary = byName[log.exerciseName] {
summary.sessions += 1
summary.best = max(summary.best, top)
byName[log.exerciseName] = summary
} else {
byName[log.exerciseName] = ExerciseSummary(
name: log.exerciseName, sessions: 1, best: top, last: log.date)
}
}
return byName.values.sorted { $0.last > $1.last }
}
var body: some View {
List(summaries) { summary in
NavigationLink {
ExerciseTrendDetailView(exerciseName: summary.name)
} label: {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(summary.name)
Text("\(summary.sessions) session\(summary.sessions == 1 ? "" : "s") · last \(summary.last.daysAgoLabel())")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text(weightUnit.format(summary.best))
.font(.subheadline.weight(.semibold))
.monospacedDigit()
.foregroundStyle(.secondary)
}
}
}
.overlay {
if summaries.isEmpty {
ContentUnavailableView(
"No Weighted Exercises Yet",
systemImage: "dumbbell",
description: Text("Complete weighted sets and each exercise's progression shows up here."))
}
}
.navigationTitle("Exercise Trends")
}
}
/// One exercise's progression: headline stats over its completed history plus the
/// weight-progression chart.
struct ExerciseTrendDetailView: View {
let exerciseName: String
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
/// Completed logs for this exercise, oldest first (matching the chart's order).
@Query private var logs: [WorkoutLog]
init(exerciseName: String) {
self.exerciseName = exerciseName
let name = exerciseName
let completedRaw = WorkoutStatus.completed.rawValue
_logs = Query(
filter: #Predicate<WorkoutLog> {
$0.exerciseName == name && $0.statusRaw == completedRaw
},
sort: \WorkoutLog.date,
order: .forward)
}
private var bestWeight: Double {
logs.map { $0.setEntries?.compactMap(\.weight).max() ?? $0.weight }.max() ?? 0
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 12) {
ProgressStatTile(
title: "Best",
value: weightUnit.format(bestWeight),
unit: nil)
ProgressStatTile(
title: "Sessions",
value: "\(logs.count)",
unit: nil)
ProgressStatTile(
title: "Last",
value: logs.last?.date.daysAgoLabel() ?? "—",
unit: nil)
}
WeightProgressionChartView(exerciseName: exerciseName)
.progressCard()
}
.padding(.horizontal)
}
.background(Color(.systemGroupedBackground))
.navigationTitle(exerciseName)
.navigationBarTitleDisplayMode(.inline)
}
}