This commit is contained in:
2025-07-19 16:42:47 -04:00
parent 6e46775f58
commit e3c3f2c6f0
38 changed files with 556 additions and 367 deletions

View File

@ -0,0 +1,105 @@
//
// WorkoutAddEditView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 9:13PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct WorkoutLogEditView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State var workoutLog: WorkoutLog
@State private var showingSaveConfirmation = false
var body: some View {
NavigationStack {
Form {
Section (header: Text("Exercise")) {
Text(workoutLog.exerciseName)
.font(.headline)
}
Section(header: Text("Sets/Reps")) {
Stepper("Sets: \(workoutLog.sets)", value: $workoutLog.sets, in: 1...10)
Stepper("Reps: \(workoutLog.reps)", value: $workoutLog.reps, in: 1...50)
}
Section(header: Text("Weight")) {
HStack {
VStack(alignment: .center) {
Text("\(workoutLog.weight) lbs")
.font(.headline)
}
Spacer()
VStack(alignment: .trailing) {
Stepper("±1", value: $workoutLog.weight, in: 0...1000)
Stepper("±5", value: $workoutLog.weight, in: 0...1000, step: 5)
}
.frame(width: 130)
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
showingSaveConfirmation = true
}
}
}
.confirmationDialog("Save Options", isPresented: $showingSaveConfirmation) {
Button("Save Workout Log Only") {
try? modelContext.save()
dismiss()
}
Button("Save Workout Log and Update Split") {
// Save the workout log
try? modelContext.save()
// Update the split with this workout log's data
// Note: Implementation depends on how splits are updated in your app
updateSplit(from: workoutLog)
dismiss()
}
Button("Cancel", role: .cancel) {
// Do nothing, dialog will dismiss
}
}
}
}
private func updateSplit(from workoutLog: WorkoutLog) {
let split = workoutLog.workout?.split
// Find the matching exercise in split.exercises by name
if let exercises = split?.exercises {
for exercise in exercises {
if exercise.name == workoutLog.exerciseName {
// Update the sets, reps, and weight in the split exercise assignment
exercise.sets = workoutLog.sets
exercise.reps = workoutLog.reps
exercise.weight = workoutLog.weight
// Save the changes to the split
try? modelContext.save()
break
}
}
}
}
}

View File

@ -0,0 +1,215 @@
//
// WorkoutLogView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 6:58PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct WorkoutLogListView: View {
@Environment(\.modelContext) private var modelContext
@State var workout: Workout
@State private var showingAddSheet = false
@State private var itemToEdit: WorkoutLog? = nil
@State private var itemToDelete: WorkoutLog? = nil
var sortedWorkoutLogs: [WorkoutLog] {
if let logs = workout.logs {
logs.sorted(by: {
$0.order == $1.order ? $0.exerciseName < $1.exerciseName : $0.order < $1.order
})
} else {
[]
}
}
var body: some View {
Form {
Section (header: Text("\(workout.label)")) {
List {
ForEach (sortedWorkoutLogs) { log in
let workoutLogStatus = log.status?.checkboxStatus ?? (log.completed ? CheckboxStatus.checked : CheckboxStatus.unchecked)
NavigationLink(destination: ExerciseView(workoutLog: log, allLogs: sortedWorkoutLogs)) {
CheckboxListItem(
status: workoutLogStatus,
title: log.exerciseName,
subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
)
.swipeActions(edge: .leading, allowsFullSwipe: false) {
let status = log.status ?? WorkoutStatus.notStarted
if [.inProgress,.completed].contains(status) {
Button {
resetWorkout(log)
} label: {
Label("Not Started", systemImage: WorkoutStatus.notStarted.checkboxStatus.systemName)
}
.tint(WorkoutStatus.notStarted.checkboxStatus.color)
}
if [.notStarted,.completed].contains(status) {
Button {
startWorkout(log)
} label: {
Label("In Progress", systemImage: WorkoutStatus.inProgress.checkboxStatus.systemName)
}
.tint(WorkoutStatus.inProgress.checkboxStatus.color)
}
if [.notStarted,.inProgress].contains(status) {
Button {
completeWorkout(log)
} label: {
Label("Complete", systemImage: WorkoutStatus.completed.checkboxStatus.systemName)
}
.tint(WorkoutStatus.completed.checkboxStatus.color)
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button {
itemToDelete = log
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.secondary)
Button {
itemToEdit = log
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
}
}
}
}
.navigationTitle("\(workout.split?.name ?? Split.unnamed) Split")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddSheet) {
ExercisePickerView { exerciseNames in
let setsRepsWeight = getSetsRepsWeight(exerciseNames.first ?? "Exercise.unnamed", in: modelContext)
let workoutLog = WorkoutLog(
workout: workout,
exerciseName: exerciseNames.first ?? "Exercise.unnamed",
date: Date(),
sets: setsRepsWeight.sets,
reps: setsRepsWeight.reps,
weight: setsRepsWeight.weight,
completed: false
)
workout.logs?.append(workoutLog)
try? modelContext.save()
}
}
.sheet(item: $itemToEdit) { item in
WorkoutLogEditView(workoutLog: item)
}
.confirmationDialog(
"Delete?",
isPresented: Binding<Bool>(
get: { itemToDelete != nil },
set: { if !$0 { itemToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let item = itemToDelete {
withAnimation {
modelContext.delete(item)
try? modelContext.save()
itemToDelete = nil
}
}
}
Button("Cancel", role: .cancel) {
itemToDelete = nil
}
} message: {
Text("Are you sure you want to delete workout started \(itemToDelete?.exerciseName ?? "this item")?")
}
}
func startWorkout (_ log: WorkoutLog) {
withAnimation {
log.status = .inProgress
updateWorkout(log)
}
}
func resetWorkout (_ log: WorkoutLog) {
withAnimation {
log.status = .notStarted
updateWorkout(log)
}
}
func completeWorkout (_ log: WorkoutLog) {
withAnimation {
log.status = .completed
updateWorkout(log)
}
}
func updateWorkout (_ log: WorkoutLog) {
if let workout = log.workout {
if let _ = workout.logs?.first(where: { $0.status != .completed }) {
if let notStartedLogs = workout.logs?.filter({ $0.status == .notStarted }) {
if notStartedLogs.count == workout.logs?.count ?? 0 {
workout.status = .notStarted
}
}
if let _ = workout.logs?.first(where: { $0.status == .inProgress }) {
workout.status = .inProgress
}
} else {
workout.status = .completed
workout.end = Date()
}
try? modelContext.save()
}
}
func getSetsRepsWeight(_ exerciseName: String, in modelContext: ModelContext) -> SetsRepsWeight {
// Use a single expression predicate that works with SwiftData
print("Searching for exercise name: \(exerciseName)")
var descriptor = FetchDescriptor<WorkoutLog>(
predicate: #Predicate<WorkoutLog> { log in
log.exerciseName == exerciseName
},
sortBy: [SortDescriptor(\WorkoutLog.date, order: .reverse)]
)
descriptor.fetchLimit = 1
let results = try? modelContext.fetch(descriptor)
if let log = results?.first {
return SetsRepsWeight(sets: log.sets, reps: log.reps, weight: log.weight)
} else {
return SetsRepsWeight(sets: 3, reps: 10, weight: 40)
}
}
}
struct SetsRepsWeight {
let sets: Int
let reps: Int
let weight: Int
}

View File

@ -0,0 +1,35 @@
//
// WorkoutStatus.swift
// Workouts
//
// Created by rzen on 7/16/25 at 7:03PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
enum WorkoutStatus: Int, Codable {
case notStarted = 1
case inProgress = 2
case completed = 3
case skipped = 4
static var unnamed = "Undetermined"
var name: String {
switch (self) {
case .notStarted: "Not Started"
case .inProgress: "In Progress"
case .completed: "Completed"
case .skipped: "Skipped"
}
}
var checkboxStatus: CheckboxStatus {
switch (self) {
case .notStarted: .unchecked
case .inProgress: .intermediate
case .completed: .checked
case .skipped: .cancelled
}
}
}