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 routine 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 `RoutineDocument`; 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 case warmUp = 6 case stretching = 7 case mindAndBody = 8 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" case .warmUp: "Warm-Up" case .stretching: "Stretching" case .mindAndBody: "Mind & Body" } } 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" case .warmUp: "figure.arms.open" case .stretching: "figure.flexibility" case .mindAndBody: "figure.mind.and.body" } } } /// 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)" } } // MARK: - Goals /// The predefined practice-goal categories. Deliberately a small, closed set — /// schedules reference them by raw value, so raw values are stable persisted data. enum GoalKind: String, CaseIterable, Codable, Sendable, Identifiable { // Declaration order IS the initial board order (before any user reorder): // the day reads top-to-bottom — morning mobility, then cardio, then strength, // with mindfulness closing it out. case mobility, cardio, strength, mindfulness var id: String { rawValue } var displayName: String { switch self { case .strength: "Strength" // Deliberately double-barreled: the same practice serves performance // ("endurance") and health ("cardio") motivations — the name excludes neither. case .cardio: "Endurance/Cardio" case .mobility: "Mobility" case .mindfulness: "Mindfulness" } } var systemImage: String { switch self { case .strength: "dumbbell.fill" case .cardio: "figure.run" case .mobility: "figure.flexibility" case .mindfulness: "brain.head.profile" } } /// A hue name `Color.color(from:)` understands. Four distinct hues so the goals /// read apart at a glance. var colorName: String { switch self { case .strength: "red" case .cardio: "orange" case .mobility: "teal" case .mindfulness: "purple" } } } extension GoalKind { static let orderDefaultsKey = "goalKindOrder" /// All cases in the user's chosen order: stored raw values first (unknown ones /// dropped, duplicates collapsed), then any cases missing from the stored list in /// declaration order — so newly added kinds appear automatically. static func orderedCases(defaults: UserDefaults = .standard) -> [GoalKind] { let stored = defaults.stringArray(forKey: orderDefaultsKey) ?? [] var ordered: [GoalKind] = [] var seen = Set() for raw in stored { if let kind = GoalKind(rawValue: raw), seen.insert(kind).inserted { ordered.append(kind) } } for kind in GoalKind.allCases where !seen.contains(kind) { ordered.append(kind) } return ordered } /// Persist the display order as `[String]` of raw values. static func saveOrder(_ kinds: [GoalKind], defaults: UserDefaults = .standard) { defaults.set(kinds.map(\.rawValue), forKey: orderDefaultsKey) } } // MARK: - Schedules /// How often a schedule expects its routine. Raw values are persisted in ScheduleDocument. enum ScheduleRecurrence: String, CaseIterable, Codable, Sendable { case once, daily, fixedDays var displayName: String { switch self { case .once: "Once" case .daily: "Daily" case .fixedDays: "Fixed Days" } } } extension ScheduleRecurrence { /// Human-readable recurrence description shared by `ScheduleDocument` and the /// `Schedule` cache entity, so both render identically. `weekdays` uses Calendar /// weekday numbers (1 = Sun … 7 = Sat); `date` is the one-off day (`.once` only). /// A degenerate case (fixed days with no weekdays, a nil date) falls back to the /// recurrence's own `displayName`. func summary(weekdays: [Int]?, date: Date?) -> String { switch self { case .once: return date?.formatDate() ?? displayName // "Jul 12, 2026" / "Once" case .daily: return displayName // "Daily" case .fixedDays: // A fixed Gregorian calendar's `shortWeekdaySymbols` is 0-indexed from // Sunday, so weekday number `n` maps to index `n - 1` regardless of locale. let symbols = Calendar(identifier: .gregorian).shortWeekdaySymbols let names = (weekdays ?? []).sorted().compactMap { n -> String? in (1...7).contains(n) ? symbols[n - 1] : nil } return names.isEmpty ? displayName : Self.joined(names) } } /// Join weekday short names conversationally: "Mon", "Mon & Thu", "Mon, Wed & Fri". private static func joined(_ names: [String]) -> String { switch names.count { case 0: return "" case 1: return names[0] case 2: return "\(names[0]) & \(names[1])" default: return names.dropLast().joined(separator: ", ") + " & " + (names.last ?? "") } } }