This commit is contained in:
2025-07-25 17:30:11 -04:00
parent 310c120ca3
commit 3fd6887ce7
55 changed files with 1062 additions and 649 deletions
@@ -1,6 +1,7 @@
{
"images" : [
{
"filename" : "DumbBellIcon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
@@ -12,6 +13,7 @@
"value" : "dark"
}
],
"filename" : "DumbBellIcon 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
@@ -23,6 +25,7 @@
"value" : "tinted"
}
],
"filename" : "DumbBellIcon 2.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

+24 -2
View File
@@ -5,8 +5,15 @@ import SwiftData
final class Workout {
var start: Date = Date()
var end: Date?
var status: WorkoutStatus? = WorkoutStatus.notStarted
var status: Int = 1
// var status: WorkoutStatus = WorkoutStatus.notStarted
//case notStarted = 1
//case inProgress = 2
//case completed = 3
//case skipped = 4
@Relationship(deleteRule: .nullify)
var split: Split?
@@ -20,10 +27,25 @@ final class Workout {
}
var label: String {
if status == .completed, let endDate = end {
if status == 3, let endDate = end {
// if status == .completed, let endDate = end {
return "\(start.formattedDate())\(endDate.formattedDate())"
} else {
return start.formattedDate()
}
}
var statusName: String {
if status == 1 {
return "Not Started"
} else if status == 2 {
return "In Progress"
} else if status == 3 {
return "Completed"
} else if status == 4 {
return "Skipped"
} else {
return "In progress"
}
}
}
+3
View File
@@ -11,6 +11,9 @@ final class WorkoutLog {
var order: Int = 0
var exerciseName: String = ""
var currentStateIndex: Int? = nil
var elapsedSeconds: Int? = nil
var completed: Bool = false
@Relationship(deleteRule: .nullify)
@@ -10,21 +10,6 @@ struct WorkoutsMigrationPlan: SchemaMigrationPlan {
toVersion: SchemaV1.self,
willMigrate: { context in
print("migrating from v1 to v1")
let workouts = try? context.fetch(FetchDescriptor<Workout>())
workouts?.forEach { workout in
if let status = workout.status {
} else {
workout.status = .notStarted
}
// if let endDate = workout.end {
//
// } else {
// workout.end = Date()
// }
workout.end = Date()
}
},
didMigrate: { _ in
// No additional actions needed after migration
+1 -1
View File
@@ -21,7 +21,7 @@ struct SplitAddEditView: View {
var body: some View {
NavigationStack {
Form {
Form {
Section(header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
+198
View File
@@ -0,0 +1,198 @@
//
// SplitDetailView.swift
// Workouts
//
// Created by rzen on 7/25/25 at 3:27PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct SplitDetailView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State 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
@State private var showingDeleteConfirmation: Bool = false
var body: some View {
NavigationStack {
Form {
Section (header: Text("What is a Split?")) {
Text("A “split” is simply how you divide (or “split up”) your weekly training across different days. Instead of working every muscle group every session, you assign certain muscle groups, movement patterns, or training emphases to specific days.")
.font(.caption)
}
Section (header: Text("Exercises")) {
List {
if let assignments = split.exercises, !assignments.isEmpty {
let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.name < $1.name : $0.order < $1.order })
ForEach(sortedAssignments) { item in
ListItem(
title: item.name,
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs \(item.order)"
)
.swipeActions {
Button {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
.onMove(perform: { indices, destination in
var exerciseArray = Array(sortedAssignments)
exerciseArray.move(fromOffsets: indices, toOffset: destination)
for (index, exercise) in exerciseArray.enumerated() {
exercise.order = index
}
if let modelContext = exerciseArray.first?.modelContext {
do {
try modelContext.save()
} catch {
print("Error saving after reordering: \(error)")
}
}
})
Button {
showingAddSheet = true
} label: {
ListItem(title: "Add Exercise")
}
} else {
Text("No exercises added yet.")
Button(action: { showingAddSheet.toggle() }) {
ListItem(title: "Add Exercise")
}
}
}
}
Button ("Delete This Split", role: .destructive) {
showingDeleteConfirmation = true
}
.tint(.red)
}
.navigationTitle("\(split.name)")
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Start This Split") {
let workout = Workout(start: Date(), end: Date(), split: split)
modelContext.insert(workout)
if let exercises = split.exercises {
for assignment in exercises {
let workoutLog = WorkoutLog(
workout: workout,
exerciseName: assignment.name,
date: Date(),
order: assignment.order,
sets: assignment.sets,
reps: assignment.reps,
weight: assignment.weight
)
modelContext.insert(workoutLog)
}
}
try? modelContext.save()
// Set the created workout to trigger navigation
createdWorkout = workout
}
}
}
.navigationDestination(item: $createdWorkout, destination: { workout in
WorkoutLogListView(workout: workout)
})
.sheet (isPresented: $showingAddSheet) {
ExercisePickerView(onExerciseSelected: { exerciseNames in
let splitId = split.persistentModelID
print("exerciseNames: \(exerciseNames)")
if exerciseNames.count == 1 {
itemToEdit = Exercise(
split: split,
exerciseName: exerciseNames.first ?? "Exercise.unnamed",
order: 0,
sets: 3,
reps: 10,
weight: 40
)
} else {
for exerciseName in exerciseNames {
var duplicateExercise: [Exercise]? = nil
do {
duplicateExercise = try modelContext.fetch(FetchDescriptor<Exercise>(predicate: #Predicate{ exercise in
exerciseName == exercise.name && splitId == exercise.split?.persistentModelID
}))
} catch {
print("ERROR: failed to fetch \(exerciseName)")
}
if let dup = duplicateExercise, dup.count > 0 {
print("Skipping duplicate \(exerciseName) found \(dup.count) duplicate(s)")
} else {
print("Creating \(exerciseName) for \(split.name)")
modelContext.insert(Exercise(
split: split,
exerciseName: exerciseName,
order: 0,
sets: 3,
reps: 10,
weight: 40
))
}
}
}
try? modelContext.save()
}, allowMultiSelect: true)
}
.sheet(item: $itemToEdit) { item in
ExerciseAddEditView(model: item)
}
.confirmationDialog(
"Delete Exercise?",
isPresented: .constant(itemToDelete != nil),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let item = itemToDelete {
withAnimation {
modelContext.delete(item)
try? modelContext.save()
itemToDelete = nil
}
}
}
}
.confirmationDialog(
"Delete This Split?",
isPresented: $showingDeleteConfirmation,
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
modelContext.delete(split)
try? modelContext.save()
dismiss()
}
}
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ struct SplitsView: View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
SortableForEach($splits, allowReordering: $allowSorting) { split, dragging in
NavigationLink {
ExerciseListView(split: split)
SplitDetailView(split: split)
} label: {
SplitItem(
name: split.name,
@@ -40,7 +40,7 @@ struct WorkoutLogListView: View {
CheckboxListItem(
status: workoutLogStatus,
title: log.exerciseName,
subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
subtitle: "\(log.sets) × \(log.reps) reps × \(log.weight) lbs"
)
.swipeActions(edge: .leading, allowsFullSwipe: false) {
let status = log.status ?? WorkoutStatus.notStarted
@@ -89,6 +89,21 @@ struct WorkoutLogListView: View {
}
}
.onMove(perform: { indices, destination in
var workoutLogArray = Array(sortedWorkoutLogs)
workoutLogArray.move(fromOffsets: indices, toOffset: destination)
for (index, log) in workoutLogArray.enumerated() {
log.order = index
}
if let modelContext = workoutLogArray.first?.modelContext {
do {
try modelContext.save()
} catch {
print("Error saving after reordering: \(error)")
}
}
})
}
}
}
@@ -171,14 +186,14 @@ struct WorkoutLogListView: View {
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
workout.status = WorkoutStatus.notStarted.rawValue
}
}
if let _ = workout.logs?.first(where: { $0.status == .inProgress }) {
workout.status = .inProgress
workout.status = WorkoutStatus.inProgress.rawValue
}
} else {
workout.status = .completed
workout.status = WorkoutStatus.completed.rawValue
workout.end = Date()
}
try? modelContext.save()
@@ -7,6 +7,8 @@
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import Foundation
enum WorkoutStatus: Int, Codable {
case notStarted = 1
case inProgress = 2
@@ -25,12 +25,12 @@ struct WorkoutEditView: View {
}
Section (header: Text("Status")) {
Text("\(workout.status?.name ?? WorkoutStatus.unnamed)")
Text("\(workout.statusName)")
}
Section (header: Text("Start/End")) {
DatePicker("Started", selection: $workout.start)
if workout.status == .completed {
if workout.status == WorkoutStatus.completed.rawValue {
DatePicker("Ended", selection: $workoutEndDate)
}
}
@@ -46,7 +46,7 @@ struct WorkoutEditView: View {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
try? modelContext.save()
if workout.status == .completed {
if workout.status == WorkoutStatus.completed.rawValue {
workout.end = workoutEndDate
}
dismiss()
@@ -37,8 +37,8 @@ struct WorkoutListView: View {
CalendarListItem(
date: workout.start,
title: workout.split?.name ?? Split.unnamed,
subtitle: "\(workout.status == .completed ? workout.start.humanTimeInterval(to: (workout.end ?? Date())) : "\(workout.start.formattedDate()) - \(workout.status?.name ?? WorkoutStatus.unnamed)" )",
subtitle2: "\(workout.status?.name ?? WorkoutStatus.unnamed)"
subtitle: "\(workout.status == WorkoutStatus.completed.rawValue ? workout.start.humanTimeInterval(to: (workout.end ?? Date())) : "\(workout.start.formattedDate()) - \(workout.statusName)" )",
subtitle2: "\(workout.statusName)"
)
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {