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
@@ -1,52 +0,0 @@
//
// CheckboxListItem.swift
// Workouts
//
// Created by rzen on 7/13/25 at 10:42 AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct CheckboxListItem: View {
var status: CheckboxStatus
var title: String
var subtitle: String?
var count: Int?
var onCheckboxTap: (() -> Void)? = nil
var body: some View {
HStack(alignment: .top) {
Button {
onCheckboxTap?()
} label: {
Image(systemName: status.systemName)
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
.foregroundStyle(status.color)
}
.buttonStyle(.plain)
VStack(alignment: .leading) {
Text("\(title)")
.font(.headline)
.foregroundColor(.primary)
HStack(alignment: .bottom) {
if let subtitle = subtitle {
Text("\(subtitle)")
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
Spacer()
if let count = count {
Text("\(count)")
.font(.caption)
.foregroundColor(.gray)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
+3 -3
View File
@@ -19,8 +19,8 @@ enum CheckboxStatus {
switch self {
case .checked: .accentColor
case .unchecked: .gray
case .intermediate: .gray
case .cancelled: .red
case .intermediate: .accentColor
case .cancelled: .gray
}
}
@@ -29,7 +29,7 @@ enum CheckboxStatus {
case .checked: "checkmark.circle.fill"
case .unchecked: "circle"
case .intermediate: "ellipsis.circle"
case .cancelled: "xmark.circle"
case .cancelled: "wrongwaysign"
}
}
}
@@ -0,0 +1,70 @@
//
// MachineSettingsEditor.swift
// Workouts
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
/// Reusable editor for an ordered list of machine comfort settings (seat height,
/// back-rest incline, pin position). Drop it into a `Form`/`List` `Section`: it
/// renders one `name`/`value` row per setting with swipe-to-delete and
/// drag-to-reorder, plus an "Add Setting" row. Shared by the exercise add/edit
/// screen (Surface 1) and the in-workout settings sheet (Surface 2).
///
/// `MachineSetting` is a plain Codable wire type with no identity, so the editor
/// keeps a per-row `UUID` internally (stable across edits/reorders, unlike an
/// index) and mirrors changes back into the `settings` binding. The editor is the
/// sole mutator of `settings` while it's on screen, so a one-way seed at init is
/// enough there's no external write to reconcile.
struct MachineSettingsEditor: View {
@Binding var settings: [MachineSetting]
@State private var rows: [Row]
init(settings: Binding<[MachineSetting]>) {
_settings = settings
_rows = State(initialValue: settings.wrappedValue.map(Row.init))
}
var body: some View {
ForEach($rows) { $row in
HStack {
TextField("Setting", text: $row.name)
.textInputAutocapitalization(.words)
TextField("Value", text: $row.value)
.multilineTextAlignment(.trailing)
.foregroundStyle(.secondary)
.frame(maxWidth: 120)
}
}
.onDelete { rows.remove(atOffsets: $0) }
.onMove { rows.move(fromOffsets: $0, toOffset: $1) }
Button {
withAnimation { rows.append(Row(MachineSetting(name: "", value: ""))) }
} label: {
Label("Add Setting", systemImage: "plus.circle.fill")
}
// Mirror the identified rows back into the plain wire-type binding on every
// edit/add/delete/reorder.
.onChange(of: rows) { _, newRows in
settings = newRows.map(\.setting)
}
}
/// Session-only identified wrapper so `ForEach` and reordering have a stable
/// identity that survives text edits.
fileprivate struct Row: Identifiable, Equatable {
let id = UUID()
var name: String
var value: String
init(_ setting: MachineSetting) {
name = setting.name
value = setting.value
}
var setting: MachineSetting { MachineSetting(name: name, value: value) }
}
}