- Migrate from SwiftData to CoreData with CloudKit sync - Add core models: Split, Exercise, Workout, WorkoutLog - Implement tab-based UI: Workout Logs, Splits, Settings - Add SF Symbols picker for split icons - Add exercise picker filtered by split with exclusion of added exercises - Integrate IndieAbout for settings/about section - Add Yams for YAML exercise definition parsing - Include starter exercise libraries (bodyweight, Planet Fitness) - Add Date extensions for formatting (formattedTime, isSameDay) - Format workout date ranges to show time-only for same-day end dates - Add build number update script - Add app icons
164 lines
5.5 KiB
Swift
164 lines
5.5 KiB
Swift
//
|
||
// ExerciseListView.swift
|
||
// Workouts
|
||
//
|
||
// Created by rzen on 7/18/25 at 8:38 AM.
|
||
//
|
||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||
//
|
||
|
||
import SwiftUI
|
||
import CoreData
|
||
|
||
struct ExerciseListView: View {
|
||
@Environment(\.managedObjectContext) private var viewContext
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
@ObservedObject var split: Split
|
||
|
||
@State private var showingAddSheet: Bool = false
|
||
@State private var itemToEdit: Exercise? = nil
|
||
@State private var itemToDelete: Exercise? = nil
|
||
@State private var createdWorkout: Workout? = nil
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
Form {
|
||
let sortedExercises = split.exercisesArray
|
||
|
||
if !sortedExercises.isEmpty {
|
||
ForEach(sortedExercises, id: \.objectID) { item in
|
||
ListItem(
|
||
title: item.name,
|
||
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs"
|
||
)
|
||
.swipeActions {
|
||
Button {
|
||
itemToDelete = item
|
||
} label: {
|
||
Label("Delete", systemImage: "trash")
|
||
}
|
||
.tint(.red)
|
||
Button {
|
||
itemToEdit = item
|
||
} label: {
|
||
Label("Edit", systemImage: "pencil")
|
||
}
|
||
.tint(.indigo)
|
||
}
|
||
}
|
||
.onMove(perform: moveExercises)
|
||
|
||
Button {
|
||
showingAddSheet = true
|
||
} label: {
|
||
ListItem(title: "Add Exercise")
|
||
}
|
||
} else {
|
||
Text("No exercises added yet.")
|
||
Button(action: { showingAddSheet.toggle() }) {
|
||
ListItem(title: "Add Exercise")
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle("\(split.name)")
|
||
}
|
||
.toolbar {
|
||
ToolbarItem(placement: .primaryAction) {
|
||
Button("Start This Split") {
|
||
startWorkout()
|
||
}
|
||
}
|
||
}
|
||
.navigationDestination(item: $createdWorkout) { workout in
|
||
WorkoutLogListView(workout: workout)
|
||
}
|
||
.sheet(isPresented: $showingAddSheet) {
|
||
ExercisePickerView(onExerciseSelected: { exerciseNames in
|
||
addExercises(names: exerciseNames)
|
||
}, allowMultiSelect: true)
|
||
}
|
||
.sheet(item: $itemToEdit) { item in
|
||
ExerciseAddEditView(exercise: item)
|
||
}
|
||
.confirmationDialog(
|
||
"Delete Exercise?",
|
||
isPresented: .constant(itemToDelete != nil),
|
||
titleVisibility: .visible
|
||
) {
|
||
Button("Delete", role: .destructive) {
|
||
if let item = itemToDelete {
|
||
withAnimation {
|
||
viewContext.delete(item)
|
||
try? viewContext.save()
|
||
itemToDelete = nil
|
||
}
|
||
}
|
||
}
|
||
Button("Cancel", role: .cancel) {
|
||
itemToDelete = nil
|
||
}
|
||
}
|
||
}
|
||
|
||
private func moveExercises(from source: IndexSet, to destination: Int) {
|
||
var exercises = split.exercisesArray
|
||
exercises.move(fromOffsets: source, toOffset: destination)
|
||
for (index, exercise) in exercises.enumerated() {
|
||
exercise.order = Int32(index)
|
||
}
|
||
try? viewContext.save()
|
||
}
|
||
|
||
private func startWorkout() {
|
||
let workout = Workout(context: viewContext)
|
||
workout.start = Date()
|
||
workout.end = Date()
|
||
workout.status = .notStarted
|
||
workout.split = split
|
||
|
||
for exercise in split.exercisesArray {
|
||
let workoutLog = WorkoutLog(context: viewContext)
|
||
workoutLog.exerciseName = exercise.name
|
||
workoutLog.date = Date()
|
||
workoutLog.order = exercise.order
|
||
workoutLog.sets = exercise.sets
|
||
workoutLog.reps = exercise.reps
|
||
workoutLog.weight = exercise.weight
|
||
workoutLog.status = .notStarted
|
||
workoutLog.workout = workout
|
||
}
|
||
|
||
try? viewContext.save()
|
||
createdWorkout = workout
|
||
}
|
||
|
||
private func addExercises(names: [String]) {
|
||
if names.count == 1 {
|
||
let exercise = Exercise(context: viewContext)
|
||
exercise.name = names.first ?? "Unnamed"
|
||
exercise.order = Int32(split.exercisesArray.count)
|
||
exercise.sets = 3
|
||
exercise.reps = 10
|
||
exercise.weight = 40
|
||
exercise.loadType = Int32(LoadType.weight.rawValue)
|
||
exercise.split = split
|
||
try? viewContext.save()
|
||
itemToEdit = exercise
|
||
} else {
|
||
let existingNames = Set(split.exercisesArray.map { $0.name })
|
||
for name in names where !existingNames.contains(name) {
|
||
let exercise = Exercise(context: viewContext)
|
||
exercise.name = name
|
||
exercise.order = Int32(split.exercisesArray.count)
|
||
exercise.sets = 3
|
||
exercise.reps = 10
|
||
exercise.weight = 40
|
||
exercise.loadType = Int32(LoadType.weight.rawValue)
|
||
exercise.split = split
|
||
}
|
||
try? viewContext.save()
|
||
}
|
||
}
|
||
}
|