This commit is contained in:
2025-07-18 10:03:58 -04:00
parent 4f01a6445f
commit 66f257609f
37 changed files with 845 additions and 704 deletions

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
A45FA1FE2E27171B00581607 /* Worksouts Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = A45FA1F12E27171A00581607 /* Worksouts Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
A45FA2732E29B12500581607 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = A45FA2722E29B12500581607 /* Yams */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -70,6 +71,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A45FA2732E29B12500581607 /* Yams in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -123,6 +125,7 @@
);
name = Workouts;
packageProductDependencies = (
A45FA2722E29B12500581607 /* Yams */,
);
productName = Workouts;
productReference = A45FA0912E21B3DD00581607 /* Workouts.app */;
@ -177,6 +180,9 @@
);
mainGroup = A45FA0882E21B3DC00581607;
minimizedProjectReferenceProxies = 1;
packageReferences = (
A45FA26B2E297F5B00581607 /* XCRemoteSwiftPackageReference "Yams" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = A45FA0922E21B3DD00581607 /* Products */;
projectDirPath = "";
@ -505,6 +511,25 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
A45FA26B2E297F5B00581607 /* XCRemoteSwiftPackageReference "Yams" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/jpsim/Yams";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 6.0.2;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
A45FA2722E29B12500581607 /* Yams */ = {
isa = XCSwiftPackageProductDependency;
package = A45FA26B2E297F5B00581607 /* XCRemoteSwiftPackageReference "Yams" */;
productName = Yams;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = A45FA0892E21B3DC00581607 /* Project object */;
}

View File

@ -0,0 +1,15 @@
{
"originHash" : "81702b8f7fa6fc73458821cd4dfa7dde6684a9d1d4b6d63ecde0b6b832d8d75e",
"pins" : [
{
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams",
"state" : {
"revision" : "f4d4d6827d36092d151ad7f6fef1991c1b7192f6",
"version" : "6.0.2"
}
}
],
"version" : 3
}

View File

@ -17,9 +17,14 @@ struct ContentView: View {
var body: some View {
NavigationView {
TabView {
SplitsView()
.tabItem {
Label("Workouts", systemImage: "figure.strengthtraining.traditional")
}
WorkoutsView()
.tabItem {
Label("Workout", systemImage: "figure.strengthtraining.traditional")
Label("Logs", systemImage: "list.bullet.clipboard.fill")
}

View File

@ -1,72 +0,0 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class Exercise {
var name: String = ""
var descr: String = ""
@Relationship(deleteRule: .nullify, inverse: \ExerciseType.exercises)
var type: ExerciseType?
@Relationship(deleteRule: .nullify, inverse: \Muscle.exercises)
var muscles: [Muscle]? = []
@Relationship(deleteRule: .nullify, inverse: \SplitExerciseAssignment.exercise)
var splits: [SplitExerciseAssignment]? = []
@Relationship(deleteRule: .nullify, inverse: \WorkoutLog.exercise)
var logs: [WorkoutLog]? = []
init(name: String, descr: String) {
self.name = name
self.descr = descr
}
static let unnamed = "Unnamed Exercise"
}
extension Exercise: EditableEntity {
static func createNew() -> Exercise {
return Exercise(name: "", descr: "")
}
static var navigationTitle: String {
return "Exercises"
}
@ViewBuilder
static func formView(for model: Exercise) -> some View {
EntityAddEditView(model: model) { $model in
// This internal view is necessary to use @Query within the form.
ExerciseFormView(model: $model)
}
}
}
fileprivate struct ExerciseFormView: View {
@Binding var model: Exercise
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var exerciseTypes: [ExerciseType]
var body: some View {
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Exercise Type")) {
Picker("Type", selection: $model.type) {
Text("Select a type").tag(nil as ExerciseType?)
ForEach(exerciseTypes) { type in
Text(type.name).tag(type as ExerciseType?)
}
}
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
}
}
}

View File

@ -0,0 +1,66 @@
import Foundation
import Yams
struct ExerciseList: Codable {
let name: String
let source: String
let exercises: [ExerciseItem]
struct ExerciseItem: Codable, Identifiable {
let name: String
let descr: String
let type: String
var id: String { name }
}
}
class ExerciseListLoader {
static func loadExerciseLists() -> [String: ExerciseList] {
var exerciseLists: [String: ExerciseList] = [:]
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: [ExerciseList.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 exercise = ExerciseList.ExerciseItem(name: name, descr: descr, type: type)
exercises.append(exercise)
}
}
let exerciseList = ExerciseList(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
}
}

View File

@ -1,48 +0,0 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class ExerciseType {
var name: String = ""
var descr: String = ""
@Relationship(deleteRule: .nullify)
var exercises: [Exercise]? = []
init(name: String, descr: String) {
self.name = name
self.descr = descr
}
}
// MARK: - EditableEntity Conformance
extension ExerciseType: EditableEntity {
var count: Int? {
return self.exercises?.count
}
static func createNew() -> ExerciseType {
return ExerciseType(name: "", descr: "")
}
static var navigationTitle: String {
return "Exercise Types"
}
@ViewBuilder
static func formView(for model: ExerciseType) -> some View {
EntityAddEditView(model: model) { $model in
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
}
}
}
}

View File

@ -1,73 +0,0 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class Muscle {
var name: String = ""
var descr: String = ""
@Relationship(deleteRule: .nullify, inverse: \MuscleGroup.muscles)
var muscleGroup: MuscleGroup?
@Relationship(deleteRule: .nullify)
var exercises: [Exercise]? = []
init(name: String, descr: String, muscleGroup: MuscleGroup? = nil) {
self.name = name
self.descr = descr
self.muscleGroup = muscleGroup
}
}
// MARK: - EditableEntity Conformance
extension Muscle: EditableEntity {
var count: Int? {
return self.exercises?.count
}
static func createNew() -> Muscle {
return Muscle(name: "", descr: "")
}
static var navigationTitle: String {
return "Muscles"
}
@ViewBuilder
static func formView(for model: Muscle) -> some View {
EntityAddEditView(model: model) { $model in
MuscleFormView(model: $model)
}
}
}
// MARK: - Private Form View
fileprivate struct MuscleFormView: View {
@Binding var model: Muscle
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var muscleGroups: [MuscleGroup]
var body: some View {
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Muscle Group")) {
Picker("Muscle Group", selection: $model.muscleGroup) {
Text("Select a muscle group").tag(nil as MuscleGroup?)
ForEach(muscleGroups) { group in
Text(group.name).tag(group as MuscleGroup?)
}
}
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
}
}
}

View File

@ -1,49 +0,0 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class MuscleGroup {
var name: String = ""
var descr: String = ""
@Relationship(deleteRule: .nullify)
var muscles: [Muscle]? = []
init(name: String, descr: String) {
self.name = name
self.descr = descr
}
}
// MARK: - EditableEntity Conformance
extension MuscleGroup: EditableEntity {
static func createNew() -> MuscleGroup {
return MuscleGroup(name: "", descr: "")
}
static var navigationTitle: String {
return "Muscle Groups"
}
@ViewBuilder
static func formView(for model: MuscleGroup) -> some View {
EntityAddEditView(model: model) { $model in
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
}
}
}
var count: Int? {
return muscles?.count
}
}

View File

@ -5,27 +5,13 @@ import SwiftUI
@Model
final class Split {
var name: String = ""
var intro: String = ""
var color: String = "indigo"
var systemImage: String = "dumbbell.fill"
var order: Int = 0
// Returns the SwiftUI Color for the stored color name
func getColor () -> Color {
switch color {
case "red": return .red
case "orange": return .orange
case "yellow": return .yellow
case "green": return .green
case "mint": return .mint
case "teal": return .teal
case "cyan": return .cyan
case "blue": return .blue
case "indigo": return .indigo
case "purple": return .purple
case "pink": return .pink
case "brown": return .brown
default: return .indigo
}
return Color.color(from: self.color)
}
@Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split)
@ -34,11 +20,11 @@ final class Split {
@Relationship(deleteRule: .nullify, inverse: \Workout.split)
var workouts: [Workout]? = []
init(name: String, intro: String, color: String = "indigo", systemImage: String = "dumbbell.fill") {
init(name: String, color: String = "indigo", systemImage: String = "dumbbell.fill", order: Int = 0) {
self.name = name
self.intro = intro
self.color = color
self.systemImage = systemImage
self.order = order
}
static let unnamed = "Unnamed Split"
@ -52,7 +38,7 @@ extension Split: EditableEntity {
}
static func createNew() -> Split {
return Split(name: "", intro: "")
return Split(name: "")
}
static var navigationTitle: String {
@ -72,10 +58,6 @@ extension Split: EditableEntity {
fileprivate struct SplitFormView: View {
@Binding var model: Split
@State private var showingAddSheet: Bool = false
@State private var itemToEdit: SplitExerciseAssignment? = nil
@State private var itemToDelete: SplitExerciseAssignment? = nil
// Available colors for splits
private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
@ -88,15 +70,10 @@ fileprivate struct SplitFormView: View {
.bold()
}
Section(header: Text("Description")) {
TextEditor(text: $model.intro)
.frame(minHeight: 100)
}
Section(header: Text("Appearance")) {
Picker("Color", selection: $model.color) {
ForEach(availableColors, id: \.self) { colorName in
let tempSplit = Split(name: "", intro: "", color: colorName)
let tempSplit = Split(name: "", color: colorName)
HStack {
Circle()
.fill(tempSplit.getColor())
@ -121,75 +98,7 @@ fileprivate struct SplitFormView: View {
Section(header: Text("Exercises")) {
NavigationLink {
NavigationStack {
Form {
List {
if let assignments = model.exercises, !assignments.isEmpty {
let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.exercise?.name ?? Exercise.unnamed < $1.exercise?.name ?? Exercise.unnamed : $0.order < $1.order })
ForEach(sortedAssignments) { item in
ListItem(
title: item.exercise?.name ?? Exercise.unnamed,
text: item.setup.isEmpty ? nil : item.setup,
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs"
)
.swipeActions {
Button(role: .destructive) {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
} else {
Text("No exercises added yet.")
}
}
}
.navigationTitle("Exercises")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
.sheet (isPresented: $showingAddSheet) {
ExercisePickerView { exercise in
itemToEdit = SplitExerciseAssignment(
order: 0,
sets: 3,
reps: 10,
weight: 40,
split: model,
exercise: exercise
)
}
}
.sheet(item: $itemToEdit) { item in
SplitExerciseAssignmentAddEditView(model: item)
}
.confirmationDialog(
"Delete Exercise?",
isPresented: .constant(itemToDelete != nil),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let item = itemToDelete {
withAnimation {
model.exercises?.removeAll { $0.id == item.id }
itemToDelete = nil
}
}
}
}
SplitExercisesListView(model: model)
} label: {
ListItem(
text: "Exercises",

View File

@ -3,25 +3,21 @@ import SwiftData
@Model
final class SplitExerciseAssignment {
var exerciseName: String = ""
var order: Int = 0
var sets: Int = 0
var reps: Int = 0
var weight: Int = 0
var setup: String = ""
@Relationship(deleteRule: .nullify)
var split: Split?
@Relationship(deleteRule: .nullify)
var exercise: Exercise?
init(order: Int, sets: Int, reps: Int, weight: Int, setup: String = "", split: Split, exercise: Exercise) {
init(split: Split, exerciseName: String, order: Int, sets: Int, reps: Int, weight: Int) {
self.split = split
self.exerciseName = exerciseName
self.order = order
self.sets = sets
self.reps = reps
self.weight = weight
self.setup = setup
self.split = split
self.exercise = exercise
}
}

View File

@ -9,16 +9,14 @@ final class WorkoutLog {
var weight: Int = 0
var status: WorkoutStatus? = WorkoutStatus.notStarted
var order: Int = 0
var exerciseName: String = ""
var completed: Bool = false
@Relationship(deleteRule: .nullify)
var workout: Workout?
@Relationship(deleteRule: .nullify)
var exercise: Exercise?
init(workout: Workout, exercise: Exercise, date: Date, order: Int = 0, sets: Int, reps: Int, weight: Int, status: WorkoutStatus = .notStarted, completed: Bool = false) {
init(workout: Workout, exerciseName: String, date: Date, order: Int = 0, sets: Int, reps: Int, weight: Int, status: WorkoutStatus = .notStarted, completed: Bool = false) {
self.date = date
self.order = order
self.sets = sets
@ -26,7 +24,7 @@ final class WorkoutLog {
self.weight = weight
self.status = status
self.workout = workout
self.exercise = exercise
self.exerciseName = exerciseName
self.completed = completed
}
}

View File

@ -1,3 +1,6 @@
name: Beginner
source: Planet Fitness
exercises:
- name: Lat Pull Down
setup: 'Seat: 3, Thigh Pad: 4'
descr: Sit upright with your knees secured under the pad. Grip the bar wider than

View File

@ -0,0 +1,135 @@
- name: Pectoralis Major
muscleGroup: Chest
descr: Large chest muscle located on the front of the upper torso, responsible for arm flexion and pushing.
- name: Pectoralis Minor
muscleGroup: Chest
descr: Thin, triangular muscle beneath the pectoralis major, stabilizes the scapula.
- name: Latissimus Dorsi
muscleGroup: Back
descr: Broad muscle on the mid to lower back, responsible for pulling and shoulder extension.
- name: Trapezius
muscleGroup: Back
descr: Large muscle covering the upper back and neck, involved in shoulder movement and posture.
- name: Rhomboids
muscleGroup: Back
descr: Muscles between the shoulder blades, responsible for scapular retraction.
- name: Erector Spinae
muscleGroup: Back
descr: Long vertical muscles along the spine that maintain posture and extend the back.
- name: Deltoid (Anterior)
muscleGroup: Shoulders
descr: Front portion of the shoulder muscle, raises the arm forward.
- name: Deltoid (Lateral)
muscleGroup: Shoulders
descr: Middle portion of the shoulder muscle, raises the arm to the side.
- name: Deltoid (Posterior)
muscleGroup: Shoulders
descr: Rear portion of the shoulder muscle, moves the arm backward.
- name: Rotator Cuff Muscles
muscleGroup: Shoulders
descr: Group of four muscles surrounding the shoulder joint, stabilizing and rotating the arm.
- name: Biceps Brachii
muscleGroup: Arms
descr: Front upper arm muscle, responsible for elbow flexion and forearm rotation.
- name: Triceps Brachii
muscleGroup: Arms
descr: Back upper arm muscle, responsible for elbow extension.
- name: Brachialis
muscleGroup: Arms
descr: Muscle beneath the biceps, assists in elbow flexion.
- name: Brachioradialis
muscleGroup: Arms
descr: Forearm muscle on the thumb side, aids in elbow flexion.
- name: Rectus Abdominis
muscleGroup: Abdominals
descr: Vertical muscle on the front of the abdomen, responsible for trunk flexion (the 'six-pack').
- name: Transverse Abdominis
muscleGroup: Abdominals
descr: Deepest abdominal muscle, wraps around the torso to stabilize the core.
- name: Internal Obliques
muscleGroup: Abdominals
descr: Muscles located beneath the external obliques, involved in trunk rotation and lateral flexion.
- name: External Obliques
muscleGroup: Abdominals
descr: Muscles on the sides of the abdomen, responsible for trunk twisting and side bending.
- name: Gluteus Maximus
muscleGroup: Glutes
descr: Largest glute muscle located in the buttocks, responsible for hip extension and rotation.
- name: Gluteus Medius
muscleGroup: Glutes
descr: Muscle on the outer surface of the pelvis, important for hip abduction and stability.
- name: Gluteus Minimus
muscleGroup: Glutes
descr: Smallest glute muscle, located beneath the medius, assists in hip abduction.
- name: Rectus Femoris
muscleGroup: Quadriceps
descr: Front thigh muscle, part of the quadriceps group, helps extend the knee and flex the hip.
- name: Vastus Lateralis
muscleGroup: Quadriceps
descr: Outer thigh muscle, part of the quadriceps, involved in knee extension.
- name: Vastus Medialis
muscleGroup: Quadriceps
descr: Inner thigh muscle, part of the quadriceps, assists in knee extension and stabilization.
- name: Vastus Intermedius
muscleGroup: Quadriceps
descr: Deep thigh muscle beneath rectus femoris, assists in knee extension.
- name: Biceps Femoris
muscleGroup: Hamstrings
descr: Muscle on the back of the thigh, responsible for knee flexion and hip extension.
- name: Semitendinosus
muscleGroup: Hamstrings
descr: Medial hamstring muscle, assists in knee flexion and internal rotation.
- name: Semimembranosus
muscleGroup: Hamstrings
descr: Deep medial hamstring muscle, also assists in knee flexion and hip extension.
- name: Gastrocnemius
muscleGroup: Calves
descr: Large calf muscle visible on the back of the lower leg, responsible for plantar flexion of the foot.
- name: Soleus
muscleGroup: Calves
descr: Flat muscle beneath the gastrocnemius, aids in plantar flexion when the knee is bent.
- name: Flexor Carpi Radialis
muscleGroup: Forearms
descr: Muscle on the front of the forearm, flexes and abducts the wrist.
- name: Flexor Carpi Ulnaris
muscleGroup: Forearms
descr: Forearm muscle that flexes and adducts the wrist.
- name: Extensor Carpi Radialis
muscleGroup: Forearms
descr: Posterior forearm muscle that extends and abducts the wrist.
- name: Pronator Teres
muscleGroup: Forearms
descr: Muscle running across the forearm that pronates the forearm (palm down).
- name: Sternocleidomastoid
muscleGroup: Neck
descr: Prominent neck muscle responsible for rotating and flexing the head.
- name: Splenius Capitis
muscleGroup: Neck
descr: Back of neck muscle that extends and rotates the head.
- name: Scalenes
muscleGroup: Neck
descr: Lateral neck muscles that assist in neck flexion and elevate the ribs during breathing.
- name: Iliopsoas
muscleGroup: Hip Flexors
descr: Deep muscle group connecting the lower spine to the femur, main hip flexor.
- name: Rectus Femoris
muscleGroup: Hip Flexors
descr: Also part of the quadriceps, helps flex the hip and extend the knee.
- name: Sartorius
muscleGroup: Hip Flexors
descr: Longest muscle in the body, crosses the thigh to flex and rotate the hip and knee.
- name: Adductor Longus
muscleGroup: Adductors
descr: Medial thigh muscle that adducts the leg and assists with hip flexion.
- name: Adductor Brevis
muscleGroup: Adductors
descr: Short adductor muscle that helps pull the thigh inward.
- name: Adductor Magnus
muscleGroup: Adductors
descr: Large, deep inner thigh muscle that performs hip adduction and extension.
- name: Gracilis
muscleGroup: Adductors
descr: Thin inner thigh muscle that assists in adduction and knee flexion.
- name: Tensor Fasciae Latae
muscleGroup: Abductors
descr: Lateral hip muscle that abducts and medially rotates the thigh.

View File

@ -0,0 +1,69 @@
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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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

View File

@ -1,167 +0,0 @@
import Foundation
import SwiftData
struct DataLoader {
static let logger = AppLogger(
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
category: "InitialData"
)
// Data structures for JSON decoding
private struct ExerciseTypeData: Codable {
let name: String
let descr: String
}
private struct MuscleGroupData: Codable {
let name: String
let descr: String
}
private struct MuscleData: Codable {
let name: String
let descr: String
let muscleGroup: String
}
private struct ExerciseData: Codable {
let name: String
let setup: String
let descr: String
let sets: Int
let reps: Int
let weight: Int
let type: String
let muscles: [String]
}
private struct SplitExerciseAssignmentData: Codable {
let exercise: String
let weight: Int
let sets: Int
let reps: Int
}
private struct SplitData: Codable {
let name: String
let intro: String
let splitExerciseAssignments: [SplitExerciseAssignmentData]
}
@MainActor
static func create(modelContext: ModelContext) {
logger.info("Creating initial data from JSON files")
// Load and insert data
do {
// Dictionaries to store references
var exerciseTypes: [String: ExerciseType] = [:]
var muscleGroups: [String: MuscleGroup] = [:]
var muscles: [String: Muscle] = [:]
var exercises: [String: Exercise] = [:]
// 1. Load Exercise Types
let exerciseTypeData = try loadJSON(forResource: "exercise-types", type: [ExerciseTypeData].self)
for typeData in exerciseTypeData {
let exerciseType = ExerciseType(name: typeData.name, descr: typeData.descr)
exerciseTypes[typeData.name] = exerciseType
modelContext.insert(exerciseType)
}
// 2. Load Muscle Groups
let muscleGroupData = try loadJSON(forResource: "muscle-groups", type: [MuscleGroupData].self)
for groupData in muscleGroupData {
let muscleGroup = MuscleGroup(name: groupData.name, descr: groupData.descr)
muscleGroups[groupData.name] = muscleGroup
modelContext.insert(muscleGroup)
}
// 3. Load Muscles
let muscleData = try loadJSON(forResource: "muscles", type: [MuscleData].self)
for data in muscleData {
// Find the muscle group for this muscle
if let muscleGroup = muscleGroups[data.muscleGroup] {
let muscle = Muscle(name: data.name, descr: data.descr, muscleGroup: muscleGroup)
muscles[data.name] = muscle
modelContext.insert(muscle)
} else {
logger.warning("Muscle group not found for muscle: \(data.name)")
}
}
// 4. Load Exercises
let exerciseData = try loadJSON(forResource: "exercises", type: [ExerciseData].self)
for data in exerciseData {
let exercise = Exercise(name: data.name, descr: data.descr)
// Set exercise type
if let type = exerciseTypes[data.type] {
exercise.type = type
} else {
logger.warning("Exercise type not found: \(data.type) for exercise: \(data.name)")
}
// Set muscles
var exerciseMuscles: [Muscle] = []
for muscleName in data.muscles {
if let muscle = muscles[muscleName] {
exerciseMuscles.append(muscle)
} else {
logger.warning("Muscle not found: \(muscleName) for exercise: \(data.name)")
}
}
exercise.muscles = exerciseMuscles
exercises[data.name] = exercise
modelContext.insert(exercise)
}
// 5. Load Splits and Exercise Assignments
let splitData = try loadJSON(forResource: "splits", type: [SplitData].self)
for data in splitData {
let split = Split(name: data.name, intro: data.intro)
modelContext.insert(split)
// Create exercise assignments for this split
for (index, assignment) in data.splitExerciseAssignments.enumerated() {
if let exercise = exercises[assignment.exercise] {
let splitAssignment = SplitExerciseAssignment(
order: index + 1, // 1-based ordering
sets: assignment.sets,
reps: assignment.reps,
weight: assignment.weight,
split: split,
exercise: exercise
)
modelContext.insert(splitAssignment)
} else {
logger.warning("Exercise not found: \(assignment.exercise) for split: \(data.name)")
}
}
}
// Save all the inserted data
try modelContext.save()
logger.info("Initial data loaded successfully from JSON files")
} catch {
logger.error("Failed to load initial data from JSON files: \(error.localizedDescription)")
}
}
// Helper method to load and decode JSON from a file
private static func loadJSON<T: Decodable>(forResource name: String, type: T.Type) throws -> T {
guard let url = Bundle.main.url(forResource: name, withExtension: "json") else {
logger.error("Could not find JSON file: \(name).json")
throw NSError(domain: "InitialData", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not find JSON file: \(name).json"])
}
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
logger.error("Failed to decode JSON file \(name).json: \(error.localizedDescription)")
throw error
}
}
}

View File

@ -4,10 +4,6 @@ enum SchemaV1: VersionedSchema {
static var versionIdentifier: Schema.Version = .init(1, 0, 0)
static var models: [any PersistentModel.Type] = [
Exercise.self,
ExerciseType.self,
Muscle.self,
MuscleGroup.self,
Split.self,
SplitExerciseAssignment.self,
Workout.self,

View File

@ -4,10 +4,6 @@ enum SchemaV2: VersionedSchema {
static var versionIdentifier: Schema.Version = .init(1, 0, 1)
static var models: [any PersistentModel.Type] = [
Exercise.self,
ExerciseType.self,
Muscle.self,
MuscleGroup.self,
Split.self,
SplitExerciseAssignment.self,
Workout.self,

View File

@ -4,10 +4,6 @@ enum SchemaV3: VersionedSchema {
static var versionIdentifier: Schema.Version = .init(1, 0, 2)
static var models: [any PersistentModel.Type] = [
Exercise.self,
ExerciseType.self,
Muscle.self,
MuscleGroup.self,
Split.self,
SplitExerciseAssignment.self,
Workout.self,

View File

@ -9,7 +9,7 @@ final class WorkoutsContainer {
static func create() -> ModelContainer {
// Using the current models directly without migration plan to avoid reference errors
let schema = Schema(SchemaV2.models)
let schema = Schema(SchemaV1.models)
let configuration = ModelConfiguration(cloudKitDatabase: .automatic)
let container = try! ModelContainer(for: schema, configurations: configuration)
return container
@ -20,13 +20,10 @@ final class WorkoutsContainer {
let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
do {
let schema = Schema(SchemaV2.models)
let schema = Schema(SchemaV1.models)
let container = try ModelContainer(for: schema, configurations: configuration)
let context = ModelContext(container)
// Create default data for previews
DataLoader.create(modelContext: context)
return container
} catch {
fatalError("Failed to create preview ModelContainer: \(error.localizedDescription)")

View File

@ -0,0 +1,30 @@
//
// Color+color.swift
// Workouts
//
// Created by rzen on 7/17/25 at 10:41AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUICore
extension Color {
static func color (from: String) -> Color {
switch from {
case "red": return .red
case "orange": return .orange
case "yellow": return .yellow
case "green": return .green
case "mint": return .mint
case "teal": return .teal
case "cyan": return .cyan
case "blue": return .blue
case "indigo": return .indigo
case "purple": return .purple
case "pink": return .pink
case "brown": return .brown
default: return .black
}
}
}

View File

@ -18,14 +18,9 @@ enum AppStorageKeys {
struct SettingsView: View {
@Environment(\.modelContext) private var modelContext
@State private var showingPopulateData = false
@State private var showingClearAllDataConfirmation = false
var splitsCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Split>()) }
var musclesCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Muscle>()) }
var muscleGroupsCount: Int? { try? modelContext.fetchCount(FetchDescriptor<MuscleGroup>()) }
var exerciseTypeCount: Int? { try? modelContext.fetchCount(FetchDescriptor<ExerciseType>()) }
var exercisesCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Exercise>()) }
var body: some View {
NavigationStack {
@ -40,67 +35,9 @@ struct SettingsView: View {
.foregroundColor(.gray)
}
}
NavigationLink(destination: MuscleGroupsListView()) {
HStack {
Text("Muscle Groups")
Spacer()
Text("\(muscleGroupsCount ?? 0)")
.font(.caption)
.foregroundColor(.gray)
}
}
NavigationLink(destination: MusclesListView()) {
HStack {
Text("Muscles")
Spacer()
Text("\(musclesCount ?? 0)")
.font(.caption)
.foregroundColor(.gray)
}
}
NavigationLink(destination: ExerciseTypeListView()) {
HStack {
Text("Exercise Types")
Spacer()
Text("\(exerciseTypeCount ?? 0)")
.font(.caption)
.foregroundColor(.gray)
}
}
NavigationLink(destination: ExercisesListView()) {
HStack {
Text("Exercises")
Spacer()
Text("\(exercisesCount ?? 0)")
.font(.caption)
.foregroundColor(.gray)
}
}
}
Section(header: Text("Developer")) {
Button(action: {
showingPopulateData = true
}) {
HStack {
Label("Populate Data", systemImage: "plus")
Spacer()
}
}
.confirmationDialog(
"Populate Data?",
isPresented: $showingPopulateData,
titleVisibility: .hidden
) {
Button("Populate Data") {
DataLoader.create(modelContext: modelContext)
}
Button("Cancel", role: .cancel) {}
// } message: {
// Text("This action cannot be undone. All data will be permanently deleted.")
}
Button(action: {
showingClearAllDataConfirmation = true
}) {
@ -139,10 +76,6 @@ struct SettingsView: View {
private func clearAllData () {
do {
try deleteAllObjects(ofType: ExerciseType.self, from: modelContext)
try deleteAllObjects(ofType: Exercise.self, from: modelContext)
try deleteAllObjects(ofType: Muscle.self, from: modelContext)
try deleteAllObjects(ofType: MuscleGroup.self, from: modelContext)
try deleteAllObjects(ofType: Split.self, from: modelContext)
try deleteAllObjects(ofType: SplitExerciseAssignment.self, from: modelContext)
try deleteAllObjects(ofType: Workout.self, from: modelContext)
@ -154,30 +87,6 @@ struct SettingsView: View {
}
}
struct ExercisesListView: View {
var body: some View {
EntityListView<Exercise>(sort: [SortDescriptor(\Exercise.name)])
}
}
struct ExerciseTypeListView: View {
var body: some View {
EntityListView<ExerciseType>(sort: [SortDescriptor(\ExerciseType.name)])
}
}
struct MuscleGroupsListView: View {
var body: some View {
EntityListView<MuscleGroup>(sort: [SortDescriptor(\MuscleGroup.name)])
}
}
struct MusclesListView: View {
var body: some View {
EntityListView<Muscle>(sort: [SortDescriptor(\Muscle.name)])
}
}
struct SplitsListView: View {
var body: some View {
EntityListView<Split>(sort: [SortDescriptor(\Split.name)])

View File

@ -12,15 +12,24 @@ import SwiftUI
struct SplitExerciseAssignmentAddEditView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State private var showingExercisePicker = false
@State var model: SplitExerciseAssignment
var body: some View {
NavigationStack {
Form {
Section (header: Text("Setup")) {
TextEditor(text: $model.setup)
.frame(minHeight: 60)
Section(header: Text("Exercise")) {
Button(action: {
showingExercisePicker = true
}) {
HStack {
Text(model.exerciseName.isEmpty ? "Select Exercise" : model.exerciseName)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
}
}
Section (header: Text("Sets/Reps")) {
@ -44,7 +53,12 @@ struct SplitExerciseAssignmentAddEditView: View {
}
}
}
.navigationTitle("\(model.exercise?.name ?? Exercise.unnamed)")
.sheet(isPresented: $showingExercisePicker) {
ExercisePickerView { exerciseName in
model.exerciseName = exerciseName
}
}
.navigationTitle(model.exerciseName.isEmpty ? "New Exercise" : model.exerciseName)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {

View File

@ -0,0 +1,64 @@
//
// SplitAddEditView.swift
// Workouts
//
// Created by rzen on 7/18/25 at 9:42AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct SplitAddEditView: View {
@State var model: Split
private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
private let availableIcons = ["dumbbell.fill", "figure.strengthtraining.traditional", "figure.run", "figure.hiking", "figure.cooldown", "figure.boxing", "figure.wrestling", "figure.gymnastics", "figure.handball", "figure.core.training", "heart.fill", "bolt.fill"]
var body: some View {
Form {
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Appearance")) {
Picker("Color", selection: $model.color) {
ForEach(availableColors, id: \.self) { colorName in
let tempSplit = Split(name: "", color: colorName)
HStack {
Circle()
.fill(tempSplit.getColor())
.frame(width: 20, height: 20)
Text(colorName.capitalized)
}
.tag(colorName)
}
}
Picker("Icon", selection: $model.systemImage) {
ForEach(availableIcons, id: \.self) { iconName in
HStack {
Image(systemName: iconName)
.frame(width: 24, height: 24)
Text(iconName.replacingOccurrences(of: ".fill", with: "").replacingOccurrences(of: "figure.", with: "").capitalized)
}
.tag(iconName)
}
}
}
Section(header: Text("Exercises")) {
NavigationLink {
SplitExercisesListView(model: model)
} label: {
ListItem(
text: "Exercises",
count: model.exercises?.count ?? 0
)
}
}
}
}
}

View File

@ -0,0 +1,108 @@
//
// SplitExercisesListView.swift
// Workouts
//
// Created by rzen on 7/18/25 at 8:38AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct SplitExercisesListView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
var model: Split
@State private var showingAddSheet: Bool = false
@State private var itemToEdit: SplitExerciseAssignment? = nil
@State private var itemToDelete: SplitExerciseAssignment? = nil
var body: some View {
NavigationStack {
Form {
List {
if let assignments = model.exercises, !assignments.isEmpty {
let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.exerciseName < $1.exerciseName : $0.order < $1.order })
ForEach(sortedAssignments) { item in
ListItem(
title: item.exerciseName,
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs \(item.order)"
)
.swipeActions {
Button {
itemToDelete = item
} label: {
Label("Delete", systemImage: "circle")
}
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
.onMove(perform: { indices, destination in
var exerciseArray = Array(sortedAssignments)
exerciseArray.move(fromOffsets: indices, toOffset: destination)
for (index, exercise) in exerciseArray.enumerated() {
exercise.order = index
}
if let modelContext = exerciseArray.first?.modelContext {
do {
try modelContext.save()
} catch {
print("Error saving after reordering: \(error)")
}
}
})
} else {
Text("No exercises added yet.")
}
}
}
.navigationTitle("\(model.name)")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
.sheet (isPresented: $showingAddSheet) {
ExercisePickerView { exerciseName in
itemToEdit = SplitExerciseAssignment(
split: model,
exerciseName: exerciseName,
order: 0,
sets: 3,
reps: 10,
weight: 40
)
}
}
.sheet(item: $itemToEdit) { item in
SplitExerciseAssignmentAddEditView(model: item)
}
.confirmationDialog(
"Delete Exercise?",
isPresented: .constant(itemToDelete != nil),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let item = itemToDelete {
withAnimation {
modelContext.delete(item)
try? modelContext.save()
itemToDelete = nil
}
}
}
}
}
}

View File

@ -0,0 +1,102 @@
//
// SplitsView.swift
// Workouts
//
// Created by rzen on 7/17/25 at 6:55PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct SplitsView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@Query(sort: [
SortDescriptor(\Split.order),
SortDescriptor(\Split.name)
]) private var splits: [Split]
@State private var showingAddSheet: Bool = false
var body: some View {
NavigationStack {
ScrollView {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
ForEach(splits) { split in
NavigationLink {
SplitExercisesListView(model: split)
} label: {
VStack {
ZStack(alignment: .bottom) {
// Golden ratio rectangle (1:1.618)
RoundedRectangle(cornerRadius: 12)
.fill(
LinearGradient(
gradient: Gradient(colors: [split.getColor(), split.getColor().darker(by: 0.2)]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.aspectRatio(1.618, contentMode: .fit)
.shadow(radius: 2)
VStack {
// Icon in the center
Image(systemName: split.systemImage)
.font(.system(size: 40, weight: .medium))
.foregroundColor(.white)
.offset(y: -15)
// Name at the bottom inside the rectangle
Text(split.name)
.font(.headline)
.foregroundColor(.white)
.lineLimit(1)
.padding(.horizontal, 8)
.padding(.bottom, 8)
}
}
// Exercise count below the rectangle
Text("\(split.exercises?.count ?? 0) exercises")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.onMove(perform: { indices, destination in
var splitArray = Array(splits)
splitArray.move(fromOffsets: indices, toOffset: destination)
for (index, split) in splitArray.enumerated() {
split.order = index
}
if let modelContext = splitArray.first?.modelContext {
do {
try modelContext.save()
} catch {
print("Error saving after reordering: \(error)")
}
}
})
}
.padding()
}
.navigationTitle("Splits")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
.sheet (isPresented: $showingAddSheet) {
SplitAddEditView(model: Split(name: "New Split"))
}
}
}

View File

@ -0,0 +1,89 @@
//
// CalendarListItem.swift
// Workouts
//
// Created by rzen on 7/18/25 at 8:44AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct CalendarListItem: View {
var date: Date
var title: String
var subtitle: String?
var count: Int?
var body: some View {
HStack (alignment: .top) {
ZStack {
VStack {
Text("\(date.abbreviatedWeekday)")
.font(.caption)
.foregroundColor(.secondary)
Text("\(date.dayOfMonth)")
.font(.headline)
.foregroundColor(.accentColor)
Text("\(date.abbreviatedMonth)")
.font(.caption)
.foregroundColor(.secondary)
}
.padding([.trailing], 10)
}
HStack {
VStack (alignment: .leading) {
Text("\(title)")
.font(.headline)
if let subtitle = subtitle {
Text("\(subtitle)")
.font(.footnote)
}
}
if let count = count {
Spacer()
Text("\(count)")
.font(.caption)
.foregroundColor(.gray)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
}
extension Date {
private static let monthFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateFormat = "MMM"
return formatter
}()
private static let dayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateFormat = "d"
return formatter
}()
private static let weekdayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateFormat = "E"
return formatter
}()
var abbreviatedMonth: String {
Date.monthFormatter.string(from: self)
}
var dayOfMonth: String {
Date.dayFormatter.string(from: self)
}
var abbreviatedWeekday: String {
Date.weekdayFormatter.string(from: self)
}
}

View File

@ -1,8 +1,8 @@
//
// SplitPickerView.swift
// ExercisePickerView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 7:17PM.
// Created by rzen on 7/13/25 at 7:17 PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
@ -11,37 +11,65 @@ import SwiftUI
import SwiftData
struct ExercisePickerView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State private var exerciseLists: [String: ExerciseList] = [:]
@State private var selectedListName: String? = nil
@Query(sort: [SortDescriptor(\ExerciseType.name)]) private var exerciseTypes: [ExerciseType]
// @Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
var onExerciseSelected: (Exercise) -> Void
var onExerciseSelected: (String) -> Void
var body: some View {
NavigationStack {
VStack {
Form {
ForEach (exerciseTypes) { exerciseType in
if let exercises = exerciseType.exercises, !exercises.isEmpty {
let sortedExercises = exercises.sorted(by: { $0.name < $1.name })
Section (header: Text("\(exerciseType.name)")) {
Group {
if selectedListName == nil {
// Show list of exercise list files
List {
ForEach(sortedExercises) { exercise in
ForEach(Array(exerciseLists.keys.sorted()), id: \.self) { fileName in
if let list = exerciseLists[fileName] {
Button(action: {
onExerciseSelected(exercise)
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)
}
}
}
}
.navigationTitle("Exercise Lists")
} else if let fileName = selectedListName, let list = exerciseLists[fileName] {
// Show exercises in the selected list
List {
ForEach(list.exercises) { exercise in
Button(action: {
onExerciseSelected(exercise.name)
dismiss()
}) {
ListItem(text: exercise.name)
VStack(alignment: .leading) {
Text(exercise.name)
.font(.headline)
Text(exercise.type)
.font(.caption)
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
.padding(.vertical, 2)
}
}
}
.navigationTitle(list.name)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Back") {
selectedListName = nil
}
}
}
}
}
@ -53,5 +81,12 @@ struct ExercisePickerView: View {
}
}
}
.onAppear {
loadExerciseLists()
}
}
private func loadExerciseLists() {
exerciseLists = ExerciseListLoader.loadExerciseLists()
}
}

View File

@ -15,10 +15,8 @@ struct SplitPickerView: View {
@Environment(\.dismiss) private var dismiss
@Query(sort: [SortDescriptor(\Split.name)]) private var splits: [Split]
@Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
var onSplitSelected: (Split) -> Void
// var onExerciseSelected: (Exercise) -> Void
var body: some View {
NavigationStack {

View File

@ -49,7 +49,7 @@ struct WorkoutEditView: View {
// List {
// ForEach (workoutLogs) { log in
// ListItem(
// title: log.exercise?.name ?? Exercise.unnamed,
// title: log.exerciseName,
// subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
// )
// }

View File

@ -21,7 +21,7 @@ struct WorkoutLogEditView: View {
NavigationStack {
Form {
Section (header: Text("Exercise")) {
Text("\(workoutLog.exercise?.name ?? Exercise.unnamed)")
Text(workoutLog.exerciseName)
.font(.headline)
}
@ -86,20 +86,20 @@ struct WorkoutLogEditView: View {
let split = workoutLog.workout?.split
// Find the matching exercise in split.exercises by name
// if let exercises = split?.exercises {
// for exerciseAssignment in exercises {
// if exerciseAssignment.exercise.name == workoutLog.exercise.name {
// // Update the sets, reps, and weight in the split exercise assignment
// exerciseAssignment.sets = workoutLog.sets
// exerciseAssignment.reps = workoutLog.reps
// exerciseAssignment.weight = workoutLog.weight
//
// // Save the changes to the split
// try? modelContext.save()
// break
// }
// }
// }
if let exercises = split?.exercises {
for exerciseAssignment in exercises {
if exerciseAssignment.exerciseName == workoutLog.exerciseName {
// Update the sets, reps, and weight in the split exercise assignment
exerciseAssignment.sets = workoutLog.sets
exerciseAssignment.reps = workoutLog.reps
exerciseAssignment.weight = workoutLog.weight
// Save the changes to the split
try? modelContext.save()
break
}
}
}
}
}

View File

@ -22,7 +22,7 @@ struct WorkoutLogView: View {
var sortedWorkoutLogs: [WorkoutLog] {
if let logs = workout.logs {
logs.sorted(by: {
$0.order == $1.order ? $0.exercise!.name < $1.exercise!.name : $0.order < $1.order
$0.order == $1.order ? $0.exerciseName < $1.exerciseName : $0.order < $1.order
})
} else {
[]
@ -41,7 +41,7 @@ struct WorkoutLogView: View {
CheckboxListItem(
status: workoutLogStatus,
title: log.exercise?.name ?? Exercise.unnamed,
title: log.exerciseName,
subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
)
@ -85,11 +85,12 @@ struct WorkoutLogView: View {
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
Button {
itemToDelete = log
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.secondary)
Button {
itemToEdit = log
} label: {
@ -110,11 +111,11 @@ struct WorkoutLogView: View {
}
}
.sheet(isPresented: $showingAddSheet) {
ExercisePickerView { exercise in
let setsRepsWeight = getSetsRepsWeight(exercise, in: modelContext)
ExercisePickerView { exerciseName in
let setsRepsWeight = getSetsRepsWeight(exerciseName, in: modelContext)
let workoutLog = WorkoutLog(
workout: workout,
exercise: exercise,
exerciseName: exerciseName,
date: Date(),
sets: setsRepsWeight.sets,
reps: setsRepsWeight.reps,
@ -149,20 +150,18 @@ struct WorkoutLogView: View {
itemToDelete = nil
}
} message: {
Text("Are you sure you want to delete workout started \(itemToDelete?.exercise?.name ?? "this item")?")
Text("Are you sure you want to delete workout started \(itemToDelete?.exerciseName ?? "this item")?")
}
}
func getSetsRepsWeight(_ exercise: Exercise, in modelContext: ModelContext) -> SetsRepsWeight {
func getSetsRepsWeight(_ exerciseName: String, in modelContext: ModelContext) -> SetsRepsWeight {
// Use a single expression predicate that works with SwiftData
let exerciseID = exercise.persistentModelID
print("Searching for exercise ID: \(exerciseID)")
print("Searching for exercise name: \(exerciseName)")
var descriptor = FetchDescriptor<WorkoutLog>(
predicate: #Predicate<WorkoutLog> { log in
log.exercise?.persistentModelID == exerciseID
log.exerciseName == exerciseName
},
sortBy: [SortDescriptor(\WorkoutLog.date, order: .reverse)]
)

View File

@ -34,7 +34,8 @@ struct WorkoutsView: View {
List {
ForEach (workouts) { workout in
NavigationLink(destination: WorkoutLogView(workout: workout)) {
ListItem(
CalendarListItem(
date: workout.start,
title: workout.split?.name ?? Split.unnamed,
subtitle: workout.label
)
@ -95,13 +96,11 @@ struct WorkoutsView: View {
SplitPickerView { split in
let workout = Workout(start: Date(), split: split)
modelContext.insert(workout)
if let exercises = split.exercises {
for assignment in exercises {
if let exercise = assignment.exercise {
let workoutLog = WorkoutLog(
workout: workout,
exercise: exercise,
exerciseName: assignment.exerciseName,
date: Date(),
order: assignment.order,
sets: assignment.sets,
@ -109,9 +108,6 @@ struct WorkoutsView: View {
weight: assignment.weight
)
modelContext.insert(workoutLog)
} else {
logger.debug("An exercise entity for a split is nil")
}
}
}
try? modelContext.save()