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:
@@ -1,5 +1,9 @@
|
|||||||
**July 2026**
|
**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 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.
|
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
|
// MARK: - Workout Log Row
|
||||||
|
|
||||||
/// One exercise line item: the status checkbox, the exercise name with a
|
/// One exercise line item: the status checkbox, the exercise name with the
|
||||||
/// prominent sets × reps line under it, and a trailing column for the load.
|
/// machine settings line under it, and a trailing column with the load on top
|
||||||
/// Weight only appears for weighted exercises — a bodyweight exercise
|
/// and sets × reps below. Weight only appears for weighted exercises — a
|
||||||
/// (`LoadType.none`) shows no load at all.
|
/// bodyweight exercise (`LoadType.none`) shows no load at all.
|
||||||
private struct WorkoutLogRow: View {
|
private struct WorkoutLogRow: View {
|
||||||
let status: CheckboxStatus
|
let status: CheckboxStatus
|
||||||
let log: WorkoutLogDocument
|
let log: WorkoutLogDocument
|
||||||
@@ -385,21 +385,25 @@ private struct WorkoutLogRow: View {
|
|||||||
|
|
||||||
private var loadType: LoadType { LoadType(rawValue: log.loadType) ?? .none }
|
private var loadType: LoadType { LoadType(rawValue: log.loadType) ?? .none }
|
||||||
|
|
||||||
/// Prominent "3 × 12" (or "3 × 5 min" for timed exercises).
|
/// "3 × 12" (or "3 × 5 min" for timed exercises) with the numbers in full
|
||||||
private var setsAndReps: String {
|
/// 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 {
|
if loadType == .duration {
|
||||||
let mins = log.durationSeconds / 60
|
let mins = log.durationSeconds / 60
|
||||||
let secs = log.durationSeconds % 60
|
let secs = log.durationSeconds % 60
|
||||||
if mins > 0 && secs > 0 {
|
if mins > 0 && secs > 0 {
|
||||||
return "\(log.sets) × \(mins)m \(secs)s"
|
count = "\(mins)m \(secs)s"
|
||||||
} else if mins > 0 {
|
} else if mins > 0 {
|
||||||
return "\(log.sets) × \(mins) min"
|
count = "\(mins) min"
|
||||||
} else {
|
} else {
|
||||||
return "\(log.sets) × \(secs) sec"
|
count = "\(secs) sec"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return "\(log.sets) × \(log.reps)"
|
count = "\(log.reps)"
|
||||||
}
|
}
|
||||||
|
return Text("\(log.sets)") + times + Text(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var showsWeight: Bool { loadType == .weight }
|
private var showsWeight: Bool { loadType == .weight }
|
||||||
@@ -415,8 +419,16 @@ private struct WorkoutLogRow: View {
|
|||||||
return parts.isEmpty ? nil : parts.joined(separator: " · ")
|
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 {
|
var body: some View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: rowAlignment) {
|
||||||
Button {
|
Button {
|
||||||
onCheckboxTap()
|
onCheckboxTap()
|
||||||
} label: {
|
} label: {
|
||||||
@@ -428,44 +440,44 @@ private struct WorkoutLogRow: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(log.exerciseName)
|
Text(log.exerciseName)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
Text(setsAndReps)
|
|
||||||
.font(.title3.weight(.semibold))
|
|
||||||
.monospacedDigit()
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
// Machine comfort settings — the line appears only once settings are
|
||||||
|
// actually recorded (an unconfigured machine shows nothing; recording
|
||||||
VStack(alignment: .trailing, spacing: 4) {
|
// the first values happens in the exercise's edit screen or progress
|
||||||
if showsWeight {
|
// flow). Tapping opens the editor sheet; `.plain` keeps the tap from
|
||||||
Text(weightUnit.format(log.weight))
|
// firing the row's NavigationLink (same trick as the checkbox).
|
||||||
.font(.title3.weight(.semibold))
|
if let settingsSummary {
|
||||||
.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 {
|
|
||||||
Button(action: onSettingsTap) {
|
Button(action: onSettingsTap) {
|
||||||
Label(settingsSummary ?? "Settings", systemImage: "slider.horizontal.3")
|
Label(settingsSummary, systemImage: "slider.horizontal.3")
|
||||||
.font(.footnote.weight(.medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundStyle(Color.accentColor)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.trailing)
|
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.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)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user