wip
This commit is contained in:
105
Workouts/Views/WorkoutLog/WorkoutLogEditView.swift
Normal file
105
Workouts/Views/WorkoutLog/WorkoutLogEditView.swift
Normal file
@ -0,0 +1,105 @@
|
||||
//
|
||||
// WorkoutAddEditView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 9:13 PM.
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
215
Workouts/Views/WorkoutLog/WorkoutLogListView.swift
Normal file
215
Workouts/Views/WorkoutLog/WorkoutLogListView.swift
Normal file
@ -0,0 +1,215 @@
|
||||
//
|
||||
// WorkoutLogView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 6:58 PM.
|
||||
//
|
||||
// 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
|
||||
}
|
35
Workouts/Views/WorkoutLog/WorkoutStatus.swift
Normal file
35
Workouts/Views/WorkoutLog/WorkoutStatus.swift
Normal file
@ -0,0 +1,35 @@
|
||||
//
|
||||
// WorkoutStatus.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/16/25 at 7:03 PM.
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user