This commit is contained in:
2025-07-18 17:41:23 -04:00
parent 66f257609f
commit 34942bfc48
9 changed files with 337 additions and 111 deletions

View File

@ -0,0 +1,56 @@
//
// DraggableSplitItem.swift
// Workouts
//
// Created by rzen on 7/18/25 at 2:45PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct DraggableSplitItem: View {
var name: String
var color: Color
var systemImageName: String
var exerciseCount: Int
var body: some View {
VStack {
ZStack(alignment: .bottom) {
// Golden ratio rectangle (1:1.618)
RoundedRectangle(cornerRadius: 12)
.fill(
LinearGradient(
gradient: Gradient(colors: [color, color.darker(by: 0.2)]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.aspectRatio(1.618, contentMode: .fit)
.shadow(radius: 2)
VStack {
// Icon in the center
Image(systemName: systemImageName)
.font(.system(size: 40, weight: .bold))
.offset(y: -15)
// Name at the bottom inside the rectangle
Text(name)
.font(.headline)
.lineLimit(1)
.padding(.horizontal, 8)
Text("\(exerciseCount) exercises")
.font(.caption)
.padding(.bottom, 8)
}
.foregroundColor(.white)
}
}
}
}

View File

@ -0,0 +1,99 @@
//
// SortableForEach.swift
// Workouts
//
// Created by rzen on 7/18/25 at 2:04PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import UniformTypeIdentifiers
public struct SortableForEach<Data, Content>: View where Data: Hashable, Content: View {
@Binding var data: [Data]
@Binding var allowReordering: Bool
private let content: (Data, Bool) -> Content
@State private var draggedItem: Data?
@State private var hasChangedLocation: Bool = false
public init(_ data: Binding<[Data]>,
allowReordering: Binding<Bool>,
@ViewBuilder content: @escaping (Data, Bool) -> Content) {
_data = data
_allowReordering = allowReordering
self.content = content
}
public var body: some View {
ForEach(data, id: \.self) { item in
if allowReordering {
content(item, hasChangedLocation && draggedItem == item)
.onDrag {
draggedItem = item
return NSItemProvider(object: "\(item.hashValue)" as NSString)
}
.onDrop(of: [UTType.plainText], delegate: DragRelocateDelegate(
item: item,
data: $data,
draggedItem: $draggedItem,
hasChangedLocation: $hasChangedLocation))
} else {
content(item, false)
}
}
}
struct DragRelocateDelegate<ItemType>: DropDelegate where ItemType : Equatable {
let item: ItemType
@Binding var data: [ItemType]
@Binding var draggedItem: ItemType?
@Binding var hasChangedLocation: Bool
func dropEntered(info: DropInfo) {
guard item != draggedItem,
let current = draggedItem,
let from = data.firstIndex(of: current),
let to = data.firstIndex(of: item)
else {
return
}
hasChangedLocation = true
if data[to] != current {
withAnimation {
data.move(
fromOffsets: IndexSet(integer: from),
toOffset: (to > from) ? to + 1 : to
)
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
// Update the order property of each item to match its position in the array
updateItemOrders()
hasChangedLocation = false
draggedItem = nil
return true
}
// Helper method to update the order property of each item
private func updateItemOrders() {
// Only update orders if we're working with items that have an 'order' property
for (index, item) in data.enumerated() {
// Use key path and dynamic member lookup to set the order if available
if let orderableItem = item as? any OrderableItem {
orderableItem.updateOrder(to: index)
}
}
}
}
}

View File

@ -8,6 +8,7 @@
//
import SwiftUI
import SwiftData
struct SplitExercisesListView: View {
@Environment(\.modelContext) private var modelContext
@ -18,6 +19,7 @@ struct SplitExercisesListView: View {
@State private var showingAddSheet: Bool = false
@State private var itemToEdit: SplitExerciseAssignment? = nil
@State private var itemToDelete: SplitExerciseAssignment? = nil
@State private var createdWorkout: Workout? = nil
var body: some View {
NavigationStack {
@ -67,24 +69,59 @@ struct SplitExercisesListView: View {
.navigationTitle("\(model.name)")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
ToolbarItem(placement: .primaryAction) {
Button("Start This Split") {
let split = model
let workout = Workout(start: Date(), split: split)
modelContext.insert(workout)
if let exercises = split.exercises {
for assignment in exercises {
let workoutLog = WorkoutLog(
workout: workout,
exerciseName: assignment.exerciseName,
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
}
}
}
.sheet (isPresented: $showingAddSheet) {
ExercisePickerView { exerciseName in
itemToEdit = SplitExerciseAssignment(
split: model,
exerciseName: exerciseName,
order: 0,
sets: 3,
reps: 10,
weight: 40
)
}
}
.navigationDestination(item: $createdWorkout, destination: { workout in
WorkoutLogView(workout: workout)
})
// .sheet(item: $createdWorkout) { workout in
// NavigationStack {
// WorkoutLogView(workout: workout)
// }
// }
// .toolbar {
// ToolbarItem(placement: .navigationBarTrailing) {
// Button(action: { showingAddSheet.toggle() }) {
// Image(systemName: "plus")
// }
// }
// }
// .sheet (isPresented: $showingAddSheet) {
// ExercisePickerView { exerciseName in
// itemToEdit = SplitExerciseAssignment(
// split: model,
// exerciseName: exerciseName,
// order: 0,
// sets: 3,
// reps: 10,
// weight: 40
// )
// }
// }
.sheet(item: $itemToEdit) { item in
SplitExerciseAssignmentAddEditView(model: item)
}

View File

@ -14,83 +14,49 @@ struct SplitsView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@Query(sort: [
SortDescriptor(\Split.order),
SortDescriptor(\Split.name)
]) private var splits: [Split]
@State var splits: [Split] = []
@State private var showingAddSheet: Bool = false
@State private var allowSorting: Bool = true
var body: some View {
NavigationStack {
ScrollView {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
ForEach(splits) { split in
SortableForEach($splits, allowReordering: $allowSorting) { split, dragging in
NavigationLink {
SplitExercisesListView(model: split)
} label: {
VStack {
ZStack(alignment: .bottom) {
// Golden ratio rectangle (1:1.618)
RoundedRectangle(cornerRadius: 12)
.fill(
LinearGradient(
gradient: Gradient(colors: [split.getColor(), split.getColor().darker(by: 0.2)]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.aspectRatio(1.618, contentMode: .fit)
.shadow(radius: 2)
VStack {
// Icon in the center
Image(systemName: split.systemImage)
.font(.system(size: 40, weight: .medium))
.foregroundColor(.white)
.offset(y: -15)
// Name at the bottom inside the rectangle
Text(split.name)
.font(.headline)
.foregroundColor(.white)
.lineLimit(1)
.padding(.horizontal, 8)
.padding(.bottom, 8)
}
}
// Exercise count below the rectangle
Text("\(split.exercises?.count ?? 0) exercises")
.font(.caption)
.foregroundColor(.secondary)
}
DraggableSplitItem(
name: split.name,
color: Color.color(from: split.color),
systemImageName: split.systemImage,
exerciseCount: split.exercises?.count ?? 0
)
.overlay(dragging ? Color.white.opacity(0.8) : Color.clear)
}
}
.onMove(perform: { indices, destination in
var splitArray = Array(splits)
splitArray.move(fromOffsets: indices, toOffset: destination)
for (index, split) in splitArray.enumerated() {
split.order = index
}
if let modelContext = splitArray.first?.modelContext {
do {
try modelContext.save()
} catch {
print("Error saving after reordering: \(error)")
}
}
})
}
.padding()
}
.navigationTitle("Splits")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
.onAppear {
do {
self.splits = try modelContext.fetch(FetchDescriptor<Split>(
sortBy: [
SortDescriptor(\Split.order),
SortDescriptor(\Split.name)
]
))
} catch {
print("ERROR: failed to load splits \(error)")
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
}

View File

@ -59,16 +59,16 @@ struct WorkoutsView: View {
}
}
.navigationTitle("Workouts")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Start Workout") {
showingSplitPicker = true
}
}
}
.sheet(item: $itemToEdit) { item in
WorkoutEditView(workout: item)
}
// .toolbar {
// ToolbarItem(placement: .primaryAction) {
// Button("Start Workout") {
// showingSplitPicker = true
// }
// }
// }
// .sheet(item: $itemToEdit) { item in
// WorkoutEditView(workout: item)
// }
.confirmationDialog(
"Delete?",
isPresented: Binding<Bool>(