Redesign the workout exercise row layout

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
This commit is contained in:
2026-07-07 18:06:11 -04:00
parent df6da35aa6
commit 29f04a878a
2 changed files with 55 additions and 39 deletions
+4
View File
@@ -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.
@@ -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)
}