From 29f04a878a3bd2775681f2a71171dffc24225ad6 Mon Sep 17 00:00:00 2001 From: rzen Date: Tue, 7 Jul 2026 18:06:11 -0400 Subject: [PATCH] Redesign the workout exercise row layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Weight moves up beside the exercise name (matching font), sets × reps sits below it with a dimmed ×, and recorded machine settings show as a secondary line under the name — no placeholder when unconfigured. Single-line bodyweight rows center against the checkbox. Claude-Session: https://claude.ai/code/session_01P152LxjZ4vePHsSorQa5Jf --- CHANGELOG.md | 4 + .../WorkoutLogs/WorkoutLogListView.swift | 90 +++++++++++-------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e58fb6..077ad3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ **July 2026** +Exercise rows in a workout have a cleaner layout — the weight sits beside the exercise name, with sets × reps and any recorded machine settings beneath. + +The Splits and Exercises entries in Settings now show how many each contains. + The Abdominal figure's arms no longer bend backward at the elbow, and a few other form guides lose their subtly hyperextended joints. The exercise library more than doubles — deadlifts, squats, lunges, bench presses, dumbbell work, pull-ups, and the cable stations — each with its own animated form guide. diff --git a/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift b/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift index 2657bd1..87e198a 100644 --- a/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift +++ b/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift @@ -372,10 +372,10 @@ struct WorkoutLogListView: View { // MARK: - Workout Log Row -/// One exercise line item: the status checkbox, the exercise name with a -/// prominent sets × reps line under it, and a trailing column for the load. -/// Weight only appears for weighted exercises — a bodyweight exercise -/// (`LoadType.none`) shows no load at all. +/// One exercise line item: the status checkbox, the exercise name with the +/// machine settings line under it, and a trailing column with the load on top +/// and sets × reps below. Weight only appears for weighted exercises — a +/// bodyweight exercise (`LoadType.none`) shows no load at all. private struct WorkoutLogRow: View { let status: CheckboxStatus let log: WorkoutLogDocument @@ -385,21 +385,25 @@ private struct WorkoutLogRow: View { private var loadType: LoadType { LoadType(rawValue: log.loadType) ?? .none } - /// Prominent "3 × 12" (or "3 × 5 min" for timed exercises). - private var setsAndReps: String { + /// "3 × 12" (or "3 × 5 min" for timed exercises) with the numbers in full + /// strength and the × dimmed, so the counts read at a glance. + private var setsAndReps: Text { + let times = Text(" × ").foregroundStyle(.secondary) + let count: String if loadType == .duration { let mins = log.durationSeconds / 60 let secs = log.durationSeconds % 60 if mins > 0 && secs > 0 { - return "\(log.sets) × \(mins)m \(secs)s" + count = "\(mins)m \(secs)s" } else if mins > 0 { - return "\(log.sets) × \(mins) min" + count = "\(mins) min" } else { - return "\(log.sets) × \(secs) sec" + count = "\(secs) sec" } } else { - return "\(log.sets) × \(log.reps)" + count = "\(log.reps)" } + return Text("\(log.sets)") + times + Text(count) } private var showsWeight: Bool { loadType == .weight } @@ -415,8 +419,16 @@ private struct WorkoutLogRow: View { return parts.isEmpty ? nil : parts.joined(separator: " · ") } + /// Top-align when either column stacks two lines (weight over sets × reps, + /// or name over settings), so the first lines pair up. A single-line row — + /// a bodyweight exercise with no settings — centers against the checkbox + /// instead of hanging off its top edge. + private var rowAlignment: VerticalAlignment { + showsWeight || settingsSummary != nil ? .top : .center + } + var body: some View { - HStack(alignment: .top) { + HStack(alignment: rowAlignment) { Button { onCheckboxTap() } label: { @@ -428,44 +440,44 @@ private struct WorkoutLogRow: View { } .buttonStyle(.plain) - VStack(alignment: .leading, spacing: 2) { + VStack(alignment: .leading, spacing: 4) { Text(log.exerciseName) .font(.headline) .foregroundColor(.primary) - Text(setsAndReps) - .font(.title3.weight(.semibold)) - .monospacedDigit() - .foregroundStyle(.primary) - } - Spacer() - - VStack(alignment: .trailing, spacing: 4) { - if showsWeight { - Text(weightUnit.format(log.weight)) - .font(.title3.weight(.semibold)) - .monospacedDigit() - .foregroundStyle(.primary) - } - - // Machine comfort settings — shown when the log carries settings, or - // for any exercise the authored library identifies as machine-based - // (same rule as ExerciseView), so older logs offer the affordance - // before settings are first recorded. Tapping opens the editor sheet; - // `.plain` keeps the tap from firing the row's NavigationLink (same - // trick as the checkbox button). - if log.machineSettings != nil - || ExerciseInfoLibrary.info(for: log.exerciseName)?.isMachineBased == true { + // Machine comfort settings — the line appears only once settings are + // actually recorded (an unconfigured machine shows nothing; recording + // the first values happens in the exercise's edit screen or progress + // flow). Tapping opens the editor sheet; `.plain` keeps the tap from + // firing the row's NavigationLink (same trick as the checkbox). + if let settingsSummary { Button(action: onSettingsTap) { - Label(settingsSummary ?? "Settings", systemImage: "slider.horizontal.3") - .font(.footnote.weight(.medium)) - .foregroundStyle(Color.accentColor) - .multilineTextAlignment(.trailing) + Label(settingsSummary, systemImage: "slider.horizontal.3") + .font(.subheadline.weight(.medium)) + .foregroundStyle(.secondary) .lineLimit(2) } .buttonStyle(.plain) } } + + Spacer() + + // The weight matches the exercise name's font so the two sit on the + // same visual line; sets × reps keeps its larger glanceable size below. + VStack(alignment: .trailing, spacing: 4) { + if showsWeight { + Text(weightUnit.format(log.weight)) + .font(.headline) + .monospacedDigit() + .foregroundStyle(.primary) + } + + setsAndReps + .font(.title3) + .monospacedDigit() + .foregroundStyle(.primary) + } } .frame(maxWidth: .infinity, alignment: .leading) }