Every completed set now writes a SetEntry (reps/weight or seconds), pre-filled from the plan by transition(to:) so the list checkbox, both run flows, and One More all capture for free; reset clears, skip keeps partials. The rest and finish pages show the just-done set as a pill that opens a stepper sheet for correcting reps and weight (2.5 lb / 1.25 kg steps). The Weight Progression chart plots the top-set actual weight and workout volume sums recorded sets, both falling back to the plan for legacy logs via effectiveSetEntries. Storage side of UX #3 rides along: plan weights are Double now. Schema bumps: SplitDocument 2→3, WorkoutDocument 3→4 (a fractional weight fails an older Int decode, and a rewrite would strip the irreplaceable actuals), SwiftData cache 4→5. A per-log updatedAt is reserved for the future cross-device log merge. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
116 lines
3.6 KiB
Swift
116 lines
3.6 KiB
Swift
import Foundation
|
|
|
|
/// Completion state of a workout or an individual exercise log. Persisted as its
|
|
/// raw string in both the JSON documents and the SwiftData cache.
|
|
enum WorkoutStatus: String, CaseIterable, Codable, Sendable {
|
|
case notStarted
|
|
case inProgress
|
|
case completed
|
|
case skipped
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .notStarted: "Not Started"
|
|
case .inProgress: "In Progress"
|
|
case .completed: "Completed"
|
|
case .skipped: "Skipped"
|
|
}
|
|
}
|
|
|
|
var name: String { displayName }
|
|
}
|
|
|
|
/// How an exercise's effort is measured. Persisted as its raw `Int` value.
|
|
enum LoadType: Int, CaseIterable, Codable, Sendable {
|
|
case none = 0
|
|
case weight = 1
|
|
case duration = 2
|
|
|
|
var name: String {
|
|
switch self {
|
|
case .none: "None"
|
|
case .weight: "Weight"
|
|
case .duration: "Duration"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The kind of training a split represents — used to tag the watch's Apple Health
|
|
/// workout so it lands in the right category (and credits the rings correctly).
|
|
/// Persisted as its raw `Int` on `SplitDocument`; the `HKWorkoutActivityType`
|
|
/// mapping lives in `Shared/HealthKit/HealthKitMapping.swift` to keep this enum
|
|
/// HealthKit-free.
|
|
enum WorkoutActivityType: Int, CaseIterable, Codable, Sendable {
|
|
case traditionalStrength = 0 // default
|
|
case functionalStrength = 1
|
|
case hiit = 2
|
|
case coreTraining = 3
|
|
case cardio = 4
|
|
case cycling = 5
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .traditionalStrength: "Strength Training"
|
|
case .functionalStrength: "Functional Strength"
|
|
case .hiit: "HIIT"
|
|
case .coreTraining: "Core Training"
|
|
case .cardio: "Cardio"
|
|
case .cycling: "Cycling"
|
|
}
|
|
}
|
|
|
|
var systemImage: String {
|
|
switch self {
|
|
case .traditionalStrength: "dumbbell.fill"
|
|
case .functionalStrength: "figure.strengthtraining.functional"
|
|
case .hiit: "figure.highintensity.intervaltraining"
|
|
case .coreTraining: "figure.core.training"
|
|
case .cardio: "figure.run"
|
|
case .cycling: "figure.outdoor.cycle"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Where a workout's health metrics came from: real watch sensors, or — for records
|
|
/// written by app versions up to 2.x — a phone-side MET estimate made when no watch
|
|
/// session ran. The phone no longer writes estimates (recording is watch-only), but
|
|
/// `phoneEstimate` is retained so legacy documents on disk still decode. Persisted in
|
|
/// `WorkoutMetrics`.
|
|
enum MetricSource: String, Codable, Sendable {
|
|
case watch
|
|
case phoneEstimate
|
|
}
|
|
|
|
/// Display unit for weights. The stored weight values are unit-less numbers and
|
|
/// are never rewritten when this changes — switching only relabels what's shown.
|
|
enum WeightUnit: String, CaseIterable, Codable, Sendable {
|
|
case lb
|
|
case kg
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .lb: "Pounds (lb)"
|
|
case .kg: "Kilograms (kg)"
|
|
}
|
|
}
|
|
|
|
var abbreviation: String {
|
|
switch self {
|
|
case .lb: "lb"
|
|
case .kg: "kg"
|
|
}
|
|
}
|
|
|
|
/// Render a stored weight value with the current unit's label, trimming a
|
|
/// trailing ".0" so whole values stay clean: "45 lb", "42.5 kg".
|
|
func format(_ value: Double) -> String {
|
|
let text = value.truncatingRemainder(dividingBy: 1) == 0
|
|
? String(Int(value))
|
|
: String(value)
|
|
return "\(text) \(abbreviation)"
|
|
}
|
|
|
|
/// Render a stored weight value with the current unit's label, e.g. "135 lb".
|
|
func format(_ value: Int) -> String { "\(value) \(abbreviation)" }
|
|
}
|