This commit is contained in:
2025-07-13 21:54:09 -04:00
parent 0545f5dbc7
commit bdaa406876
33 changed files with 984 additions and 714 deletions

View File

@ -1,9 +1,10 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class Exercise: ListableItem {
@Attribute(.unique) var name: String = ""
final class Exercise {
var name: String = ""
var setup: String = ""
var descr: String = ""
var sets: Int = 0
@ -31,3 +32,62 @@ final class Exercise: ListableItem {
self.weight = weight
}
}
extension Exercise: EditableEntity {
static func createNew() -> Exercise {
return Exercise(name: "", setup: "", descr: "", sets: 3, reps: 10, weight: 30)
}
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)
}
Section(header: Text("Setup")) {
TextEditor(text: $model.setup)
.frame(minHeight: 100)
}
Section(header: Text("Weight")) {
HStack {
Text("\(model.weight)")
.bold()
Text("lbs")
Spacer()
Stepper("", value: $model.weight, in: 0...1000)
}
}
}
}

View File

@ -1,9 +1,10 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class ExerciseType: ListableItem {
@Attribute(.unique) var name: String = ""
final class ExerciseType {
var name: String = ""
var descr: String = ""
@Relationship(deleteRule: .nullify)
@ -14,3 +15,34 @@ final class ExerciseType: ListableItem {
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,9 +1,10 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class Muscle: ListableItem {
@Attribute(.unique) var name: String = ""
final class Muscle {
var name: String = ""
var descr: String = ""
@ -13,9 +14,60 @@ final class Muscle: ListableItem {
@Relationship(deleteRule: .nullify)
var exercises: [Exercise]? = []
init(name: String, descr: String, muscleGroup: MuscleGroup) {
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,9 +1,10 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class MuscleGroup: ListableItem {
@Attribute(.unique) var name: String = ""
final class MuscleGroup {
var name: String = ""
var descr: String = ""
@Relationship(deleteRule: .nullify)
@ -14,3 +15,35 @@ final class MuscleGroup: ListableItem {
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

@ -1,9 +1,10 @@
import Foundation
import SwiftData
import SwiftUI
@Model
final class Split: ListableItem {
@Attribute(.unique) var name: String = ""
final class Split {
var name: String = ""
var intro: String = ""
@Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split)
@ -17,3 +18,80 @@ final class Split: ListableItem {
self.intro = intro
}
}
// MARK: - EditableEntity Conformance
extension Split: EditableEntity {
var count: Int? {
return self.exercises?.count
}
static func createNew() -> Split {
return Split(name: "", intro: "")
}
static var navigationTitle: String {
return "Splits"
}
@ViewBuilder
static func formView(for model: Split) -> some View {
EntityAddEditView(model: model) { $model in
SplitFormView(model: $model)
}
}
}
// MARK: - Private Form View
fileprivate struct SplitFormView: View {
@Binding var model: Split
@State private var itemToDelete: SplitExerciseAssignment? = nil
var body: some View {
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Description")) {
TextEditor(text: $model.intro)
.frame(minHeight: 100)
}
Section(header: Text("Exercises")) {
if let assignments = model.exercises, !assignments.isEmpty {
ForEach(assignments) { item in
ListItem(
title: item.exercise?.name ?? "Unnamed",
subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs"
)
.swipeActions {
Button(role: .destructive) {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
}
}
} else {
Text("No exercises added yet.")
}
Button(action: {
// TODO: Implement add exercise functionality
}) {
Label("Add Exercise", systemImage: "plus.circle")
}
}
.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
}
}
}
}
}
}

View File

@ -17,4 +17,8 @@ final class Workout {
self.end = end
self.split = split
}
var label: String {
start.formattedDate()
}
}

View File

@ -15,7 +15,7 @@ final class WorkoutLog {
@Relationship(deleteRule: .nullify)
var exercise: Exercise?
init(date: Date, sets: Int, reps: Int, weight: Int, completed: Bool, workout: Workout, exercise: Exercise) {
init(workout: Workout, exercise: Exercise, date: Date, sets: Int, reps: Int, weight: Int, completed: Bool) {
self.date = date
self.sets = sets
self.reps = reps