Replace the YAML exercise catalogs with the motion-rig exercise library

The exercise picker now lists the bundled exercise library — the
motion-rig exercises exported from Exercise Library/ — instead of the
two *.exercises.yaml starter catalogs. ExerciseListLoader and the Yams
dependency are removed; ExerciseMotionLibrary gains exerciseNames, the
bundle enumeration the picker reads.
This commit is contained in:
2026-07-06 12:27:44 -04:00
parent 936a585ece
commit 888852cc2e
9 changed files with 58 additions and 350 deletions
@@ -83,6 +83,19 @@ enum ExerciseMotionLibrary {
let body: ExerciseBodyProfile
}
/// Every exercise with a bundled motion script, sorted alphabetically. XcodeGen
/// flattens resource groups, so `<Exercise Name>.motion.json` files land in the
/// bundle root alongside `body.json` enumerate all json and filter on the
/// compound suffix (which `body.json` doesn't match).
static let exerciseNames: [String] = {
let urls = Bundle.main.urls(forResourcesWithExtension: "json", subdirectory: nil) ?? []
return urls
.map(\.lastPathComponent)
.filter { $0.hasSuffix(".motion.json") }
.map { String($0.dropLast(".motion.json".count)) }
.sorted()
}()
/// The motion script plus the neutral body profile for `exerciseName`, or `nil`
/// when no bundled motion matches (most exercises have none the caller keeps
/// its space empty).
@@ -1,77 +0,0 @@
name: Starter Set - Bodyweight
source: Home or Minimal Equipment
exercises:
- name: Pull-Up
descr: Grip a bar with hands shoulder-width or wider. Pull your chin above the bar, engaging your back and arms. Lower with control.
type: Bodyweight
split: Upper Body
- name: Inverted Row
descr: Lie underneath a bar or sturdy edge, pull your chest toward the bar while keeping your body straight. Squeeze shoulder blades together.
type: Bodyweight
split: Upper Body
- name: Pike Push-Up
descr: Begin in downward dog position. Lower your head toward the ground by bending your elbows, then press back up. Focus on shoulder engagement.
type: Bodyweight
split: Upper Body
- name: Push-Up
descr: With hands just outside shoulder width, lower your body until elbows are at 90°, then push back up. Keep your body in a straight line.
type: Bodyweight
split: Upper Body
- name: Tricep Dip
descr: Using a chair or bench, lower your body by bending your elbows behind you, then press back up. Keep elbows tight to your body.
type: Bodyweight
split: Upper Body
- name: Towel Curl
descr: Sit on the floor with a towel looped under your feet. Pull against the towel using biceps, optionally resisting with your legs.
type: Bodyweight
split: Upper Body
- name: Crunch
descr: Lie on your back with knees bent. Curl your upper back off the floor using your abdominal muscles, then return slowly.
type: Bodyweight
split: Core
- name: Russian Twist
descr: Sit with knees bent and feet off the ground. Twist your torso side to side while keeping your abs engaged.
type: Bodyweight
split: Core
- name: Bodyweight Squat
descr: Stand with feet shoulder-width apart. Lower your hips back and down, keeping your heels on the floor. Rise back to standing.
type: Bodyweight
split: Lower Body
- name: Wall Sit
descr: Lean your back against a wall and lower into a seated position. Hold as long as possible while maintaining good form.
type: Bodyweight
split: Lower Body
- name: Glute Bridge
descr: Lie on your back with knees bent. Push through your heels to lift your hips, then lower slowly. Focus on hamstrings and glutes.
type: Bodyweight
split: Lower Body
- name: Hamstring Walkout
descr: Start in a glute bridge, then slowly walk your heels outward and back in, maintaining control and tension in the hamstrings.
type: Bodyweight
split: Lower Body
- name: Side-Lying Leg Raise (Inner)
descr: Lie on your side with bottom leg straight. Raise the bottom leg upward, engaging the inner thigh.
type: Bodyweight
split: Lower Body
- name: Side-Lying Leg Raise (Outer)
descr: Lie on your side with top leg straight. Raise the top leg upward to engage the outer thigh and glute.
type: Bodyweight
split: Lower Body
- name: Calf Raise
descr: Stand on the balls of your feet (on flat ground or a step). Raise your heels, then lower slowly for a full stretch.
type: Bodyweight
split: Lower Body
@@ -1,83 +0,0 @@
name: Starter Set
source: Planet Fitness
exercises:
- name: Lat Pull Down
descr: Sit upright with your knees secured under the pad. Grip the bar wider than
shoulder-width. Pull the bar down to your chest, squeezing your shoulder blades
together. Avoid leaning back excessively or using momentum.
type: Machine-Based
split: Upper Body
- name: Seated Row
descr: With your chest firmly against the pad, grip the handles and pull straight
back while keeping your elbows close to your body. Focus on retracting your shoulder
blades and avoid rounding your back.
type: Machine-Based
split: Upper Body
- name: Shoulder Press
descr: Sit with your back against the pad, grip the handles just outside shoulder-width.
Press upward without locking out your elbows. Keep your neck relaxed and avoid
shrugging your shoulders.
type: Machine-Based
split: Upper Body
- name: Chest Press
descr: Adjust the seat so the handles are at mid-chest height. Push forward until arms
are nearly extended, then return slowly. Keep wrists straight and dont let your elbows
drop too low.
type: Machine-Based
split: Upper Body
- name: Tricep Press
descr: With elbows close to your sides, press the handles downward in a controlled
motion. Avoid flaring your elbows or using your shoulders to assist the motion.
type: Machine-Based
split: Upper Body
- name: Arm Curl
descr: Position your arms over the pad and grip the handles. Curl the weight upward
while keeping your upper arms stationary. Avoid using momentum and fully control
the lowering phase.
type: Machine-Based
split: Upper Body
- name: Abdominal
descr: Sit with the pads resting against your chest. Contract your abs to curl forward,
keeping your lower back in contact with the pad. Avoid pulling with your arms
or hips.
type: Machine-Based
split: Core
- name: Rotary
descr: Rotate your torso from side to side in a controlled motion, keeping your
hips still. Focus on using your obliques to generate the twist, not momentum or
the arms.
type: Machine-Based
split: Core
- name: Leg Press
descr: Place your feet shoulder-width on the platform. Press upward through your
heels without locking your knees. Keep your back flat against the pad throughout
the motion.
type: Machine-Based
split: Lower Body
- name: Leg Extension
descr: Sit upright and align your knees with the pivot point. Extend your legs to
a straightened position, then lower with control. Avoid jerky movements or lifting
your hips off the seat.
type: Machine-Based
split: Lower Body
- name: Leg Curl
descr: Lie face down or sit depending on the version. Curl your legs toward your
glutes, focusing on hamstring engagement. Avoid arching your back or using momentum.
type: Machine-Based
split: Lower Body
- name: Adductor
descr: Sit with legs placed outside the pads. Bring your legs together using inner
thigh muscles. Control the motion both in and out, avoiding fast swings.
type: Machine-Based
split: Lower Body
- name: Abductor
descr: Sit with legs inside the pads and push outward to engage outer thighs and
glutes. Avoid leaning forward and keep the motion controlled throughout.
type: Machine-Based
split: Lower Body
- name: Calfs
descr: Place the balls of your feet on the platform with heels hanging off. Raise
your heels by contracting your calves, then slowly lower them below the platform
level for a full stretch.
type: Machine-Based
split: Lower Body
@@ -1,75 +0,0 @@
//
// ExerciseListLoader.swift
// Workouts
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import Foundation
import Yams
class ExerciseListLoader {
struct ExerciseListData: Codable {
let name: String
let source: String
let exercises: [ExerciseItem]
struct ExerciseItem: Codable, Identifiable {
let name: String
let descr: String
let type: String
let split: String
var id: String { name }
}
}
static func loadExerciseLists() -> [String: ExerciseListData] {
var exerciseLists: [String: ExerciseListData] = [:]
guard let resourcePath = Bundle.main.resourcePath else {
print("Could not find resource path")
return exerciseLists
}
do {
let fileManager = FileManager.default
let resourceURL = URL(fileURLWithPath: resourcePath)
let yamlFiles = try fileManager.contentsOfDirectory(at: resourceURL, includingPropertiesForKeys: nil)
.filter { $0.pathExtension == "yaml" && $0.lastPathComponent.hasSuffix(".exercises.yaml") }
for yamlFile in yamlFiles {
let fileName = yamlFile.lastPathComponent
do {
let yamlString = try String(contentsOf: yamlFile, encoding: .utf8)
if let exerciseList = try Yams.load(yaml: yamlString) as? [String: Any],
let name = exerciseList["name"] as? String,
let source = exerciseList["source"] as? String,
let exercisesData = exerciseList["exercises"] as? [[String: Any]] {
var exercises: [ExerciseListData.ExerciseItem] = []
for exerciseData in exercisesData {
if let name = exerciseData["name"] as? String,
let descr = exerciseData["descr"] as? String,
let type = exerciseData["type"] as? String,
let split = exerciseData["split"] as? String {
let exercise = ExerciseListData.ExerciseItem(name: name, descr: descr, type: type, split: split)
exercises.append(exercise)
}
}
let exerciseList = ExerciseListData(name: name, source: source, exercises: exercises)
exerciseLists[fileName] = exerciseList
}
} catch {
print("Error loading YAML file \(fileName): \(error)")
}
}
} catch {
print("Error listing directory contents: \(error)")
}
return exerciseLists
}
}
+37 -104
View File
@@ -11,8 +11,6 @@ import SwiftUI
struct ExercisePickerView: View {
@Environment(\.dismiss) private var dismiss
@State private var exerciseLists: [String: ExerciseListLoader.ExerciseListData] = [:]
@State private var selectedListName: String? = nil
@State private var selectedExercises: Set<String> = []
var onExerciseSelected: ([String]) -> Void
@@ -25,117 +23,52 @@ struct ExercisePickerView: View {
var body: some View {
NavigationStack {
Group {
if selectedListName == nil {
// Show list of exercise list files
List {
ForEach(Array(exerciseLists.keys.sorted()), id: \.self) { fileName in
if let list = exerciseLists[fileName] {
Button(action: {
selectedListName = fileName
}) {
VStack(alignment: .leading) {
Text(list.name)
.font(.headline)
Text(list.source)
.font(.subheadline)
.foregroundColor(.secondary)
Text("\(list.exercises.count) exercises")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
}
List(ExerciseMotionLibrary.exerciseNames, id: \.self) { exerciseName in
if allowMultiSelect {
Button(action: {
if selectedExercises.contains(exerciseName) {
selectedExercises.remove(exerciseName)
} else {
selectedExercises.insert(exerciseName)
}
}) {
HStack {
Text(exerciseName)
Spacer()
if selectedExercises.contains(exerciseName) {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
} else {
Button(action: {
onExerciseSelected([exerciseName])
dismiss()
}) {
Text(exerciseName)
}
}
}
.navigationTitle("Exercise Library")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
if allowMultiSelect {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Select") {
if !selectedExercises.isEmpty {
onExerciseSelected(Array(selectedExercises))
dismiss()
}
}
.disabled(selectedExercises.isEmpty)
}
.navigationTitle("Exercise Lists")
} else if let fileName = selectedListName, let list = exerciseLists[fileName] {
// Show exercises in the selected list grouped by split
List {
let exercisesByGroup = Dictionary(grouping: list.exercises) { $0.split }
let sortedGroups = exercisesByGroup.keys.sorted()
ForEach(sortedGroups, id: \.self) { splitName in
Section(header: Text(splitName)) {
ForEach(exercisesByGroup[splitName]?.sorted(by: { $0.name < $1.name }) ?? [], id: \.id) { exercise in
if allowMultiSelect {
Button(action: {
if selectedExercises.contains(exercise.name) {
selectedExercises.remove(exercise.name)
} else {
selectedExercises.insert(exercise.name)
}
}) {
HStack {
VStack(alignment: .leading) {
Text(exercise.name)
.font(.headline)
Text(exercise.type)
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 2)
Spacer()
if selectedExercises.contains(exercise.name) {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
} else {
Button(action: {
onExerciseSelected([exercise.name])
dismiss()
}) {
VStack(alignment: .leading) {
Text(exercise.name)
.font(.headline)
Text(exercise.type)
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 2)
}
}
}
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Back") {
selectedListName = nil
selectedExercises.removeAll()
}
}
if allowMultiSelect {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Select") {
if !selectedExercises.isEmpty {
onExerciseSelected(Array(selectedExercises))
dismiss()
}
}
.disabled(selectedExercises.isEmpty)
}
}
}
.navigationTitle(list.name)
}
}
}
.onAppear {
exerciseLists = ExerciseListLoader.loadExerciseLists()
}
}
}