Files
KisaniCal/KisaniCal/Models/ExerciseModels.swift
kutesir bcb66ef691 Workout "mark all complete" + recurring-task done confirmation notification
- Workout window: the header ⋯ menu gains "Mark All Complete" and "Clear All
  Sets" (Manage Workouts preserved). New WorkoutViewModel.setAllSetsDone(_:)
  ticks every set across all sections/exercises and runs the normal completion/
  logging path.
- Recurring tasks: completing an occurrence now posts a quiet "✓ Done. Repeats
  <tomorrow / in 1 week / …>" confirmation via NotificationManager
  .notifyRecurringCompleted, phrased from the next active occurrence. Hooked
  into TaskViewModel.toggleOccurrence; un-completing does nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 23:20:13 +03:00

1094 lines
55 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import Combine
// MARK: - Exercise Models
struct ExerciseSet: Identifiable, Codable {
var id = UUID()
var weight: Double = 0
var reps: Int = 10
var isDone: Bool = false
}
struct Exercise: Identifiable, Codable {
var id = UUID()
var name: String
var sfSymbol: String
var colorIndex: Int = 0
var sets: [ExerciseSet]
var isExpanded: Bool = true
var isDumbbell: Bool = false // per-side input total = weight × 2
var totalSets: Int { sets.count }
var doneSets: Int { sets.filter(\.isDone).count }
var isPartialDone: Bool { doneSets > 0 && doneSets < totalSets }
var isAllDone: Bool { doneSets == totalSets && totalSets > 0 }
}
// MARK: - Workout Section
struct WorkoutSection: Identifiable, Codable {
var id = UUID()
var name: String
var exercises: [Exercise]
var isExpanded: Bool = true
var totalSets: Int { exercises.reduce(0) { $0 + $1.sets.count } }
var doneSets: Int { exercises.reduce(0) { $0 + $1.doneSets } }
var exerciseCount: Int { exercises.count }
}
// MARK: - Workout Program
struct WorkoutProgram: Identifiable, Codable {
let id: UUID
var name: String
var emoji: String
var colorIndex: Int
var sections: [WorkoutSection]
var allExercises: [Exercise] { sections.flatMap(\.exercises) }
var totalSets: Int { sections.reduce(0) { $0 + $1.totalSets } }
var doneSets: Int { sections.reduce(0) { $0 + $1.doneSets } }
var totalExercises: Int { sections.reduce(0) { $0 + $1.exerciseCount } }
init(id: UUID = UUID(), name: String, emoji: String, colorIndex: Int = 0, sections: [WorkoutSection] = []) {
self.id = id; self.name = name; self.emoji = emoji
self.colorIndex = colorIndex; self.sections = sections
}
}
// MARK: - Per-day Workout History
/// A snapshot of what was logged on a given day, captured before the daily reset.
struct WorkoutDayLog: Identifiable, Codable {
var id: String { date } // "yyyy-MM-dd"
let date: String
let programName: String
let programEmoji: String
let exercises: [LoggedExercise]
var totalSets: Int { exercises.reduce(0) { $0 + $1.sets.count } }
var doneSets: Int { exercises.reduce(0) { $0 + $1.sets.filter(\.done).count } }
/// Total training volume in kg for completed sets (Σ weight × reps).
var volume: Double {
exercises.reduce(0) { sum, ex in
sum + ex.sets.reduce(0) { $0 + ($1.done ? $1.weight * Double($1.reps) : 0) }
}
}
}
// MARK: - Workout stats aggregation
enum StatPeriod: String, CaseIterable, Identifiable {
case day = "Day", week = "Week", month = "Month"
var id: String { rawValue }
var calComponent: Calendar.Component {
switch self { case .day: return .day; case .week: return .weekOfYear; case .month: return .month }
}
}
struct WorkoutStat {
var workouts: Int = 0
var sets: Int = 0
var volume: Double = 0
}
struct StatBucket: Identifiable {
let id = UUID()
let label: String
let stat: WorkoutStat
}
struct LoggedExercise: Identifiable, Codable {
var id = UUID()
let name: String
let sfSymbol: String
let sets: [LoggedSet]
var doneSets: Int { sets.filter(\.done).count }
}
struct LoggedSet: Codable {
let weight: Double
let reps: Int
let done: Bool
}
// MARK: - Exercise Template Library
enum ExerciseMuscle: String, CaseIterable, Identifiable {
var id: String { rawValue }
case chest = "Chest"
case back = "Back"
case legs = "Legs"
case shoulders = "Shoulders"
case arms = "Arms"
case core = "Core"
case cardio = "Cardio"
var sfSymbol: String {
switch self {
case .chest: return "figure.strengthtraining.traditional"
case .back: return "figure.pull.up"
case .legs: return "figure.squat"
case .shoulders: return "figure.arms.open"
case .arms: return "dumbbell.fill"
case .core: return "figure.core.training"
case .cardio: return "figure.run"
}
}
}
struct ExerciseTemplate: Identifiable {
let id = UUID()
let name: String
let sfSymbol: String
let muscle: ExerciseMuscle
var isDumbbell: Bool = false
}
let exerciseLibrary: [ExerciseTemplate] = [
// Chest
ExerciseTemplate(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", muscle: .chest),
ExerciseTemplate(name: "Incline Bench Press", sfSymbol: "figure.strengthtraining.traditional", muscle: .chest, isDumbbell: true),
ExerciseTemplate(name: "Incline Dumbbell Press", sfSymbol: "dumbbell.fill", muscle: .chest, isDumbbell: true),
ExerciseTemplate(name: "Decline Bench Press", sfSymbol: "figure.strengthtraining.traditional", muscle: .chest),
ExerciseTemplate(name: "Cable Fly", sfSymbol: "figure.strengthtraining.functional", muscle: .chest),
ExerciseTemplate(name: "Chest Flys", sfSymbol: "dumbbell.fill", muscle: .chest, isDumbbell: true),
ExerciseTemplate(name: "Push-ups", sfSymbol: "figure.gymnastics", muscle: .chest),
ExerciseTemplate(name: "Chest Dips", sfSymbol: "figure.gymnastics", muscle: .chest),
ExerciseTemplate(name: "Cable Crossover", sfSymbol: "figure.strengthtraining.functional", muscle: .chest),
// Back
ExerciseTemplate(name: "Pull-ups", sfSymbol: "figure.pull.up", muscle: .back),
ExerciseTemplate(name: "Wide Pull-ups", sfSymbol: "figure.pull.up", muscle: .back),
ExerciseTemplate(name: "Lat Pulldown", sfSymbol: "figure.pull.up", muscle: .back),
ExerciseTemplate(name: "Barbell Row", sfSymbol: "figure.strengthtraining.traditional", muscle: .back),
ExerciseTemplate(name: "Seated Cable Row", sfSymbol: "figure.rowing", muscle: .back),
ExerciseTemplate(name: "Cable Row", sfSymbol: "figure.rowing", muscle: .back),
ExerciseTemplate(name: "Incline High Row", sfSymbol: "figure.strengthtraining.functional", muscle: .back),
ExerciseTemplate(name: "Face Pull", sfSymbol: "figure.strengthtraining.functional", muscle: .back),
ExerciseTemplate(name: "Deadlift", sfSymbol: "figure.strengthtraining.traditional", muscle: .back),
ExerciseTemplate(name: "T-Bar Row", sfSymbol: "figure.strengthtraining.traditional", muscle: .back),
ExerciseTemplate(name: "Shrugs", sfSymbol: "figure.arms.open", muscle: .back),
ExerciseTemplate(name: "Lat Pulldown Wide Grip", sfSymbol: "figure.pull.up", muscle: .back),
// Legs
ExerciseTemplate(name: "Squat", sfSymbol: "figure.squat", muscle: .legs),
ExerciseTemplate(name: "Hack Squat", sfSymbol: "figure.squat", muscle: .legs),
ExerciseTemplate(name: "Romanian Deadlift", sfSymbol: "figure.strengthtraining.traditional", muscle: .legs),
ExerciseTemplate(name: "Leg Press", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Leg Extension", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Adductor Machine", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Lying Leg Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Seated Leg Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Hamstring Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Standing Calf Raise", sfSymbol: "figure.walk", muscle: .legs),
ExerciseTemplate(name: "Seated Calf Raise", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
ExerciseTemplate(name: "Bulgarian Split Squat", sfSymbol: "figure.squat", muscle: .legs),
ExerciseTemplate(name: "Lunges", sfSymbol: "figure.walk", muscle: .legs),
ExerciseTemplate(name: "Stiff-Legged Deadlift", sfSymbol: "figure.strengthtraining.traditional", muscle: .legs),
ExerciseTemplate(name: "Leg Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
// Shoulders
ExerciseTemplate(name: "Overhead Press", sfSymbol: "figure.arms.open", muscle: .shoulders),
ExerciseTemplate(name: "Dumbbell Shoulder Press",sfSymbol: "dumbbell.fill", muscle: .shoulders, isDumbbell: true),
ExerciseTemplate(name: "Arnold Press", sfSymbol: "dumbbell.fill", muscle: .shoulders, isDumbbell: true),
ExerciseTemplate(name: "Lateral Raises", sfSymbol: "figure.arms.open", muscle: .shoulders, isDumbbell: true),
ExerciseTemplate(name: "Front Raises", sfSymbol: "figure.arms.open", muscle: .shoulders, isDumbbell: true),
ExerciseTemplate(name: "Cable Lateral Raise", sfSymbol: "figure.strengthtraining.functional", muscle: .shoulders),
ExerciseTemplate(name: "Reverse Fly", sfSymbol: "dumbbell.fill", muscle: .shoulders, isDumbbell: true),
// Arms
ExerciseTemplate(name: "Bicep Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
ExerciseTemplate(name: "Hammer Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
ExerciseTemplate(name: "Incline Dumbbell Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
ExerciseTemplate(name: "Incline Hammer Curl", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
ExerciseTemplate(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", muscle: .arms),
ExerciseTemplate(name: "Preacher Curl", sfSymbol: "dumbbell.fill", muscle: .arms),
ExerciseTemplate(name: "Concentration Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
ExerciseTemplate(name: "Dips", sfSymbol: "figure.gymnastics", muscle: .arms),
ExerciseTemplate(name: "Tricep Dips", sfSymbol: "figure.gymnastics", muscle: .arms),
ExerciseTemplate(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", muscle: .arms),
ExerciseTemplate(name: "Straight Bar Pushdown", sfSymbol: "figure.strengthtraining.functional", muscle: .arms),
ExerciseTemplate(name: "Skull Crushers", sfSymbol: "figure.strengthtraining.traditional", muscle: .arms),
ExerciseTemplate(name: "Overhead Tricep Ext", sfSymbol: "dumbbell.fill", muscle: .arms),
ExerciseTemplate(name: "Rope Pushdown", sfSymbol: "figure.strengthtraining.functional", muscle: .arms),
// Core
ExerciseTemplate(name: "Plank", sfSymbol: "figure.core.training", muscle: .core),
ExerciseTemplate(name: "Crunches", sfSymbol: "figure.core.training", muscle: .core),
ExerciseTemplate(name: "Ab Wheel Rollout", sfSymbol: "figure.roll", muscle: .core),
ExerciseTemplate(name: "Russian Twists", sfSymbol: "figure.cross.training", muscle: .core),
ExerciseTemplate(name: "Hanging Leg Raise", sfSymbol: "figure.pull.up", muscle: .core),
ExerciseTemplate(name: "Cable Crunches", sfSymbol: "figure.cooldown", muscle: .core),
// Cardio
ExerciseTemplate(name: "Treadmill", sfSymbol: "figure.run", muscle: .cardio),
ExerciseTemplate(name: "Cycling", sfSymbol: "figure.outdoor.cycle", muscle: .cardio),
ExerciseTemplate(name: "Jump Rope", sfSymbol: "figure.jumprope", muscle: .cardio),
ExerciseTemplate(name: "Rowing Machine", sfSymbol: "figure.rowing", muscle: .cardio),
ExerciseTemplate(name: "Stair Climber", sfSymbol: "figure.stair.stepper", muscle: .cardio),
]
let exerciseSFSymbol: [String: String] = Dictionary(
uniqueKeysWithValues: exerciseLibrary.map { ($0.name, $0.sfSymbol) }
)
// MARK: - Workout View Model
@MainActor
class WorkoutViewModel: ObservableObject {
@Published var programs: [WorkoutProgram]
@Published var schedule: [Int: UUID]
@Published var activeProgramId: UUID
@Published var activeSections: [WorkoutSection]
@Published var workoutDates: [String] = []
@Published var missedDates: [String] = [] // days marked "didn't do it"
@Published var compensations: [String: String] = [:] // rest-day "yyyy-MM-dd" programId
@Published var askCompensate: MissedDay? = nil // triggers the compensation sheet
@Published var history: [WorkoutDayLog] = []
struct MissedDay: Identifiable {
let id: String // "yyyy-MM-dd" of the missed day
let programId: UUID? // what was scheduled that day
let programName: String
}
@Published var restTimerSeconds: Int = 60
@Published var restTimerEnabled: Bool = false
@Published var restCountdown: Int = 0
@Published var restTimerTotal: Int = 0
@Published var sessionStartDate: Date? = nil
var streakDays: Int {
let cal = Calendar.current
let fmt = iso; let dateSet = Set(workoutDates)
var streak = 0
var check = cal.startOfDay(for: Date())
if !dateSet.contains(fmt.string(from: check)) {
check = cal.date(byAdding: .day, value: -1, to: check)!
}
while dateSet.contains(fmt.string(from: check)) {
streak += 1
check = cal.date(byAdding: .day, value: -1, to: check)!
}
return streak
}
private let iso: DateFormatter = {
let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd"; return f
}()
@Published var showAddExercise: Bool = false
@Published var addExerciseTargetSectionId: UUID?
private let kPrograms: String
private let kSchedule: String
private let kActivePid: String
private let kWorkoutDates: String
private let kLastActiveDay: String
private let kWorkoutHistory: String
private let kMissedDates: String
private let kCompensations: String
init() {
let uid = AuthManager.shared.currentUser?.id ?? "shared"
self.kPrograms = "kisani.workout.programs.\(uid)"
self.kSchedule = "kisani.workout.schedule.\(uid)"
self.kActivePid = "kisani.workout.activePid.\(uid)"
self.kWorkoutDates = "kisani.workout.completedDates.\(uid)"
self.kLastActiveDay = "kisani.workout.lastActiveDay.\(uid)"
self.kWorkoutHistory = "kisani.workout.history.\(uid)"
self.kMissedDates = "kisani.workout.missedDates.\(uid)"
self.kCompensations = "kisani.workout.compensations.\(uid)"
// Restore all workout keys from iCloud if missing locally (fresh install / new build)
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.programs.\(uid)")
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.schedule.\(uid)")
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.activePid.\(uid)")
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.completedDates.\(uid)")
// Attempt to restore persisted state
if let data = UserDefaults.kisani.data(forKey: kPrograms),
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
!saved.isEmpty {
// Migrate any legacy emoji SF Symbol name
let emojiMap: [String: String] = [
"🏋️": "figure.strengthtraining.traditional",
"💪": "figure.strengthtraining.functional",
"🔄": "figure.arms.open",
"🦵": "figure.run",
"": "bolt.fill",
"⚡️": "bolt.fill",
"🏃": "figure.run",
"🤸": "figure.yoga",
"🧘": "figure.yoga",
"🚴": "figure.cycling",
"🥊": "figure.boxing",
"🎯": "figure.core.training",
"🫀": "heart.fill",
"🔥": "bolt.fill",
"💥": "figure.highintensity.intervaltraining"
]
let needsMigration = saved.contains(where: { emojiMap[$0.emoji] != nil })
let migrated: [WorkoutProgram] = needsMigration
? saved.map { p in
guard let rep = emojiMap[p.emoji] else { return p }
var copy = p; copy.emoji = rep; return copy
}
: saved
var sched: [Int: UUID] = [:]
if let dict = UserDefaults.kisani.dictionary(forKey: kSchedule) as? [String: String] {
for (k, v) in dict {
if let day = Int(k), let uid = UUID(uuidString: v) { sched[day] = uid }
}
}
schedule = sched
var pid = saved[0].id
if let str = UserDefaults.kisani.string(forKey: kActivePid),
let uid = UUID(uuidString: str),
saved.contains(where: { $0.id == uid }) { pid = uid }
activeProgramId = pid
// Use local `migrated` (not self.programs) self is partially initialized here
programs = migrated
activeSections = migrated.first { $0.id == pid }?.sections ?? migrated[0].sections
if needsMigration { save() }
} else {
// First launch: start blank so the user builds their own routine
let blank = WorkoutProgram(
name: "My Workout", emoji: "dumbbell.fill", colorIndex: 0,
sections: [WorkoutSection(name: "Main", exercises: [])]
)
programs = [blank]
schedule = [:]
activeProgramId = blank.id
activeSections = blank.sections
}
workoutDates = UserDefaults.kisani.stringArray(forKey: kWorkoutDates) ?? []
missedDates = UserDefaults.kisani.stringArray(forKey: kMissedDates) ?? []
compensations = (UserDefaults.kisani.dictionary(forKey: kCompensations) as? [String: String]) ?? [:]
if let hData = UserDefaults.kisani.data(forKey: kWorkoutHistory),
let saved = try? JSONDecoder().decode([WorkoutDayLog].self, from: hData) {
history = saved
}
resetSetsForNewDayIfNeeded()
}
private func save() {
if let data = try? JSONEncoder().encode(programs) {
UserDefaults.kisani.set(data, forKey: kPrograms)
CloudSyncManager.shared.push(data: data, forKey: kPrograms)
}
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
UserDefaults.kisani.set(schedDict, forKey: kSchedule)
UserDefaults.kisani.set(activeProgramId.uuidString, forKey: kActivePid)
UserDefaults.kisani.set(workoutDates, forKey: kWorkoutDates)
UserDefaults.kisani.set(missedDates, forKey: kMissedDates)
UserDefaults.kisani.set(compensations, forKey: kCompensations)
CloudSyncManager.shared.push(value: missedDates, forKey: kMissedDates)
CloudSyncManager.shared.push(value: compensations, forKey: kCompensations)
if let hData = try? JSONEncoder().encode(history) {
UserDefaults.kisani.set(hData, forKey: kWorkoutHistory)
CloudSyncManager.shared.push(data: hData, forKey: kWorkoutHistory)
}
CloudSyncManager.shared.push(value: schedDict, forKey: kSchedule)
CloudSyncManager.shared.push(value: activeProgramId.uuidString, forKey: kActivePid)
CloudSyncManager.shared.push(value: workoutDates, forKey: kWorkoutDates)
}
// MARK: - Daily Reset
/// A fresh calendar day starts a clean log. If the checkbox state currently on
/// screen belongs to an earlier day, wipe every set's `isDone` so re-running the
/// same program on back-to-back days (e.g. Shoulders on Mon AND Tue) starts
/// unchecked. Streak history (`workoutDates`) is never touched here.
/// Safe to call repeatedly it only does work when the day actually changes.
func resetSetsForNewDayIfNeeded() {
let today = iso.string(from: Date())
let last = UserDefaults.kisani.string(forKey: kLastActiveDay)
guard last != today else { return }
if let last = last { // skip on the very first launch
snapshotDay(last) // save what was logged before wiping it
clearAllDoneSets()
applyScheduledProgramForToday() // follow the weekly schedule on a new day
}
UserDefaults.kisani.set(today, forKey: kLastActiveDay)
}
/// History newest-first, for the history view.
var historySorted: [WorkoutDayLog] { history.sorted { $0.date > $1.date } }
/// Capture the active program's completed work for `dayKey` into history.
/// Only records days where at least one set was done; updates in place.
func snapshotDay(_ dayKey: String) {
let logged: [LoggedExercise] = activeSections.flatMap { section in
section.exercises.map { ex in
LoggedExercise(name: ex.name, sfSymbol: ex.sfSymbol,
sets: ex.sets.map { LoggedSet(weight: $0.weight, reps: $0.reps, done: $0.isDone) })
}
}
guard logged.contains(where: { $0.doneSets > 0 }) else { return }
let log = WorkoutDayLog(date: dayKey,
programName: activeProgram?.name ?? "Workout",
programEmoji: activeProgram?.emoji ?? "dumbbell.fill",
exercises: logged)
history.removeAll { $0.date == dayKey }
history.append(log)
save()
}
// MARK: - Stats (daily / weekly / monthly performance)
/// History logs whose date falls inside `interval`.
private func logs(in interval: DateInterval) -> [WorkoutDayLog] {
history.filter { guard let d = iso.date(from: $0.date) else { return false }
return interval.contains(d) }
}
/// The calendar interval for `period`, shifted back by `offset` periods (0 = current).
func statInterval(_ period: StatPeriod, offset: Int) -> DateInterval {
let cal = Calendar.current
let anchor = cal.date(byAdding: period.calComponent, value: offset, to: Date()) ?? Date()
return cal.dateInterval(of: period.calComponent, for: anchor)
?? DateInterval(start: cal.startOfDay(for: anchor), duration: 86400)
}
/// Aggregate workout stats for one period (offset 0 = current, -1 = previous).
func stat(_ period: StatPeriod, offset: Int = 0) -> WorkoutStat {
let logs = logs(in: statInterval(period, offset: offset))
return WorkoutStat(
workouts: logs.count,
sets: logs.reduce(0) { $0 + $1.doneSets },
volume: logs.reduce(0) { $0 + $1.volume }
)
}
/// A series of `count` recent periods, oldest newest, for charting.
func statBuckets(_ period: StatPeriod, count: Int) -> [StatBucket] {
let fmt = DateFormatter()
switch period {
case .day: fmt.dateFormat = "EEE"
case .week: fmt.dateFormat = "MMM d"
case .month: fmt.dateFormat = "MMM"
}
return (0..<count).reversed().map { back in
let interval = statInterval(period, offset: -back)
return StatBucket(label: fmt.string(from: interval.start), stat: stat(period, offset: -back))
}
}
/// The program assigned to today in the weekly schedule (nil = rest day).
var todaysScheduledProgram: WorkoutProgram? { program(for: Date()) }
/// True when a schedule exists but today has no program a rest day.
/// A compensation scheduled for today counts as a workout day, not rest.
var isRestDayToday: Bool {
!schedule.isEmpty && program(for: Date()) == nil
}
/// On a new day, make the active program match the day's schedule so the
/// Workout tab shows today's workout instead of yesterday's. Rest days leave
/// the active program untouched (the view shows a rest state instead).
private func applyScheduledProgramForToday() {
guard let scheduled = todaysScheduledProgram, scheduled.id != activeProgramId else { return }
activeProgramId = scheduled.id
activeSections = scheduled.sections
save()
}
/// Unchecks every set across all programs and clears the in-progress session.
private func clearAllDoneSets() {
for pi in programs.indices {
for si in programs[pi].sections.indices {
for ei in programs[pi].sections[si].exercises.indices {
for setIdx in programs[pi].sections[si].exercises[ei].sets.indices {
programs[pi].sections[si].exercises[ei].sets[setIdx].isDone = false
}
}
}
}
activeSections = programs.first { $0.id == activeProgramId }?.sections ?? activeSections
sessionStartDate = nil
save()
}
func logWorkoutCompleted(on date: Date = Date()) {
let key = iso.string(from: date)
snapshotDay(key) // record today's session in history
guard !workoutDates.contains(key) else { return }
workoutDates.append(key)
save()
let start = sessionStartDate ?? date.addingTimeInterval(-3600)
Task { await HealthKitManager.shared.saveWorkout(start: start, end: date) }
sessionStartDate = nil
}
// Pull completed workout dates from Apple Health and merge into local streak
@MainActor
func syncFromHealthKit() async {
guard HealthKitManager.shared.authorized else { return }
guard let ninetyDaysAgo = Calendar.current.date(byAdding: .day, value: -90, to: Date()) else { return }
let hkDates = await HealthKitManager.shared.fetchWorkoutDates(from: ninetyDaysAgo, to: Date())
guard !hkDates.isEmpty else { return }
var newSet = Set(workoutDates); var updated = false
for d in hkDates where !newSet.contains(d) { newSet.insert(d); updated = true }
if updated { workoutDates = Array(newSet); save() }
}
// Called on foreground picks up "Yes I did it" tapped in notification
@MainActor
func checkPendingHealthConfirm() {
let key = "kisani.workout.pendingConfirm"
guard let dateStr = UserDefaults.kisani.string(forKey: key) else { return }
UserDefaults.kisani.removeObject(forKey: key)
guard !workoutDates.contains(dateStr) else { return }
workoutDates.append(dateStr)
save()
let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd"
if let date = fmt.date(from: dateStr) {
let s = Calendar.current.startOfDay(for: date)
let e = Calendar.current.date(byAdding: .hour, value: 1, to: s) ?? s
Task { await HealthKitManager.shared.saveWorkout(start: s, end: e) }
}
}
func seedStreak(days: Int) {
let cal = Calendar.current
var newDates = Set(workoutDates)
for i in 0..<days {
let d = cal.date(byAdding: .day, value: -i, to: cal.startOfDay(for: Date()))!
newDates.insert(iso.string(from: d))
}
workoutDates = Array(newDates)
save()
}
func setScheduleForDays(_ weekdays: Set<Int>) {
schedule = [:]
for day in weekdays { schedule[day] = activeProgramId }
save()
}
var activeProgram: WorkoutProgram? { programs.first { $0.id == activeProgramId } }
var workoutTitle: String { activeProgram?.name ?? "Workout" }
var workoutEmoji: String { activeProgram?.emoji ?? "dumbbell.fill" }
var totalSets: Int { activeSections.reduce(0) { $0 + $1.totalSets } }
var doneSets: Int { activeSections.reduce(0) { $0 + $1.doneSets } }
var progress: Double { totalSets > 0 ? Double(doneSets) / Double(totalSets) : 0 }
/// True if today already counts toward the streak either marked complete in-app
/// or detected from Apple Health (both land in `workoutDates`).
var isTodayRecorded: Bool { workoutDates.contains(iso.string(from: Date())) }
/// Whether a workout was actually completed on a specific calendar date.
/// Per-date (from `workoutDates`), NOT the program's live set state so
/// finishing one occurrence never marks other days of the same program.
func isWorkoutComplete(on date: Date) -> Bool {
workoutDates.contains(iso.string(from: date))
}
// MARK: - Per-date status (Pending / Done / Missed / Compensation)
enum WorkoutDayStatus { case none, pending, done, missed }
func isWorkoutMissed(on date: Date) -> Bool { missedDates.contains(iso.string(from: date)) }
func isCompensation(on date: Date) -> Bool { compensations[iso.string(from: date)] != nil }
/// Status of the workout scheduled on a specific date. Done wins over missed.
func workoutStatus(on date: Date) -> WorkoutDayStatus {
guard program(for: date) != nil else { return .none }
if isWorkoutComplete(on: date) { return .done }
if isWorkoutMissed(on: date) { return .missed }
return .pending
}
/// Mark one specific day's workout done never touches other occurrences.
func markWorkoutDone(on date: Date) {
let key = iso.string(from: date)
missedDates.removeAll { $0 == key }
logWorkoutCompleted(on: date) // adds to workoutDates, snapshots history, saves
}
/// Mark one specific day's workout missed (undoes a done mark for that day only).
func markWorkoutMissed(on date: Date) {
let key = iso.string(from: date)
workoutDates.removeAll { $0 == key }
if !missedDates.contains(key) { missedDates.append(key) }
save()
}
// MARK: - Compensation (recover a missed workout on a rest day)
/// Upcoming rest days (no scheduled program, no compensation yet), starting tomorrow.
func upcomingRestDays(within days: Int = 14) -> [Date] {
let cal = Calendar.current
let today = cal.startOfDay(for: Date())
return (1...days).compactMap { off -> Date? in
guard let d = cal.date(byAdding: .day, value: off, to: today) else { return nil }
let weekday = cal.component(.weekday, from: d)
guard schedule[weekday] == nil, compensations[iso.string(from: d)] == nil else { return nil }
return d
}
}
func scheduleCompensation(programId: UUID, on date: Date) {
compensations[iso.string(from: date)] = programId.uuidString
save()
}
func removeCompensation(on date: Date) {
compensations.removeValue(forKey: iso.string(from: date))
save()
}
/// Begin the "compensate for this missed day?" flow for a given date.
func promptCompensation(forMissed date: Date) {
let prog = program(for: date)
askCompensate = MissedDay(id: iso.string(from: date),
programId: prog?.id,
programName: prog?.name ?? "Workout")
}
/// Picks up "No, I missed it" tapped on a workout notification (set in background).
@MainActor
func checkPendingMissed() {
let key = "kisani.workout.pendingMissed"
guard let dateStr = UserDefaults.kisani.string(forKey: key) else { return }
UserDefaults.kisani.removeObject(forKey: key)
guard let date = iso.date(from: dateStr) else { return }
markWorkoutMissed(on: date)
promptCompensation(forMissed: date)
}
// MARK: - Program management
func switchProgram(to id: UUID) {
syncBack()
guard let prog = programs.first(where: { $0.id == id }) else { return }
activeProgramId = id
activeSections = prog.sections
save()
}
func addProgram(name: String, emoji: String, colorIndex: Int = 0) {
let defaultSection = WorkoutSection(name: "Main", exercises: [])
programs.append(WorkoutProgram(name: name, emoji: emoji, colorIndex: colorIndex, sections: [defaultSection]))
save()
}
func deleteProgram(_ id: UUID) {
guard id != activeProgramId, programs.count > 1 else { return }
programs.removeAll { $0.id == id }
schedule = schedule.filter { $0.value != id }
save()
}
func renameProgram(_ id: UUID, name: String, emoji: String? = nil) {
guard let idx = programs.firstIndex(where: { $0.id == id }) else { return }
programs[idx].name = name
if let emoji { programs[idx].emoji = emoji }
if id == activeProgramId { objectWillChange.send() }
save()
}
func setSchedule(weekday: Int, programId: UUID?) {
if let pid = programId { schedule[weekday] = pid }
else { schedule.removeValue(forKey: weekday) }
save()
}
func program(for date: Date) -> WorkoutProgram? {
// A compensation scheduled on this (rest) day takes the slot.
if let pidStr = compensations[iso.string(from: date)],
let pid = UUID(uuidString: pidStr),
let prog = programs.first(where: { $0.id == pid }) {
return prog
}
let weekday = Calendar.current.component(.weekday, from: date)
guard let pid = schedule[weekday] else { return nil }
return programs.first { $0.id == pid }
}
// MARK: - Section management
func addSection(name: String) {
activeSections.append(WorkoutSection(name: name, exercises: []))
syncBack()
}
func deleteSection(_ id: UUID) {
activeSections.removeAll { $0.id == id }
syncBack()
}
func renameSection(_ id: UUID, name: String) {
guard let si = activeSections.firstIndex(where: { $0.id == id }) else { return }
activeSections[si].name = name
syncBack()
}
func moveSectionUp(_ id: UUID) {
guard let idx = activeSections.firstIndex(where: { $0.id == id }), idx > 0 else { return }
activeSections.swapAt(idx, idx - 1)
syncBack()
}
func moveSectionDown(_ id: UUID) {
guard let idx = activeSections.firstIndex(where: { $0.id == id }),
idx < activeSections.count - 1 else { return }
activeSections.swapAt(idx, idx + 1)
syncBack()
}
func toggleSectionExpanded(_ id: UUID) {
guard let si = activeSections.firstIndex(where: { $0.id == id }) else { return }
activeSections[si].isExpanded.toggle()
}
// MARK: - Exercise management
func toggleSet(sectionId: UUID, exerciseId: UUID, setId: UUID) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId }),
let setIdx = activeSections[si].exercises[ei].sets.firstIndex(where: { $0.id == setId })
else { return }
let wasMarkedDone = !activeSections[si].exercises[ei].sets[setIdx].isDone
activeSections[si].exercises[ei].sets[setIdx].isDone.toggle()
// The current checkbox state now belongs to today.
UserDefaults.kisani.set(iso.string(from: Date()), forKey: kLastActiveDay)
if sessionStartDate == nil, activeSections[si].exercises[ei].sets[setIdx].isDone {
sessionStartDate = Date()
}
syncBack()
if doneSets == totalSets && totalSets > 0 { logWorkoutCompleted() }
if wasMarkedDone && restTimerEnabled { startRestTimer() }
}
/// Mark every set of one exercise done/undone (long-press quick action).
func setExerciseDone(sectionId: UUID, exerciseId: UUID, done: Bool) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId })
else { return }
for idx in activeSections[si].exercises[ei].sets.indices {
activeSections[si].exercises[ei].sets[idx].isDone = done
}
if done, sessionStartDate == nil { sessionStartDate = Date() }
syncBack()
if doneSets == totalSets && totalSets > 0 { logWorkoutCompleted() }
}
/// Mark every set of the active workout done/undone the "Select all & mark
/// complete" quick action. Logs completion when everything is checked.
func setAllSetsDone(_ done: Bool) {
for si in activeSections.indices {
for ei in activeSections[si].exercises.indices {
for idx in activeSections[si].exercises[ei].sets.indices {
activeSections[si].exercises[ei].sets[idx].isDone = done
}
}
}
if done {
if sessionStartDate == nil { sessionStartDate = Date() }
UserDefaults.kisani.set(iso.string(from: Date()), forKey: kLastActiveDay)
}
syncBack()
if done, doneSets == totalSets && totalSets > 0 { logWorkoutCompleted() }
}
func addSet(sectionId: UUID, to exerciseId: UUID) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId })
else { return }
let last = activeSections[si].exercises[ei].sets.last
activeSections[si].exercises[ei].sets.append(ExerciseSet(weight: last?.weight ?? 0, reps: last?.reps ?? 10))
syncBack()
}
// Active-program convenience wrappers
func addExercise(to sectionId: UUID, name: String, setsCount: Int, repsCount: Int) {
addExercise(programId: activeProgramId, sectionId: sectionId, name: name, setsCount: setsCount, repsCount: repsCount)
}
func deleteExercise(sectionId: UUID, exerciseId: UUID) {
deleteExercise(programId: activeProgramId, sectionId: sectionId, exerciseId: exerciseId)
}
func moveExercise(sectionId: UUID, from: IndexSet, to: Int) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }) else { return }
activeSections[si].exercises.move(fromOffsets: from, toOffset: to)
syncBack()
}
/// Relocate an exercise (within or across sections) for any program via drag-and-drop.
/// Drops the exercise just before `beforeId`; pass nil to append to the target section.
func moveExercise(programId: UUID, exerciseId: UUID, toSection targetSectionId: UUID, before beforeId: UUID?) {
guard exerciseId != beforeId else { return }
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
// Remove from its current section.
var moved: Exercise?
for si in programs[pi].sections.indices {
if let ei = programs[pi].sections[si].exercises.firstIndex(where: { $0.id == exerciseId }) {
moved = programs[pi].sections[si].exercises.remove(at: ei)
break
}
}
guard let ex = moved,
let ti = programs[pi].sections.firstIndex(where: { $0.id == targetSectionId }) else { return }
// Insert before the target row, or append if none specified.
if let beforeId, let idx = programs[pi].sections[ti].exercises.firstIndex(where: { $0.id == beforeId }) {
programs[pi].sections[ti].exercises.insert(ex, at: idx)
} else {
programs[pi].sections[ti].exercises.append(ex)
}
if programId == activeProgramId { activeSections = programs[pi].sections }
save()
}
// MARK: - Universal (any program) operations
func addExercise(programId: UUID, sectionId: UUID, name: String, setsCount: Int, repsCount: Int) {
guard let pi = programs.firstIndex(where: { $0.id == programId }),
let si = programs[pi].sections.firstIndex(where: { $0.id == sectionId })
else { return }
let template = exerciseLibrary.first { $0.name == name }
let symbol = template?.sfSymbol ?? "figure.strengthtraining.functional"
let isDumbb = template?.isDumbbell ?? false
let sets = (0..<setsCount).map { _ in ExerciseSet(weight: 0, reps: repsCount) }
let colorIdx = programs[pi].allExercises.count % 4
programs[pi].sections[si].exercises.append(
Exercise(name: name, sfSymbol: symbol, colorIndex: colorIdx, sets: sets, isDumbbell: isDumbb)
)
if programId == activeProgramId { activeSections = programs[pi].sections }
save()
}
func deleteExercise(programId: UUID, sectionId: UUID, exerciseId: UUID) {
guard let pi = programs.firstIndex(where: { $0.id == programId }),
let si = programs[pi].sections.firstIndex(where: { $0.id == sectionId })
else { return }
programs[pi].sections[si].exercises.removeAll { $0.id == exerciseId }
if programId == activeProgramId { activeSections = programs[pi].sections }
save()
}
func addSection(to programId: UUID, name: String) {
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
programs[pi].sections.append(WorkoutSection(name: name, exercises: []))
if programId == activeProgramId { activeSections = programs[pi].sections }
save()
}
func deleteSection(from programId: UUID, sectionId: UUID) {
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
programs[pi].sections.removeAll { $0.id == sectionId }
if programId == activeProgramId { activeSections = programs[pi].sections }
save()
}
func renameSection(in programId: UUID, sectionId: UUID, name: String) {
guard let pi = programs.firstIndex(where: { $0.id == programId }),
let si = programs[pi].sections.firstIndex(where: { $0.id == sectionId })
else { return }
programs[pi].sections[si].name = name
if programId == activeProgramId { activeSections = programs[pi].sections }
save()
}
func toggleExpanded(sectionId: UUID, exerciseId: UUID) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId })
else { return }
activeSections[si].exercises[ei].isExpanded.toggle()
}
func toggleDumbbell(sectionId: UUID, exerciseId: UUID) {
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId })
else { return }
activeSections[si].exercises[ei].isDumbbell.toggle()
syncBack()
}
// MARK: - Rest Timer
func startRestTimer() {
restTimerTotal = restTimerSeconds
restCountdown = restTimerSeconds
}
func stopRestTimer() {
restCountdown = 0
restTimerTotal = 0
}
private func syncBack() {
guard let idx = programs.firstIndex(where: { $0.id == activeProgramId }) else { return }
programs[idx].sections = activeSections
save()
}
// MARK: - Sample data
static func samplePrograms() -> [WorkoutProgram] {
func s(_ count: Int, reps: Int = 12) -> [ExerciseSet] {
(0..<count).map { _ in ExerciseSet(reps: reps) }
}
let warmUp = WorkoutSection(name: "Warm Up", exercises: [
Exercise(name: "Wide Pull-ups", sfSymbol: "figure.pull.up", colorIndex: 1, sets: s(3, reps: 10)),
Exercise(name: "Dips", sfSymbol: "figure.gymnastics", colorIndex: 0, sets: s(3, reps: 10)),
Exercise(name: "Ab Wheel Rollout", sfSymbol: "figure.roll", colorIndex: 2, sets: s(3, reps: 10)),
])
// lbs kg conversion factor
let lb: Double = 0.453592
// Monday: Chest & Triceps
let mon = WorkoutProgram(name: "Chest & Triceps", emoji: "figure.strengthtraining.traditional", colorIndex: 0, sections: [
warmUp,
WorkoutSection(name: "Chest", exercises: [
// Incline first 70 / 65 / 45 / 35 lbs × 12 reps (per-side dumbbell)
Exercise(name: "Incline Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
ExerciseSet(weight: 70 * lb, reps: 12),
ExerciseSet(weight: 65 * lb, reps: 12),
ExerciseSet(weight: 45 * lb, reps: 12),
ExerciseSet(weight: 35 * lb, reps: 12),
], isDumbbell: true),
// Flat 65 / 45 / 35 lbs × 12 reps
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
ExerciseSet(weight: 65 * lb, reps: 12),
ExerciseSet(weight: 45 * lb, reps: 12),
ExerciseSet(weight: 35 * lb, reps: 12),
]),
// Chest Flys 35 / 25 / 25 lbs × 12 reps (per-side dumbbell)
Exercise(name: "Chest Flys", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: [
ExerciseSet(weight: 35 * lb, reps: 12),
ExerciseSet(weight: 25 * lb, reps: 12),
ExerciseSet(weight: 25 * lb, reps: 12),
], isDumbbell: true),
]),
WorkoutSection(name: "Triceps", exercises: [
// Tricep Pushdown 50 / 45 / 45 kg × 12 reps
Exercise(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: [
ExerciseSet(weight: 50, reps: 12),
ExerciseSet(weight: 45, reps: 12),
ExerciseSet(weight: 45, reps: 12),
]),
// Overhead 50 kg × 3 sets
Exercise(name: "Overhead Tricep Ext", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: [
ExerciseSet(weight: 50, reps: 12),
ExerciseSet(weight: 50, reps: 12),
ExerciseSet(weight: 50, reps: 12),
]),
// Skull Crushers 15 kg × 3 sets
Exercise(name: "Skull Crushers", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
ExerciseSet(weight: 15, reps: 12),
ExerciseSet(weight: 15, reps: 12),
ExerciseSet(weight: 15, reps: 12),
]),
]),
])
// Tuesday: Back & Biceps
let tue = WorkoutProgram(name: "Back & Biceps", emoji: "figure.strengthtraining.functional", colorIndex: 1, sections: [
warmUp,
WorkoutSection(name: "Back", exercises: [
Exercise(name: "Lat Pulldown", sfSymbol: "figure.pull.up", colorIndex: 1, sets: [
ExerciseSet(weight: 70, reps: 12),
ExerciseSet(weight: 60, reps: 12),
ExerciseSet(weight: 60, reps: 12),
]),
Exercise(name: "Cable Row", sfSymbol: "figure.rowing", colorIndex: 0, sets: [
ExerciseSet(weight: 60 * lb, reps: 12),
ExerciseSet(weight: 55 * lb, reps: 12),
ExerciseSet(weight: 55 * lb, reps: 12),
]),
Exercise(name: "Deadlift", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 2, sets: [
ExerciseSet(weight: 70 * lb, reps: 10),
ExerciseSet(weight: 70, reps: 10),
ExerciseSet(weight: 70, reps: 10),
], isDumbbell: true),
]),
WorkoutSection(name: "Biceps", exercises: [
Exercise(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
ExerciseSet(weight: 10, reps: 12),
ExerciseSet(weight: 10, reps: 12),
ExerciseSet(weight: 10, reps: 12),
], isDumbbell: true),
Exercise(name: "Incline Hammer Curl", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: [
ExerciseSet(weight: 30, reps: 12),
ExerciseSet(weight: 30, reps: 12),
ExerciseSet(weight: 30, reps: 12),
], isDumbbell: true),
Exercise(name: "Preacher Curl", sfSymbol: "dumbbell.fill", colorIndex: 1, sets: [
ExerciseSet(weight: 60, reps: 12),
ExerciseSet(weight: 60, reps: 12),
ExerciseSet(weight: 60, reps: 12),
]),
]),
])
// Wednesday: Shoulders, Triceps & Biceps
let wed = WorkoutProgram(name: "Shoulders & Arms", emoji: "figure.arms.open", colorIndex: 2, sections: [
warmUp,
WorkoutSection(name: "Shoulders", exercises: [
Exercise(name: "Reverse Fly", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
Exercise(name: "Front Raises", sfSymbol: "figure.arms.open", colorIndex: 0, sets: s(3)),
Exercise(name: "Lateral Raises", sfSymbol: "figure.arms.open", colorIndex: 1, sets: s(3)),
Exercise(name: "Dumbbell Shoulder Press",sfSymbol: "dumbbell.fill", colorIndex: 3, sets: s(3)),
]),
WorkoutSection(name: "Triceps", exercises: [
Exercise(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
Exercise(name: "Overhead Tricep Ext",sfSymbol: "dumbbell.fill", colorIndex: 0, sets: s(3)),
]),
WorkoutSection(name: "Biceps", exercises: [
Exercise(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
Exercise(name: "Hammer Curls", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: s(3)),
]),
])
// Thursday: Legs & Chest
let thu = WorkoutProgram(name: "Legs & Chest", emoji: "figure.run", colorIndex: 3, sections: [
warmUp,
WorkoutSection(name: "Legs", exercises: [
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 2, sets: s(3)),
Exercise(name: "Leg Extension", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
Exercise(name: "Adductor Machine",sfSymbol: "figure.strengthtraining.functional", colorIndex: 3, sets: s(3)),
Exercise(name: "Lying Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 0, sets: s(3)),
Exercise(name: "Seated Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 2, sets: s(3)),
Exercise(name: "Leg Press", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
]),
WorkoutSection(name: "Chest", exercises: [
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
Exercise(name: "Chest Flys", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
]),
])
// Friday: Full Body
let fri = WorkoutProgram(name: "Full Body", emoji: "bolt.fill", colorIndex: 0, sections: [
warmUp,
WorkoutSection(name: "Upper Body", exercises: [
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
Exercise(name: "Lat Pulldown", sfSymbol: "figure.pull.up", colorIndex: 1, sets: s(3)),
Exercise(name: "Dumbbell Shoulder Press", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: s(3)),
Exercise(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
Exercise(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
]),
WorkoutSection(name: "Lower Body", exercises: [
Exercise(name: "Leg Press", sfSymbol: "figure.strengthtraining.functional", colorIndex: 2, sets: s(3)),
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 1, sets: s(3)),
Exercise(name: "Lying Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 3, sets: s(3)),
]),
])
// Saturday: Full Body
let sat = WorkoutProgram(name: "Full Body", emoji: "figure.highintensity.intervaltraining", colorIndex: 1, sections: [
warmUp,
WorkoutSection(name: "Upper Body", exercises: [
Exercise(name: "Incline Bench Press",sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
Exercise(name: "Cable Row", sfSymbol: "figure.rowing", colorIndex: 1, sets: s(3)),
Exercise(name: "Lateral Raises", sfSymbol: "figure.arms.open", colorIndex: 3, sets: s(3)),
Exercise(name: "Overhead Tricep Ext",sfSymbol: "dumbbell.fill", colorIndex: 0, sets: s(3)),
Exercise(name: "Hammer Curls", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
]),
WorkoutSection(name: "Lower Body", exercises: [
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 2, sets: s(3)),
Exercise(name: "Leg Extension", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
Exercise(name: "Seated Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 3, sets: s(3)),
]),
])
return [mon, tue, wed, thu, fri, sat]
}
}