wip
This commit is contained in:
@ -48,6 +48,9 @@ extension Split: EditableEntity {
|
|||||||
|
|
||||||
fileprivate struct SplitFormView: View {
|
fileprivate struct SplitFormView: View {
|
||||||
@Binding var model: Split
|
@Binding var model: Split
|
||||||
|
|
||||||
|
@State private var showingAddSheet: Bool = false
|
||||||
|
@State private var itemToEdit: SplitExerciseAssignment? = nil
|
||||||
@State private var itemToDelete: SplitExerciseAssignment? = nil
|
@State private var itemToDelete: SplitExerciseAssignment? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -62,11 +65,16 @@ fileprivate struct SplitFormView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Exercises")) {
|
Section(header: Text("Exercises")) {
|
||||||
|
NavigationLink {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
List {
|
||||||
if let assignments = model.exercises, !assignments.isEmpty {
|
if let assignments = model.exercises, !assignments.isEmpty {
|
||||||
ForEach(assignments) { item in
|
let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.exercise?.name ?? Exercise.unnamed < $1.exercise?.name ?? Exercise.unnamed : $0.order < $1.order })
|
||||||
|
ForEach(sortedAssignments) { item in
|
||||||
ListItem(
|
ListItem(
|
||||||
title: item.exercise?.name ?? Exercise.unnamed,
|
title: item.exercise?.name ?? Exercise.unnamed,
|
||||||
subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs"
|
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs"
|
||||||
)
|
)
|
||||||
.swipeActions {
|
.swipeActions {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
@ -74,18 +82,48 @@ fileprivate struct SplitFormView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Label("Delete", systemImage: "trash")
|
Label("Delete", systemImage: "trash")
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
itemToEdit = item
|
||||||
|
} label: {
|
||||||
|
Label("Edit", systemImage: "pencil")
|
||||||
|
}
|
||||||
|
.tint(.indigo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text("No exercises added yet.")
|
Text("No exercises added yet.")
|
||||||
}
|
}
|
||||||
Button(action: {
|
|
||||||
// TODO: Implement add exercise functionality
|
|
||||||
}) {
|
|
||||||
Label("Add Exercise", systemImage: "plus.circle")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.confirmationDialog("Delete Exercise?", isPresented: .constant(itemToDelete != nil), titleVisibility: .visible) {
|
.navigationTitle("Exercises")
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button(action: { showingAddSheet.toggle() }) {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet (isPresented: $showingAddSheet) {
|
||||||
|
ExercisePickerView { exercise in
|
||||||
|
itemToEdit = SplitExerciseAssignment(
|
||||||
|
order: 0,
|
||||||
|
sets: exercise.sets,
|
||||||
|
reps: exercise.reps,
|
||||||
|
weight: exercise.weight,
|
||||||
|
split: model,
|
||||||
|
exercise: exercise
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(item: $itemToEdit) { item in
|
||||||
|
SplitExerciseAssignmentAddEditView(model: item)
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
"Delete Exercise?",
|
||||||
|
isPresented: .constant(itemToDelete != nil),
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Delete", role: .destructive) {
|
||||||
if let item = itemToDelete {
|
if let item = itemToDelete {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
@ -95,5 +133,13 @@ fileprivate struct SplitFormView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} label: {
|
||||||
|
ListItem(
|
||||||
|
text: "Exercises",
|
||||||
|
count: model.exercises?.count ?? 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,18 +10,32 @@ struct CloudKitSyncObserver: ViewModifier {
|
|||||||
content
|
content
|
||||||
.id(refreshID) // Force view refresh when this changes
|
.id(refreshID) // Force view refresh when this changes
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .cloudKitDataDidChange)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: .cloudKitDataDidChange)) { _ in
|
||||||
// When we receive a notification that CloudKit data changed:
|
|
||||||
// 1. Create a new UUID to force view refresh
|
|
||||||
refreshID = UUID()
|
refreshID = UUID()
|
||||||
|
|
||||||
// 2. Optionally, you can also manually refresh the model context
|
|
||||||
// This is sometimes needed for complex relationships
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try? modelContext.fetch(FetchDescriptor<Exercise>())
|
// do {
|
||||||
// Add other model types as needed
|
// for entity in modelContext.container.schema.entities {
|
||||||
|
// fetchAll<entity.Type>(of: entity.Type, from: modelContext)
|
||||||
|
// }
|
||||||
|
// } catch {
|
||||||
|
// print("ERROR: failed to fetch data on CloudKit change")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// try? modelContext.fetch(FetchDescriptor<Exercise>())
|
||||||
|
// try? modelContext.fetch(FetchDescriptor<ExerciseType>())
|
||||||
|
// try? modelContext.fetch(FetchDescriptor<Muscle>())
|
||||||
|
// try? modelContext.fetch(FetchDescriptor<MuscleGroup>())
|
||||||
|
// try? modelContext.fetch(FetchDescriptor<Exercise>())
|
||||||
|
// try? modelContext.fetch(FetchDescriptor<Exercise>())
|
||||||
|
// try? modelContext.fetch(FetchDescriptor<Exercise>())
|
||||||
|
// try? modelContext.fetch(FetchDescriptor<Exercise>())
|
||||||
|
// TODO: add more entities?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func fetchAll<T: PersistentModel>(of type: T.Type,from modelContext: ModelContext) async throws -> [T]? {
|
||||||
|
try modelContext.fetch(FetchDescriptor<T>())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension to make it easier to use the modifier
|
// Extension to make it easier to use the modifier
|
||||||
|
24
Workouts/Utils/BadgeView.swift
Normal file
24
Workouts/Utils/BadgeView.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// BadgeView.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Created by rzen on 7/14/25 at 2:20 PM.
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct BadgeView: View {
|
||||||
|
|
||||||
|
var badge: Badge
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text("\(badge.text)")
|
||||||
|
.bold()
|
||||||
|
.padding([.leading,.trailing], 5)
|
||||||
|
.background(badge.color)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,8 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ListItem: View {
|
struct ListItem: View {
|
||||||
var title: String
|
var title: String?
|
||||||
|
var text: String?
|
||||||
var subtitle: String?
|
var subtitle: String?
|
||||||
var count: Int?
|
var count: Int?
|
||||||
var badges: [Badge]? = []
|
var badges: [Badge]? = []
|
||||||
@ -18,17 +19,18 @@ struct ListItem: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
VStack (alignment: .leading) {
|
VStack (alignment: .leading) {
|
||||||
|
if let title = title {
|
||||||
Text("\(title)")
|
Text("\(title)")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
} else if let text = text {
|
||||||
|
Text("\(text)")
|
||||||
|
} else {
|
||||||
|
Text("Untitled")
|
||||||
|
}
|
||||||
HStack (alignment: .bottom) {
|
HStack (alignment: .bottom) {
|
||||||
if let badges = badges {
|
if let badges = badges {
|
||||||
ForEach (badges, id: \.self) { badge in
|
ForEach (badges, id: \.self) { badge in
|
||||||
Text("\(badge.text)")
|
BadgeView(badge: badge)
|
||||||
.bold()
|
|
||||||
.padding([.leading,.trailing], 5)
|
|
||||||
.background(badge.color)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.cornerRadius(4)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let subtitle = subtitle {
|
if let subtitle = subtitle {
|
||||||
@ -44,6 +46,7 @@ struct ListItem: View {
|
|||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(height: 40)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// SplitExerciseAssignment.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Created by rzen on 7/15/25 at 7:12 AM.
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SplitExerciseAssignmentAddEditView: View {
|
||||||
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@State var model: SplitExerciseAssignment
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
Section(header: Text("Sets/Reps")) {
|
||||||
|
Stepper("Sets: \(model.sets)", value: $model.sets, in: 1...10)
|
||||||
|
Stepper("Reps: \(model.reps)", value: $model.reps, in: 1...50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight section
|
||||||
|
Section(header: Text("Weight")) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .center) {
|
||||||
|
Text("\(model.weight) lbs")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
Stepper("±1", value: $model.weight, in: 0...1000)
|
||||||
|
Stepper("±5", value: $model.weight, in: 0...1000, step: 5)
|
||||||
|
}
|
||||||
|
.frame(width: 130)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("\(model.exercise?.name ?? Exercise.unnamed)")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button("Save") {
|
||||||
|
try? modelContext.save()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,9 @@ struct ExercisePickerView: View {
|
|||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
|
@Query(sort: [SortDescriptor(\ExerciseType.name)]) private var exerciseTypes: [ExerciseType]
|
||||||
|
|
||||||
|
// @Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
|
||||||
|
|
||||||
var onExerciseSelected: (Exercise) -> Void
|
var onExerciseSelected: (Exercise) -> Void
|
||||||
|
|
||||||
@ -22,20 +24,26 @@ struct ExercisePickerView: View {
|
|||||||
NavigationStack {
|
NavigationStack {
|
||||||
VStack {
|
VStack {
|
||||||
Form {
|
Form {
|
||||||
Section (header: Text("This Split")) {
|
ForEach (exerciseTypes) { exerciseType in
|
||||||
|
if let exercises = exerciseType.exercises, !exercises.isEmpty {
|
||||||
|
let sortedExercises = exercises.sorted(by: { $0.name < $1.name })
|
||||||
|
Section (header: Text("\(exerciseType.name)")) {
|
||||||
List {
|
List {
|
||||||
ForEach(exercises) { exercise in
|
ForEach(sortedExercises) { exercise in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
onExerciseSelected(exercise)
|
onExerciseSelected(exercise)
|
||||||
dismiss()
|
dismiss()
|
||||||
}) {
|
}) {
|
||||||
ListItem(title: exercise.name)
|
ListItem(text: exercise.name)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
@ -15,14 +15,14 @@ struct WorkoutEditView: View {
|
|||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@State var workout: Workout
|
@State var workout: Workout
|
||||||
@State var endDateHidden: Bool = false
|
@State var workoutEndDate: Date = Date()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
Section (header: Text("Split")) {
|
// Section (header: Text("Split")) {
|
||||||
Text("\(workout.split?.name ?? Split.unnamed)")
|
// Text("\(workout.split?.name ?? Split.unnamed)")
|
||||||
}
|
// }
|
||||||
|
|
||||||
Section (header: Text("Start/End")) {
|
Section (header: Text("Start/End")) {
|
||||||
DatePicker("Started", selection: $workout.start)
|
DatePicker("Started", selection: $workout.start)
|
||||||
@ -31,38 +31,35 @@ struct WorkoutEditView: View {
|
|||||||
set: { newValue in
|
set: { newValue in
|
||||||
withAnimation {
|
withAnimation {
|
||||||
if newValue {
|
if newValue {
|
||||||
workout.end = Date()
|
workoutEndDate = Date()
|
||||||
endDateHidden = false
|
workout.end = workoutEndDate
|
||||||
} else {
|
} else {
|
||||||
workout.end = nil
|
workout.end = nil
|
||||||
endDateHidden = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
if !endDateHidden {
|
if workout.end != nil {
|
||||||
DatePicker("Ended", selection: $workout.start)
|
DatePicker("Ended", selection: $workoutEndDate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
endDateHidden = workout.end == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
Section (header: Text("Workout Log")) {
|
// Section (header: Text("Workout Log")) {
|
||||||
if let workoutLogs = workout.logs {
|
// if let workoutLogs = workout.logs {
|
||||||
List {
|
// List {
|
||||||
ForEach (workoutLogs) { log in
|
// ForEach (workoutLogs) { log in
|
||||||
ListItem(
|
// ListItem(
|
||||||
title: log.exercise?.name ?? Exercise.unnamed,
|
// title: log.exercise?.name ?? Exercise.unnamed,
|
||||||
subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
|
// subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
Text("No workout logs yet")
|
// Text("No workout logs yet")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
.navigationTitle("\(workout.split?.name ?? Split.unnamed) Split")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
|
@ -20,7 +20,9 @@ struct WorkoutLogView: View {
|
|||||||
|
|
||||||
var sortedWorkoutLogs: [WorkoutLog] {
|
var sortedWorkoutLogs: [WorkoutLog] {
|
||||||
if let logs = workout.logs {
|
if let logs = workout.logs {
|
||||||
logs.sorted(by: { $0.exercise!.name < $1.exercise!.name })
|
logs.sorted(by: {
|
||||||
|
$0.completed == $1.completed ? $0.exercise!.name < $1.exercise!.name : !$0.completed
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
[]
|
[]
|
||||||
}
|
}
|
||||||
@ -43,16 +45,20 @@ struct WorkoutLogView: View {
|
|||||||
.swipeActions(edge: .leading, allowsFullSwipe: false) {
|
.swipeActions(edge: .leading, allowsFullSwipe: false) {
|
||||||
if (log.completed) {
|
if (log.completed) {
|
||||||
Button {
|
Button {
|
||||||
|
withAnimation {
|
||||||
log.completed = false
|
log.completed = false
|
||||||
try? modelContext.save()
|
try? modelContext.save()
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("Complete", systemImage: "circle.fill")
|
Label("Complete", systemImage: "circle.fill")
|
||||||
}
|
}
|
||||||
.tint(.green)
|
.tint(.green)
|
||||||
} else {
|
} else {
|
||||||
Button {
|
Button {
|
||||||
|
withAnimation {
|
||||||
log.completed = true
|
log.completed = true
|
||||||
try? modelContext.save()
|
try? modelContext.save()
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("Reset", systemImage: "checkmark.circle.fill")
|
Label("Reset", systemImage: "checkmark.circle.fill")
|
||||||
}
|
}
|
||||||
@ -76,7 +82,7 @@ struct WorkoutLogView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("\(workout.split?.name ?? Split.unnamed)")
|
.navigationTitle("\(workout.split?.name ?? Split.unnamed) Split")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button(action: { showingAddSheet.toggle() }) {
|
Button(action: { showingAddSheet.toggle() }) {
|
||||||
|
Reference in New Issue
Block a user