Files
workouts/Workouts/Views/Exercises/ExerciseListView.swift
rzen 13313a32d3 Add CoreData-based workout tracking app with iOS and watchOS targets
- 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
2026-01-19 06:42:15 -05:00

164 lines
5.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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()
}
}
}