Add machine comfort settings and tighten the document schemas

Machine-based exercises now carry ordered name/value comfort settings
(seat height, back-rest position, ...) on both ExerciseDocument and, as a
plan-time snapshot, WorkoutLogDocument — the optional array doubles as the
machine flag. Editable from the exercise editor's new Machine section and
from the workout row's settings sheet, whose mid-workout edits write back
to the originating split (workout saved first, so seed clone-on-edit
repointing can't clobber the log edit).

Schema tightening rides the same rev: splits bump to v2 (weight-reminder
fields and the unused exercise category removed), workouts to v3 (the
derived `completed` flag removed; status is the single source). Starter
seeds regenerated at v2 with unchanged ULIDs; SwiftData cache schema
bumped to rebuild. SCHEMA.md documents the shapes.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
2026-07-06 16:29:44 -04:00
parent fce8fa4c17
commit 2c1e4759ae
33 changed files with 1132 additions and 586 deletions
@@ -23,7 +23,6 @@ struct ExerciseAddEditView: View {
// Local editable state
@State private var exerciseName: String
@State private var originalWeight: Int
@State private var loadType: LoadType
@State private var minutes: Int
@State private var seconds: Int
@@ -31,9 +30,11 @@ struct ExerciseAddEditView: View {
@State private var weightOnes: Int
@State private var reps: Int
@State private var sets: Int
@State private var weightReminderWeeks: Int
@State private var weightLastUpdated: Date?
@State private var category: ExerciseCategory
// Machine comfort settings. `machineEnabled` mirrors the optionality of the
// stored `machineSettings` ON persists a (possibly empty) array, OFF persists
// nil. `machineSettings` holds the ordered rows while the toggle is on.
@State private var machineEnabled: Bool
@State private var machineSettings: [MachineSetting]
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
init(exercise: Exercise, split: Split) {
@@ -42,7 +43,6 @@ struct ExerciseAddEditView: View {
let w = exercise.weight
_exerciseName = State(initialValue: exercise.name)
_originalWeight = State(initialValue: w)
_loadType = State(initialValue: exercise.loadTypeEnum)
_minutes = State(initialValue: exercise.durationMinutes)
_seconds = State(initialValue: exercise.durationSeconds)
@@ -50,9 +50,8 @@ struct ExerciseAddEditView: View {
_weightOnes = State(initialValue: w % 10)
_reps = State(initialValue: exercise.reps)
_sets = State(initialValue: exercise.sets)
_weightReminderWeeks = State(initialValue: exercise.weightReminderTimeIntervalWeeks)
_weightLastUpdated = State(initialValue: exercise.weightLastUpdated)
_category = State(initialValue: exercise.categoryEnum)
_machineEnabled = State(initialValue: exercise.machineSettings != nil)
_machineSettings = State(initialValue: exercise.machineSettings ?? [])
}
var body: some View {
@@ -100,19 +99,6 @@ struct ExerciseAddEditView: View {
}
}
Section(
header: Text("Category"),
footer: Text("The split screen groups its exercises by category, with the warm-up listed first.")
) {
Picker("", selection: $category) {
ForEach(ExerciseCategory.displayOrder, id: \.self) { cat in
Text(cat.displayName)
.tag(cat)
}
}
.pickerStyle(.segmented)
}
Section(
header: Text("Load Type"),
footer: Text("For bodyweight exercises choose None. For resistance or weight training select Weight. For exercises that are time oriented (like plank or meditation) select Time.")
@@ -170,16 +156,17 @@ struct ExerciseAddEditView: View {
}
}
Section(header: Text("Weight Increase")) {
HStack {
Text("Remind every \(weightReminderWeeks) weeks")
Spacer()
Stepper("", value: $weightReminderWeeks, in: 0...366)
}
if let lastUpdated = weightLastUpdated {
Text("Last weight change \(Date().humanTimeInterval(to: lastUpdated)) ago")
Section(
header: Text("Machine"),
footer: Text("Turn on for machine-based exercises to record comfort settings (seat height, back-rest incline, pin position…). During a workout you can adjust these and update this default.")
) {
Toggle("Machine-based", isOn: $machineEnabled.animation())
if machineEnabled {
MachineSettingsEditor(settings: $machineSettings)
}
}
}
.sheet(isPresented: $showingExercisePicker) {
ExercisePickerView { exerciseNames in
@@ -206,7 +193,6 @@ struct ExerciseAddEditView: View {
private func saveExercise() {
let newWeight = weightTens + weightOnes
let updatedWeightDate: Date? = newWeight != originalWeight ? Date() : weightLastUpdated
let durationSecs = minutes * 60 + seconds
var doc = SplitDocument(from: split)
@@ -217,9 +203,8 @@ struct ExerciseAddEditView: View {
doc.exercises[idx].weight = newWeight
doc.exercises[idx].loadType = loadType.rawValue
doc.exercises[idx].durationSeconds = durationSecs
doc.exercises[idx].weightLastUpdated = updatedWeightDate
doc.exercises[idx].weightReminderWeeks = weightReminderWeeks
doc.exercises[idx].category = category.rawValue
// ON a (possibly empty) array; OFF nil, discarding any prior settings.
doc.exercises[idx].machineSettings = machineEnabled ? machineSettings : nil
}
doc.updatedAt = Date()
Task { await sync.save(split: doc) }