Let routines rename exercises and repeat them for interval segments

An exercise's name is now a per-routine display name: a new optional
libraryName on ExerciseDocument (snapshotted onto WorkoutLogDocument at
plan time) keeps the link to the bundled library exercise, and every
figure/info/cue lookup resolves libraryName ?? name. Deliberately not
schema-bumped — an older app dropping the key only strands the
figure link, same rationale as activityType. Cache schema bumped to 9
for the new columns.

The picker no longer filters out exercises already in the routine
(an "×N" badge marks them instead), exercise rows gain a leading
Duplicate swipe that clones an entry in place, and the edit sheet gets
a Name field with the library exercise shown read-only above it.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
This commit is contained in:
2026-07-12 18:38:14 -04:00
parent d04526826c
commit df5c77eee1
15 changed files with 203 additions and 38 deletions
+4
View File
@@ -1,5 +1,9 @@
**July 2026**
A routine can now include the same exercise more than once — swipe right on an exercise to duplicate it for interval-style segments.
Exercises can now be renamed per routine, like "Warmup Walk" for a treadmill segment, while their animated figure and guide still follow the original exercise.
A new Running exercise joins the library with its own animated stride figure.
Exercise figures no longer glitch mid-rep — bars, hands, and feet stay planted instead of snapping or windmilling.
+3 -1
View File
@@ -14,7 +14,9 @@ your own iCloud Drive.
(Upper Body / Core / Lower Body machine routines, plus an equipment-free
**Bodyweight Core** circuit with its own warm-up) appear automatically on first
launch; editing one makes it your own copy, and deleted starters can be restored
from Settings.
from Settings. Any exercise can be duplicated and renamed for this routine only
(its figure and guide still come from the original) — build an interval routine
like a treadmill's Warmup / Brisk Walk / Cooldown from one library exercise.
- **Exercise library** — a bundled library of 67 exercises to populate your
splits, authored in `Exercise Library/` with per-exercise setup, cues,
mistakes, progressions, and an anatomically-rigged 3D stick-figure visual
+2
View File
@@ -56,6 +56,7 @@ SwiftData cache entity (`Shared/Model/Entities.swift`) kept in sync by
| `loadType` | Int | | Raw `LoadType` |
| `durationSeconds` | Int | | Total seconds; 0 when not a timed exercise |
| `machineSettings` | [MachineSetting] | ✓ | Ordered machine comfort settings; nil → not a machine exercise, empty → machine exercise with nothing recorded. Doubles as the machine-based flag |
| `libraryName` | String | ✓ | The library exercise `name` was customized from (e.g. one Treadmill segment renamed "Brisk Walk 10 min"); nil → `name` IS the library name. Figure/info/cue lookups resolve `libraryName ?? name`, never `name` directly. *Not schema-bumped* |
## WorkoutDocument — `currentSchemaVersion: 5`
@@ -98,6 +99,7 @@ SwiftData cache entity (`Shared/Model/Entities.swift`) kept in sync by
| `completedAt` | Date | ✓ | Last move to `.completed`. *Not schema-bumped* |
| `setEntries` | [SetEntry] | ✓ | Per-set actuals in set order; nil → legacy file / nothing recorded. Appended as sets complete (pre-filled from the plan); `transition(to:)` fills the missing tail on `.completed` and clears on `.notStarted`; `.skipped` keeps partials. Added in schema v4 |
| `updatedAt` | Date | ✓ | Per-log modification time driving the per-log merge (`WorkoutMergePlanner`); stamped by `transition(to:)` on status flips and `touch()` at content-only edit sites. Nil in files written before v5. Added (unused) in schema v4, written since v5 |
| `libraryName` | String | ✓ | Plan-time snapshot of the exercise's `libraryName` (like sets/reps/weight); nil → `exerciseName` IS the library name. *Not schema-bumped* |
## MachineSetting (embedded in ExerciseDocument and WorkoutLogDocument)
+24 -1
View File
@@ -71,6 +71,19 @@ struct ExerciseDocument: Codable, Sendable, Equatable, Identifiable {
/// empty a machine exercise with nothing recorded yet. This optionality
/// doubles as the machine-based flag there is no separate Bool.
var machineSettings: [MachineSetting]? = nil
/// The library exercise `name` was customized from (e.g. "Treadmill" renamed to
/// "Brisk Walk 10 min" for one segment of an interval routine); nil `name` IS
/// the library name. Optional and deliberately NOT schema-bumped, same rationale
/// as `RoutineDocument.activityType`: an older app dropping it on rewrite only
/// strands the figure/info link for the renamed exercise, which is preferable to
/// quarantining the user's whole routine.
var libraryName: String? = nil
}
extension ExerciseDocument {
/// The name to resolve figure/info/cues against `name` itself unless it was
/// customized, in which case the library exercise it derives from.
var libraryExerciseName: String { libraryName ?? name }
}
/// A single free-form comfort setting for a machine-based exercise. `value` is a
@@ -251,9 +264,18 @@ struct WorkoutLogDocument: Codable, Sendable, Equatable, Identifiable {
/// on two devices merges by the newer `updatedAt`). Nothing writes it yet;
/// absent in older files.
var updatedAt: Date? = nil
/// Plan-time snapshot of the exercise's `libraryName` (like sets/reps/weight).
/// Optional and deliberately NOT schema-bumped, same rationale as
/// `RoutineDocument.activityType`: an older app dropping it on rewrite only
/// strands the figure/info link for a renamed exercise, which is preferable to
/// quarantining the whole workout.
var libraryName: String? = nil
}
extension WorkoutLogDocument {
/// The name to resolve figure/info/cues against see `ExerciseDocument.libraryExerciseName`.
var libraryExerciseName: String { libraryName ?? exerciseName }
/// Build a fresh plan-time log from a routine's exercise. Snapshots the plan
/// fields (sets/reps/weight/duration) *and* the machine comfort settings, so a
/// running workout carries the exercise's settings as its own editable copy
@@ -274,7 +296,8 @@ extension WorkoutLogDocument {
status: WorkoutStatus.notStarted.rawValue,
notes: nil,
date: date,
machineSettings: exercise.machineSettings
machineSettings: exercise.machineSettings,
libraryName: exercise.libraryName
)
}
+18 -2
View File
@@ -80,11 +80,15 @@ final class Exercise {
// `Workout` stores `hrZoneSeconds` as a plain `[Double]?`. `nil` not a
// machine exercise; empty a machine exercise with nothing recorded yet.
var machineSettings: [MachineSetting]?
// The library exercise `name` was customized from; nil `name` IS the
// library name. Mirrors `ExerciseDocument.libraryName`.
var libraryName: String?
var routine: Routine?
init(id: String, name: String, order: Int, sets: Int, reps: Int, weight: Double,
loadType: Int, durationTotalSeconds: Int, machineSettings: [MachineSetting]? = nil) {
loadType: Int, durationTotalSeconds: Int, machineSettings: [MachineSetting]? = nil,
libraryName: String? = nil) {
self.id = id
self.name = name
self.order = order
@@ -94,6 +98,7 @@ final class Exercise {
self.loadType = loadType
self.durationTotalSeconds = durationTotalSeconds
self.machineSettings = machineSettings
self.libraryName = libraryName
}
var loadTypeEnum: LoadType {
@@ -101,6 +106,9 @@ final class Exercise {
set { loadType = newValue.rawValue }
}
/// The name to resolve figure/info/cues against see `ExerciseDocument.libraryExerciseName`.
var libraryExerciseName: String { libraryName ?? name }
/// One-line plan summary for list rows duration exercises show their hold time
/// ("3 × 45 sec"), unloaded ones drop the weight ("3 × 12 reps"), weighted ones
/// keep the full "3 × 10 × 40 lb".
@@ -270,6 +278,9 @@ final class WorkoutLog {
// Mirrors the document's per-log `updatedAt` (named to dodge a future
// SwiftData column collision). Reserved for H1's per-log merge; unused.
var logUpdatedAt: Date?
// Plan-time snapshot of the exercise's `libraryName`. Mirrors
// `WorkoutLogDocument.libraryName`.
var libraryName: String?
var workout: Workout?
@@ -278,7 +289,8 @@ final class WorkoutLog {
statusRaw: String, notes: String?, date: Date,
machineSettings: [MachineSetting]? = nil,
startedAt: Date? = nil, completedAt: Date? = nil,
setEntries: [SetEntry]? = nil, logUpdatedAt: Date? = nil) {
setEntries: [SetEntry]? = nil, logUpdatedAt: Date? = nil,
libraryName: String? = nil) {
self.id = id
self.exerciseName = exerciseName
self.order = order
@@ -296,6 +308,7 @@ final class WorkoutLog {
self.completedAt = completedAt
self.setEntries = setEntries
self.logUpdatedAt = logUpdatedAt
self.libraryName = libraryName
}
var status: WorkoutStatus {
@@ -308,6 +321,9 @@ final class WorkoutLog {
set { loadType = newValue.rawValue }
}
/// The name to resolve figure/info/cues against see `ExerciseDocument.libraryExerciseName`.
var libraryExerciseName: String { libraryName ?? exerciseName }
var durationMinutes: Int {
get { durationTotalSeconds / 60 }
set { durationTotalSeconds = newValue * 60 + durationSeconds }
+7 -4
View File
@@ -17,7 +17,7 @@ extension ExerciseDocument {
init(from e: Exercise) {
self.init(id: e.id, name: e.name, order: e.order, sets: e.sets, reps: e.reps,
weight: e.weight, loadType: e.loadType, durationSeconds: e.durationTotalSeconds,
machineSettings: e.machineSettings)
machineSettings: e.machineSettings, libraryName: e.libraryName)
}
}
@@ -41,7 +41,8 @@ extension WorkoutLogDocument {
status: log.statusRaw, notes: log.notes, date: log.date,
machineSettings: log.machineSettings,
startedAt: log.startedAt, completedAt: log.completedAt,
setEntries: log.setEntries, updatedAt: log.logUpdatedAt)
setEntries: log.setEntries, updatedAt: log.logUpdatedAt,
libraryName: log.libraryName)
}
}
@@ -164,7 +165,7 @@ enum CacheMapper {
private static func makeExercise(_ d: ExerciseDocument) -> Exercise {
Exercise(id: d.id, name: d.name, order: d.order, sets: d.sets, reps: d.reps, weight: d.weight,
loadType: d.loadType, durationTotalSeconds: d.durationSeconds,
machineSettings: d.machineSettings)
machineSettings: d.machineSettings, libraryName: d.libraryName)
}
private static func apply(_ d: ExerciseDocument, to e: Exercise) {
@@ -176,6 +177,7 @@ enum CacheMapper {
e.loadType = d.loadType
e.durationTotalSeconds = d.durationSeconds
e.machineSettings = d.machineSettings
e.libraryName = d.libraryName
}
// MARK: Workout
@@ -228,7 +230,7 @@ enum CacheMapper {
currentStateIndex: d.currentStateIndex, statusRaw: d.status,
notes: d.notes, date: d.date, machineSettings: d.machineSettings,
startedAt: d.startedAt, completedAt: d.completedAt,
setEntries: d.setEntries, logUpdatedAt: d.updatedAt)
setEntries: d.setEntries, logUpdatedAt: d.updatedAt, libraryName: d.libraryName)
}
private static func apply(_ d: WorkoutLogDocument, to l: WorkoutLog) {
@@ -248,6 +250,7 @@ enum CacheMapper {
l.completedAt = d.completedAt
l.setEntries = d.setEntries
l.logUpdatedAt = d.updatedAt
l.libraryName = d.libraryName
}
// MARK: Schedule
@@ -15,7 +15,8 @@ enum WorkoutsModelContainer {
/// 6: added `deletedLogIDs` to `Workout` (per-log merge deletion tombstones).
/// 7: renamed `Split` entity to `Routine`.
/// 8: added `Schedule` cache entity.
static let currentSchemaVersion = 8
/// 9: added `libraryName` to `Exercise` and `WorkoutLog`.
static let currentSchemaVersion = 9
private static let schemaVersionKey = "Workouts.persistenceSchemaVersion"
private static let identityTokenKey = "Workouts.iCloudIdentityToken"
@@ -166,7 +166,8 @@ struct WorkoutLogListView: View {
currentStateIndex: 0,
status: WorkoutStatus.notStarted.rawValue,
notes: nil,
date: doc.start
date: doc.start,
libraryName: exercise.libraryName
)
newLog.touch() // a fresh log carries a definite creation time for the merge
doc.logs.append(newLog)
@@ -23,6 +23,11 @@ struct ExerciseAddEditView: View {
// Local editable state
@State private var exerciseName: String
// The library exercise this row derives from nil only when `exerciseName`
// doesn't (yet) match any bundled exercise. Set alongside `exerciseName` when
// the picker is used; carried through unchanged when the user just retypes the
// name in the TextField, so a rename keeps its figure/info link.
@State private var libraryName: String?
@State private var loadType: LoadType
@State private var minutes: Int
@State private var seconds: Int
@@ -45,6 +50,8 @@ struct ExerciseAddEditView: View {
// shows (and saves back) its whole part until UX #3's UI lands.
let w = Int(exercise.weight)
_exerciseName = State(initialValue: exercise.name)
_libraryName = State(initialValue: exercise.libraryName
?? (ExerciseMotionLibrary.exerciseNames.contains(exercise.name) ? exercise.name : nil))
_loadType = State(initialValue: exercise.loadTypeEnum)
_minutes = State(initialValue: exercise.durationMinutes)
_seconds = State(initialValue: exercise.durationSeconds)
@@ -59,7 +66,10 @@ struct ExerciseAddEditView: View {
var body: some View {
NavigationStack {
Form {
Section(header: Text("Exercise")) {
Section(
header: Text("Exercise"),
footer: Text("You can rename this exercise for this routine — for example \"Warmup Walk\" for Treadmill. The animated figure and exercise guide still come from the original exercise.")
) {
if exerciseName.isEmpty {
Button(action: {
showingExercisePicker = true
@@ -72,7 +82,10 @@ struct ExerciseAddEditView: View {
}
}
} else {
ListItem(title: exerciseName)
if let libraryName {
ListItem(title: libraryName)
}
TextField("Name", text: $exerciseName)
}
}
@@ -172,7 +185,9 @@ struct ExerciseAddEditView: View {
}
.sheet(isPresented: $showingExercisePicker) {
ExercisePickerView { exerciseNames in
exerciseName = exerciseNames.first ?? "Unnamed"
let picked = exerciseNames.first ?? "Unnamed"
exerciseName = picked
libraryName = picked
}
}
.navigationTitle(exerciseName.isEmpty ? "New Exercise" : exerciseName)
@@ -197,9 +212,17 @@ struct ExerciseAddEditView: View {
let newWeight = weightTens + weightOnes
let durationSecs = minutes * 60 + seconds
// An emptied name falls back to the library exercise (or, failing that, the
// prior name) rather than saving a blank display name.
let trimmedName = exerciseName.trimmingCharacters(in: .whitespacesAndNewlines)
let finalName = trimmedName.isEmpty ? (libraryName ?? exercise.name) : trimmedName
var doc = RoutineDocument(from: routine)
if let idx = doc.exercises.firstIndex(where: { $0.id == exercise.id }) {
doc.exercises[idx].name = exerciseName
doc.exercises[idx].name = finalName
// nil when the name matches the library exercise (unrenamed) keeps an
// unrenamed exercise byte-identical on disk.
doc.exercises[idx].libraryName = (libraryName == finalName) ? nil : libraryName
doc.exercises[idx].sets = sets
doc.exercises[idx].reps = reps
doc.exercises[idx].weight = Double(newWeight)
@@ -57,6 +57,14 @@ struct ExerciseListView: View {
title: item.name,
subtitle: "\(item.sets) × \(item.reps) × \(weightUnit.format(item.weight))"
)
.swipeActions(edge: .leading) {
Button {
duplicateExercise(item)
} label: {
Label("Duplicate", systemImage: "plus.square.on.square")
}
.tint(.teal)
}
.swipeActions {
Button {
itemToDelete = item
@@ -90,7 +98,11 @@ struct ExerciseListView: View {
.sheet(isPresented: $showingAddSheet) {
ExercisePickerView(onExerciseSelected: { exerciseNames in
addExercises(names: exerciseNames)
}, allowMultiSelect: true)
}, allowMultiSelect: true,
inRoutineCounts: Dictionary(
routine.exercisesArray.map { ($0.libraryExerciseName, 1) },
uniquingKeysWith: +
))
}
.sheet(item: $itemToEdit) { item in
ExerciseAddEditView(exercise: item, routine: routine)
@@ -135,10 +147,8 @@ struct ExerciseListView: View {
private func addExercises(names: [String]) {
guard let routine else { return }
var doc = RoutineDocument(from: routine)
let existingNames = Set(doc.exercises.map { $0.name })
let base = doc.exercises.count
let newDocs = names
.filter { !existingNames.contains($0) }
.enumerated()
.map { i, exName -> ExerciseDocument in
// Seed from the library's authored `**Defaults:**` when available,
@@ -167,4 +177,22 @@ struct ExerciseListView: View {
doc.updatedAt = Date()
Task { await sync.save(routine: doc) }
}
/// Copy an exercise in place, right after itself the interval-routine case
/// (a treadmill's "Warmup 5 min" / "Brisk Walk 10 min" / segments all derive
/// from the same library exercise). The clone starts out an exact duplicate,
/// plan and any customized name included; renaming it is a follow-up edit.
private func duplicateExercise(_ exercise: Exercise) {
guard let routine else { return }
var doc = RoutineDocument(from: routine)
guard let idx = doc.exercises.firstIndex(where: { $0.id == exercise.id }) else { return }
var copy = doc.exercises[idx]
copy.id = ULID.make()
doc.exercises.insert(copy, at: idx + 1)
for i in doc.exercises.indices {
doc.exercises[i].order = i
}
doc.updatedAt = Date()
Task { await sync.save(routine: doc) }
}
}
@@ -15,10 +15,17 @@ struct ExercisePickerView: View {
var onExerciseSelected: ([String]) -> Void
var allowMultiSelect: Bool = false
/// How many times each exercise already appears in the routine being edited
/// shown as a "×N" badge so picking it again (for interval-style segments like
/// a treadmill's warmup/brisk/cooldown) doesn't read as a mistake. Keyed by
/// library name. Empty for callers with no notion of "already in this routine".
var inRoutineCounts: [String: Int] = [:]
init(onExerciseSelected: @escaping ([String]) -> Void, allowMultiSelect: Bool = false) {
init(onExerciseSelected: @escaping ([String]) -> Void, allowMultiSelect: Bool = false,
inRoutineCounts: [String: Int] = [:]) {
self.onExerciseSelected = onExerciseSelected
self.allowMultiSelect = allowMultiSelect
self.inRoutineCounts = inRoutineCounts
}
var body: some View {
@@ -35,6 +42,10 @@ struct ExercisePickerView: View {
HStack {
Text(exerciseName)
Spacer()
if let count = inRoutineCounts[exerciseName], count > 0 {
Text("×\(count)")
.foregroundStyle(.secondary)
}
if selectedExercises.contains(exerciseName) {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
@@ -46,7 +57,14 @@ struct ExercisePickerView: View {
onExerciseSelected([exerciseName])
dismiss()
}) {
HStack {
Text(exerciseName)
Spacer()
if let count = inRoutineCounts[exerciseName], count > 0 {
Text("×\(count)")
.foregroundStyle(.secondary)
}
}
}
}
}
@@ -88,6 +88,14 @@ struct RoutineDetailView: View {
title: item.name,
subtitle: item.planSummary(weightUnit: weightUnit)
)
.swipeActions(edge: .leading) {
Button {
duplicateExercise(item)
} label: {
Label("Duplicate", systemImage: "plus.square.on.square")
}
.tint(.teal)
}
.swipeActions {
Button {
itemToDelete = item
@@ -128,7 +136,11 @@ struct RoutineDetailView: View {
.sheet(isPresented: $showingExerciseAddSheet) {
ExercisePickerView(onExerciseSelected: { exerciseNames in
addExercises(names: exerciseNames)
}, allowMultiSelect: true)
}, allowMultiSelect: true,
inRoutineCounts: Dictionary(
routine.exercisesArray.map { ($0.libraryExerciseName, 1) },
uniquingKeysWith: +
))
}
.sheet(isPresented: $showingRoutineEditSheet) {
RoutineAddEditView(routine: routine) {
@@ -179,10 +191,8 @@ struct RoutineDetailView: View {
private func addExercises(names: [String]) {
guard let routine else { return }
var doc = RoutineDocument(from: routine)
let existingNames = Set(doc.exercises.map { $0.name })
let base = doc.exercises.count
let newDocs = names
.filter { !existingNames.contains($0) }
.enumerated()
.map { i, exName -> ExerciseDocument in
// Seed from the library's authored `**Defaults:**` when available,
@@ -215,4 +225,22 @@ struct RoutineDetailView: View {
doc.updatedAt = Date()
Task { await sync.save(routine: doc) }
}
/// Copy an exercise in place, right after itself the interval-routine case
/// (a treadmill's "Warmup 5 min" / "Brisk Walk 10 min" / segments all derive
/// from the same library exercise). The clone starts out an exact duplicate,
/// plan and any customized name included; renaming it is a follow-up edit.
private func duplicateExercise(_ exercise: Exercise) {
guard let routine else { return }
var doc = RoutineDocument(from: routine)
guard let idx = doc.exercises.firstIndex(where: { $0.id == exercise.id }) else { return }
var copy = doc.exercises[idx]
copy.id = ULID.make()
doc.exercises.insert(copy, at: idx + 1)
for i in doc.exercises.indices {
doc.exercises[i].order = i
}
doc.updatedAt = Date()
Task { await sync.save(routine: doc) }
}
}
@@ -220,12 +220,17 @@ struct ExerciseProgressView: View {
}?.id
}
/// The next not-yet-resolved log after this one, in plan order the flow hand-off
/// target's full document, so both its display and library names are reachable.
/// Nil when this is the last unfinished exercise.
private var nextLog: WorkoutLogDocument? {
guard let id = nextUnfinishedLogID else { return nil }
return doc.logs.first { $0.id == id }
}
/// Name of the next not-yet-resolved exercise (the flow hand-off target), or nil when
/// this is the last one. Drives the between-exercise rest's "Coming up " preview.
private var nextExerciseName: String? {
guard let id = nextUnfinishedLogID else { return nil }
return doc.logs.first { $0.id == id }?.exerciseName
}
private var nextExerciseName: String? { nextLog?.exerciseName }
/// The between-exercise rest flow mode's terminal page when another exercise follows.
/// This is the only rest whose "next" is a *different* exercise, so it's the one that
@@ -235,12 +240,22 @@ struct ExerciseProgressView: View {
}
/// Which exercise the bottom-half figure should show: the *next* one while resting
/// between exercises (so you see what's coming up), otherwise this exercise.
/// between exercises (so you see what's coming up), otherwise this exercise. This
/// is the display name (the headline); `figureLibraryExerciseName` is its figure/
/// info counterpart.
private var figureExerciseName: String {
if isOnBetweenExerciseRest, let next = nextExerciseName { return next }
return log?.exerciseName ?? ""
}
/// The library name to resolve the bottom-half figure against the same
/// this-exercise-or-next-during-rest choice as `figureExerciseName`, but the
/// library name the animation/info actually key on.
private var figureLibraryExerciseName: String {
if isOnBetweenExerciseRest, let next = nextLog { return next.libraryExerciseName }
return log?.libraryExerciseName ?? ""
}
/// Offset of the work/rest cycle: `1` when a Ready page leads, else `0`.
private var base: Int { showsReady ? 1 : 0 }
@@ -333,21 +348,22 @@ struct ExerciseProgressView: View {
}
/// The figure half of the split, with the big-type exercise headline above the
/// figure in landscape.
/// figure in landscape. `display` names the headline; `figure` is the library
/// name the animated rig is keyed on they diverge once an exercise is renamed.
@ViewBuilder
private func figureHalf(_ name: String) -> some View {
private func figureHalf(display: String, figure: String) -> some View {
if isLandscape {
VStack(spacing: 0) {
Text(name)
Text(display)
.font(.system(size: landscapeNameFontSize, weight: .bold, design: .rounded))
.lineLimit(1)
.minimumScaleFactor(0.5)
.padding(.horizontal)
.padding(.top, 8)
ExerciseFigureSlot(exerciseName: name)
ExerciseFigureSlot(exerciseName: figure)
}
} else {
ExerciseFigureSlot(exerciseName: name)
ExerciseFigureSlot(exerciseName: figure)
}
}
@@ -358,14 +374,14 @@ struct ExerciseProgressView: View {
// the paged flow would be, so the form-guide figure stays on screen.
splitLayout {
CompletedPhaseView(log: log)
figureHalf(log?.exerciseName ?? "")
figureHalf(display: log?.exerciseName ?? "", figure: log?.libraryExerciseName ?? "")
}
.navigationTitle(navBarTitle)
.navigationBarTitleDisplayMode(.inline)
} else if startsSkipped {
splitLayout {
SkippedPhaseView()
figureHalf(log?.exerciseName ?? "")
figureHalf(display: log?.exerciseName ?? "", figure: log?.libraryExerciseName ?? "")
}
.navigationTitle(navBarTitle)
.navigationBarTitleDisplayMode(.inline)
@@ -403,7 +419,7 @@ struct ExerciseProgressView: View {
// resting between exercises (a live preview of what's coming up), otherwise this
// one. The name swap re-loads the figure and carries through the hand-off, so it
// stays put as the next exercise takes over.
figureHalf(figureExerciseName)
figureHalf(display: figureExerciseName, figure: figureLibraryExerciseName)
}
.navigationTitle(navBarTitle)
.navigationBarTitleDisplayMode(.inline)
@@ -839,7 +855,7 @@ struct ExerciseProgressView: View {
private func announceCue() {
guard speakExerciseCues, spokenCueStyle.readsInstructions,
let announcer = speechAnnouncer, let log else { return }
guard let info = ExerciseInfoLibrary.info(for: log.exerciseName) else { return }
guard let info = ExerciseInfoLibrary.info(for: log.libraryExerciseName) else { return }
announcer.speak(info.spokenScript(name: log.exerciseName, brief: true))
}
@@ -119,7 +119,7 @@ struct ExerciseView: View {
// any exercise the authored library identifies as machine-based, so the
// section is discoverable before settings are first recorded.
if log.machineSettings != nil
|| ExerciseInfoLibrary.info(for: log.exerciseName)?.isMachineBased == true {
|| ExerciseInfoLibrary.info(for: log.libraryExerciseName)?.isMachineBased == true {
let settings = log.machineSettings ?? []
Section {
if settings.isEmpty {
@@ -365,7 +365,7 @@ struct WorkoutLogListView: View {
id: ULID.make(), name: name, order: doc.logs.count,
sets: recent.sets, reps: recent.reps, weight: recent.weight,
loadType: recent.loadType, durationSeconds: recent.durationTotalSeconds,
machineSettings: recent.machineSettings
machineSettings: recent.machineSettings, libraryName: recent.libraryName
)
}
let info = ExerciseInfoLibrary.info(for: name)