diff --git a/UI.md b/UI.md index d5f9057..6bb0289 100644 --- a/UI.md +++ b/UI.md @@ -4,7 +4,7 @@ Each tab is a root of its own navigation stack. -- Workout Log +- Workouts - Reports - Settings diff --git a/Workouts.xcodeproj/project.pbxproj b/Workouts.xcodeproj/project.pbxproj index 2dcf1d5..6d17388 100644 --- a/Workouts.xcodeproj/project.pbxproj +++ b/Workouts.xcodeproj/project.pbxproj @@ -6,8 +6,37 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + A45FA1FE2E27171B00581607 /* Worksouts Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = A45FA1F12E27171A00581607 /* Worksouts Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + A45FA1FC2E27171B00581607 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A45FA0892E21B3DC00581607 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A45FA1F02E27171A00581607; + remoteInfo = "Worksouts Watch App"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + A45FA2022E27171B00581607 /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + A45FA1FE2E27171B00581607 /* Worksouts Watch App.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ A45FA0912E21B3DD00581607 /* Workouts.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Workouts.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A45FA1F12E27171A00581607 /* Worksouts Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Worksouts Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -29,6 +58,11 @@ path = Workouts; sourceTree = ""; }; + A45FA1F22E27171A00581607 /* Worksouts Watch App */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "Worksouts Watch App"; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -39,6 +73,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A45FA1EE2E27171A00581607 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -46,6 +87,7 @@ isa = PBXGroup; children = ( A45FA0932E21B3DD00581607 /* Workouts */, + A45FA1F22E27171A00581607 /* Worksouts Watch App */, A45FA0922E21B3DD00581607 /* Products */, ); sourceTree = ""; @@ -54,6 +96,7 @@ isa = PBXGroup; children = ( A45FA0912E21B3DD00581607 /* Workouts.app */, + A45FA1F12E27171A00581607 /* Worksouts Watch App.app */, ); name = Products; sourceTree = ""; @@ -68,10 +111,12 @@ A45FA08D2E21B3DD00581607 /* Sources */, A45FA08E2E21B3DD00581607 /* Frameworks */, A45FA08F2E21B3DD00581607 /* Resources */, + A45FA2022E27171B00581607 /* Embed Watch Content */, ); buildRules = ( ); dependencies = ( + A45FA1FD2E27171B00581607 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( A45FA0932E21B3DD00581607 /* Workouts */, @@ -83,6 +128,28 @@ productReference = A45FA0912E21B3DD00581607 /* Workouts.app */; productType = "com.apple.product-type.application"; }; + A45FA1F02E27171A00581607 /* Worksouts Watch App */ = { + isa = PBXNativeTarget; + buildConfigurationList = A45FA1FF2E27171B00581607 /* Build configuration list for PBXNativeTarget "Worksouts Watch App" */; + buildPhases = ( + A45FA1ED2E27171A00581607 /* Sources */, + A45FA1EE2E27171A00581607 /* Frameworks */, + A45FA1EF2E27171A00581607 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + A45FA1F22E27171A00581607 /* Worksouts Watch App */, + ); + name = "Worksouts Watch App"; + packageProductDependencies = ( + ); + productName = "Worksouts Watch App"; + productReference = A45FA1F12E27171A00581607 /* Worksouts Watch App.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -96,6 +163,9 @@ A45FA0902E21B3DD00581607 = { CreatedOnToolsVersion = 16.2; }; + A45FA1F02E27171A00581607 = { + CreatedOnToolsVersion = 16.2; + }; }; }; buildConfigurationList = A45FA08C2E21B3DC00581607 /* Build configuration list for PBXProject "Workouts" */; @@ -113,6 +183,7 @@ projectRoot = ""; targets = ( A45FA0902E21B3DD00581607 /* Workouts */, + A45FA1F02E27171A00581607 /* Worksouts Watch App */, ); }; /* End PBXProject section */ @@ -125,6 +196,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A45FA1EF2E27171A00581607 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -135,8 +213,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A45FA1ED2E27171A00581607 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + A45FA1FD2E27171B00581607 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A45FA1F02E27171A00581607 /* Worksouts Watch App */; + targetProxy = A45FA1FC2E27171B00581607 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ A45FA0A32E21B3DE00581607 /* Debug */ = { isa = XCBuildConfiguration; @@ -319,6 +412,68 @@ }; name = Release; }; + A45FA2002E27171B00581607 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "Worksouts Watch App/Worksouts Watch App.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Worksouts Watch App/Preview Content\""; + DEVELOPMENT_TEAM = C32Z8JNLG6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Worksouts; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.rzen.indie.Workouts; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.rzen.indie.Workouts.watchkitapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 11.2; + }; + name = Debug; + }; + A45FA2012E27171B00581607 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "Worksouts Watch App/Worksouts Watch App.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Worksouts Watch App/Preview Content\""; + DEVELOPMENT_TEAM = C32Z8JNLG6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Worksouts; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.rzen.indie.Workouts; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.rzen.indie.Workouts.watchkitapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 11.2; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -340,6 +495,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + A45FA1FF2E27171B00581607 /* Build configuration list for PBXNativeTarget "Worksouts Watch App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A45FA2002E27171B00581607 /* Debug */, + A45FA2012E27171B00581607 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = A45FA0892E21B3DC00581607 /* Project object */; diff --git a/Workouts.xcodeproj/xcuserdata/rzen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Workouts.xcodeproj/xcuserdata/rzen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..941093d --- /dev/null +++ b/Workouts.xcodeproj/xcuserdata/rzen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/Workouts.xcodeproj/xcuserdata/rzen.xcuserdatad/xcschemes/xcschememanagement.plist b/Workouts.xcodeproj/xcuserdata/rzen.xcuserdatad/xcschemes/xcschememanagement.plist index 76ce1d3..b931437 100644 --- a/Workouts.xcodeproj/xcuserdata/rzen.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Workouts.xcodeproj/xcuserdata/rzen.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,6 +9,11 @@ orderHint 0 + Worksouts Watch App.xcscheme_^#shared#^_ + + orderHint + 1 + diff --git a/Workouts/Models/Exercise.swift b/Workouts/Models/Exercise.swift index cd0dcae..0ddd46f 100644 --- a/Workouts/Models/Exercise.swift +++ b/Workouts/Models/Exercise.swift @@ -5,11 +5,7 @@ import SwiftUI @Model final class Exercise { var name: String = "" - var setup: String = "" var descr: String = "" - var sets: Int = 0 - var reps: Int = 0 - var weight: Int = 0 @Relationship(deleteRule: .nullify, inverse: \ExerciseType.exercises) var type: ExerciseType? @@ -23,13 +19,9 @@ final class Exercise { @Relationship(deleteRule: .nullify, inverse: \WorkoutLog.exercise) var logs: [WorkoutLog]? = [] - init(name: String, setup: String, descr: String, sets: Int, reps: Int, weight: Int) { + init(name: String, descr: String) { self.name = name - self.setup = setup self.descr = descr - self.sets = sets - self.reps = reps - self.weight = weight } static let unnamed = "Unnamed Exercise" @@ -37,7 +29,7 @@ final class Exercise { extension Exercise: EditableEntity { static func createNew() -> Exercise { - return Exercise(name: "", setup: "", descr: "", sets: 3, reps: 10, weight: 30) + return Exercise(name: "", descr: "") } static var navigationTitle: String { @@ -76,20 +68,5 @@ fileprivate struct ExerciseFormView: View { TextEditor(text: $model.descr) .frame(minHeight: 100) } - - Section(header: Text("Setup")) { - TextEditor(text: $model.setup) - .frame(minHeight: 100) - } - - Section(header: Text("Weight")) { - HStack { - Text("\(model.weight)") - .bold() - Text("lbs") - Spacer() - Stepper("", value: $model.weight, in: 0...1000) - } - } } } diff --git a/Workouts/Models/Split.swift b/Workouts/Models/Split.swift index 453df8c..c844f57 100644 --- a/Workouts/Models/Split.swift +++ b/Workouts/Models/Split.swift @@ -6,6 +6,27 @@ import SwiftUI final class Split { var name: String = "" var intro: String = "" + var color: String = "indigo" + var systemImage: String = "dumbbell.fill" + + // Returns the SwiftUI Color for the stored color name + func getColor() -> Color { + switch color { + case "red": return .red + case "orange": return .orange + case "yellow": return .yellow + case "green": return .green + case "mint": return .mint + case "teal": return .teal + case "cyan": return .cyan + case "blue": return .blue + case "indigo": return .indigo + case "purple": return .purple + case "pink": return .pink + case "brown": return .brown + default: return .indigo + } + } @Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split) var exercises: [SplitExerciseAssignment]? = [] @@ -13,9 +34,11 @@ final class Split { @Relationship(deleteRule: .nullify, inverse: \Workout.split) var workouts: [Workout]? = [] - init(name: String, intro: String) { + init(name: String, intro: String, color: String = "indigo", systemImage: String = "dumbbell.fill") { self.name = name self.intro = intro + self.color = color + self.systemImage = systemImage } static let unnamed = "Unnamed Split" @@ -53,6 +76,12 @@ fileprivate struct SplitFormView: View { @State private var itemToEdit: SplitExerciseAssignment? = nil @State private var itemToDelete: SplitExerciseAssignment? = nil + // Available colors for splits + private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"] + + // Available system images for splits + private let availableIcons = ["dumbbell.fill", "figure.strengthtraining.traditional", "figure.run", "figure.hiking", "figure.cooldown", "figure.boxing", "figure.wrestling", "figure.gymnastics", "figure.handball", "figure.core.training", "heart.fill", "bolt.fill"] + var body: some View { Section(header: Text("Name")) { TextField("Name", text: $model.name) @@ -64,6 +93,32 @@ fileprivate struct SplitFormView: View { .frame(minHeight: 100) } + Section(header: Text("Appearance")) { + Picker("Color", selection: $model.color) { + ForEach(availableColors, id: \.self) { colorName in + let tempSplit = Split(name: "", intro: "", color: colorName) + HStack { + Circle() + .fill(tempSplit.getColor()) + .frame(width: 20, height: 20) + Text(colorName.capitalized) + } + .tag(colorName) + } + } + + Picker("Icon", selection: $model.systemImage) { + ForEach(availableIcons, id: \.self) { iconName in + HStack { + Image(systemName: iconName) + .frame(width: 24, height: 24) + Text(iconName.replacingOccurrences(of: ".fill", with: "").replacingOccurrences(of: "figure.", with: "").capitalized) + } + .tag(iconName) + } + } + } + Section(header: Text("Exercises")) { NavigationLink { NavigationStack { @@ -109,9 +164,9 @@ fileprivate struct SplitFormView: View { ExercisePickerView { exercise in itemToEdit = SplitExerciseAssignment( order: 0, - sets: exercise.sets, - reps: exercise.reps, - weight: exercise.weight, + sets: 3, + reps: 10, + weight: 40, split: model, exercise: exercise ) diff --git a/Workouts/Models/WorkoutLog.swift b/Workouts/Models/WorkoutLog.swift index e09fa3a..ce052a6 100644 --- a/Workouts/Models/WorkoutLog.swift +++ b/Workouts/Models/WorkoutLog.swift @@ -7,6 +7,9 @@ final class WorkoutLog { var sets: Int = 0 var reps: Int = 0 var weight: Int = 0 + var status: WorkoutStatus? = WorkoutStatus.notStarted + var order: Int = 0 + var completed: Bool = false @Relationship(deleteRule: .nullify) @@ -15,13 +18,15 @@ final class WorkoutLog { @Relationship(deleteRule: .nullify) var exercise: Exercise? - init(workout: Workout, exercise: Exercise, date: Date, sets: Int, reps: Int, weight: Int, completed: Bool) { + init(workout: Workout, exercise: Exercise, date: Date, order: Int = 0, sets: Int, reps: Int, weight: Int, status: WorkoutStatus = .notStarted, completed: Bool = false) { self.date = date + self.order = order self.sets = sets self.reps = reps self.weight = weight - self.completed = completed + self.status = status self.workout = workout self.exercise = exercise + self.completed = completed } } diff --git a/Workouts/Models/WorkoutStatus.swift b/Workouts/Models/WorkoutStatus.swift new file mode 100644 index 0000000..f2c6943 --- /dev/null +++ b/Workouts/Models/WorkoutStatus.swift @@ -0,0 +1,22 @@ +// +// WorkoutStatus.swift +// Workouts +// +// Created by rzen on 7/16/25 at 7:03 PM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +enum WorkoutStatus: Int, Codable { + case notStarted = 1 + case inProgress = 2 + case completed = 3 + + var checkboxStatus: CheckboxStatus { + switch (self) { + case .notStarted: .unchecked + case .inProgress: .intermediate + case .completed: .checked + } + } +} diff --git a/Workouts/Schema/DataLoader.swift b/Workouts/Schema/DataLoader.swift index a9378e8..edf62aa 100644 --- a/Workouts/Schema/DataLoader.swift +++ b/Workouts/Schema/DataLoader.swift @@ -92,8 +92,7 @@ struct DataLoader { // 4. Load Exercises let exerciseData = try loadJSON(forResource: "exercises", type: [ExerciseData].self) for data in exerciseData { - let exercise = Exercise(name: data.name, setup: data.setup, descr: data.descr, - sets: data.sets, reps: data.reps, weight: data.weight) + let exercise = Exercise(name: data.name, descr: data.descr) // Set exercise type if let type = exerciseTypes[data.type] { diff --git a/Workouts/Schema/SchemaV2.swift b/Workouts/Schema/SchemaV2.swift new file mode 100644 index 0000000..18328be --- /dev/null +++ b/Workouts/Schema/SchemaV2.swift @@ -0,0 +1,16 @@ +import SwiftData + +enum SchemaV2: VersionedSchema { + static var versionIdentifier: Schema.Version = .init(1, 0, 1) + + static var models: [any PersistentModel.Type] = [ + Exercise.self, + ExerciseType.self, + Muscle.self, + MuscleGroup.self, + Split.self, + SplitExerciseAssignment.self, + Workout.self, + WorkoutLog.self + ] +} diff --git a/Workouts/Schema/SchemaV3.swift b/Workouts/Schema/SchemaV3.swift new file mode 100644 index 0000000..be47da2 --- /dev/null +++ b/Workouts/Schema/SchemaV3.swift @@ -0,0 +1,16 @@ +import SwiftData + +enum SchemaV3: VersionedSchema { + static var versionIdentifier: Schema.Version = .init(1, 0, 2) + + static var models: [any PersistentModel.Type] = [ + Exercise.self, + ExerciseType.self, + Muscle.self, + MuscleGroup.self, + Split.self, + SplitExerciseAssignment.self, + Workout.self, + WorkoutLog.self + ] +} diff --git a/Workouts/Schema/SchemaVersion.swift b/Workouts/Schema/SchemaVersion.swift index 20f97be..4561c69 100644 --- a/Workouts/Schema/SchemaVersion.swift +++ b/Workouts/Schema/SchemaVersion.swift @@ -2,6 +2,8 @@ import SwiftData enum SchemaVersion: Int { case v1 + case v2 + case v3 - static var current: SchemaVersion { .v1 } + static var current: SchemaVersion { .v3 } } diff --git a/Workouts/Schema/WorkoutsContainer.swift b/Workouts/Schema/WorkoutsContainer.swift index 813f572..266f60e 100644 --- a/Workouts/Schema/WorkoutsContainer.swift +++ b/Workouts/Schema/WorkoutsContainer.swift @@ -8,9 +8,10 @@ final class WorkoutsContainer { ) static func create() -> ModelContainer { - let schema = Schema(versionedSchema: SchemaV1.self) + // Using the current models directly without migration plan to avoid reference errors + let schema = Schema(SchemaV2.models) let configuration = ModelConfiguration(cloudKitDatabase: .automatic) - let container = try! ModelContainer(for: schema, migrationPlan: WorkoutsMigrationPlan.self, configurations: [configuration]) + let container = try! ModelContainer(for: schema, configurations: configuration) return container } @@ -19,7 +20,7 @@ final class WorkoutsContainer { let configuration = ModelConfiguration(isStoredInMemoryOnly: true) do { - let schema = Schema(SchemaV1.models) + let schema = Schema(SchemaV2.models) let container = try ModelContainer(for: schema, configurations: configuration) let context = ModelContext(container) diff --git a/Workouts/Schema/WorkoutsMigrationPlan.swift b/Workouts/Schema/WorkoutsMigrationPlan.swift index 9df3aae..030d51f 100644 --- a/Workouts/Schema/WorkoutsMigrationPlan.swift +++ b/Workouts/Schema/WorkoutsMigrationPlan.swift @@ -2,10 +2,28 @@ import SwiftData struct WorkoutsMigrationPlan: SchemaMigrationPlan { static var schemas: [VersionedSchema.Type] = [ - SchemaV1.self + SchemaV1.self, + SchemaV2.self ] static var stages: [MigrationStage] = [ - // Add migration stages here in the future + // Migration from V1 to V2: Add status field to WorkoutLog + MigrationStage.custom( + fromVersion: SchemaV1.self, + toVersion: SchemaV2.self, + willMigrate: { context in + // Get all WorkoutLog instances + let workoutLogs = try? context.fetch(FetchDescriptor()) + + // Update each WorkoutLog with appropriate status based on completed flag + workoutLogs?.forEach { workoutLog in + // If completed is true, set status to .completed, otherwise set to .notStarted + workoutLog.status = workoutLog.completed ? WorkoutStatus.completed : WorkoutStatus.notStarted + } + }, + didMigrate: { _ in + // No additional actions needed after migration + } + ) ] } diff --git a/Workouts/Utils/CheckboxListItem.swift b/Workouts/Utils/CheckboxListItem.swift new file mode 100644 index 0000000..863a60c --- /dev/null +++ b/Workouts/Utils/CheckboxListItem.swift @@ -0,0 +1,68 @@ +// +// ListItem.swift +// Workouts +// +// Created by rzen on 7/13/25 at 10:42 AM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI + +enum CheckboxStatus { + case checked + case unchecked + case intermediate + + var color: Color { + switch (self) { + case .checked: .green + case .unchecked: .gray + case .intermediate: .yellow + } + } + + var systemName: String { + switch (self) { + case .checked: "checkmark.circle.fill" + case .unchecked: "circle" + case .intermediate: "ellipsis.circle" + } + } +} + +struct CheckboxListItem: View { + var status: CheckboxStatus + var title: String + var subtitle: String? + var count: Int? + + var body: some View { + HStack (alignment: .top) { + Image(systemName: status.systemName) + .resizable() + .scaledToFit() + .frame(width: 30) + .foregroundStyle(status.color) + VStack (alignment: .leading) { + Text("\(title)") + .font(.headline) + HStack (alignment: .bottom) { + if let subtitle = subtitle { + Text("\(subtitle)") + .font(.footnote) + } + } + } + if let count = count { + Spacer() + Text("\(count)") + .font(.caption) + .foregroundColor(.gray) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } +} + diff --git a/Workouts/Views/Workouts/SplitPickerView.swift b/Workouts/Views/Workouts/SplitPickerView.swift index 314660e..e6b17fd 100644 --- a/Workouts/Views/Workouts/SplitPickerView.swift +++ b/Workouts/Views/Workouts/SplitPickerView.swift @@ -22,44 +22,38 @@ struct SplitPickerView: View { var body: some View { NavigationStack { - VStack { - Form { - Section (header: Text("This Split")) { - List { - ForEach(splits) { split in - Button(action: { - onSplitSelected(split) - dismiss() - }) { - HStack { - Text(split.name) - .font(.headline) - Spacer() - Text("\(split.exercises?.count ?? 0)") - .font(.caption) - } - .contentShape(Rectangle()) + ScrollView { + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) { + ForEach(splits) { split in + Button(action: { + onSplitSelected(split) + dismiss() + }) { + VStack { + ZStack { + RoundedRectangle(cornerRadius: 12) + .fill(split.getColor()) + .aspectRatio(1, contentMode: .fit) + .shadow(radius: 2) + + Image(systemName: split.systemImage) + .font(.system(size: 30)) + .foregroundColor(.white) } - .buttonStyle(.plain) + + Text(split.name) + .font(.headline) + .lineLimit(1) + + Text("\(split.exercises?.count ?? 0) exercises") + .font(.caption) + .foregroundColor(.secondary) } } + .buttonStyle(PlainButtonStyle()) } - -// Section (header: Text("Additional Exercises")) { -// List { -// ForEach(exercises) { exercise in -// Button(action: { -// onExerciseSelected(exercise) -// dismiss() -// }) { -// Text(exercise.name) -// } -// .contentShape(Rectangle()) -// .buttonStyle(.plain) -// } -// } -// } } + .padding() } .toolbar { ToolbarItem(placement: .navigationBarLeading) { diff --git a/Workouts/Views/Workouts/WorkoutLogView.swift b/Workouts/Views/Workouts/WorkoutLogView.swift index c386872..36d3c41 100644 --- a/Workouts/Views/Workouts/WorkoutLogView.swift +++ b/Workouts/Views/Workouts/WorkoutLogView.swift @@ -8,6 +8,7 @@ // import SwiftUI +import SwiftData struct WorkoutLogView: View { @Environment(\.modelContext) private var modelContext @@ -21,7 +22,7 @@ struct WorkoutLogView: View { var sortedWorkoutLogs: [WorkoutLog] { if let logs = workout.logs { logs.sorted(by: { - $0.completed == $1.completed ? $0.exercise!.name < $1.exercise!.name : !$0.completed + $0.order == $1.order ? $0.exercise!.name < $1.exercise!.name : $0.order < $1.order }) } else { [] @@ -33,33 +34,54 @@ struct WorkoutLogView: View { Section (header: Text("\(workout.label)")) { List { ForEach (sortedWorkoutLogs) { log in - let badges = log.completed ? [Badge(text: "Completed", color: .green)] : [] - ListItem( - title: log.exercise?.name ?? "Untitled Exercise", - subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs", - badges: badges + // Handle optional status, defaulting to a status based on completed flag if nil + let _ = print("DEBUG: workoutLog.status=\(log.status)") + + let workoutLogStatus = log.status?.checkboxStatus ?? (log.completed ? CheckboxStatus.checked : CheckboxStatus.unchecked) + + CheckboxListItem( + status: workoutLogStatus, + title: log.exercise?.name ?? Exercise.unnamed, + subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs" ) + .swipeActions(edge: .leading, allowsFullSwipe: false) { - if (log.completed) { + let status = log.status ?? WorkoutStatus.notStarted + + if [.inProgress,.completed].contains(status) { Button { withAnimation { - log.completed = false + log.status = .notStarted try? modelContext.save() } } label: { - Label("Complete", systemImage: "circle.fill") + Label("Not Started", systemImage: WorkoutStatus.notStarted.checkboxStatus.systemName) } - .tint(.green) - } else { + .tint(WorkoutStatus.notStarted.checkboxStatus.color) + } + + if [.notStarted,.completed].contains(status) { Button { withAnimation { - log.completed = true + log.status = .inProgress try? modelContext.save() } } label: { - Label("Reset", systemImage: "checkmark.circle.fill") + Label("In Progress", systemImage: WorkoutStatus.inProgress.checkboxStatus.systemName) } - .tint(.green) + .tint(WorkoutStatus.inProgress.checkboxStatus.color) + } + + if [.notStarted,.inProgress].contains(status) { + Button { + withAnimation { + log.status = .completed + try? modelContext.save() + } + } label: { + Label("Complete", systemImage: WorkoutStatus.completed.checkboxStatus.systemName) + } + .tint(WorkoutStatus.completed.checkboxStatus.color) } } .swipeActions(edge: .trailing, allowsFullSwipe: false) { @@ -89,13 +111,14 @@ struct WorkoutLogView: View { } .sheet(isPresented: $showingAddSheet) { ExercisePickerView { exercise in + let setsRepsWeight = getSetsRepsWeight(exercise, in: modelContext) let workoutLog = WorkoutLog( workout: workout, exercise: exercise, date: Date(), - sets: exercise.sets, - reps: exercise.reps, - weight: exercise.weight, + sets: setsRepsWeight.sets, + reps: setsRepsWeight.reps, + weight: setsRepsWeight.weight, completed: false ) workout.logs?.append(workoutLog) @@ -130,4 +153,34 @@ struct WorkoutLogView: View { } } + + func getSetsRepsWeight(_ exercise: Exercise, in modelContext: ModelContext) -> SetsRepsWeight { + // Use a single expression predicate that works with SwiftData + let exerciseID = exercise.persistentModelID + + print("Searching for exercise ID: \(exerciseID)") + + var descriptor = FetchDescriptor( + predicate: #Predicate { log in + log.exercise?.persistentModelID == exerciseID + }, + sortBy: [SortDescriptor(\WorkoutLog.date, order: .reverse)] + ) + + descriptor.fetchLimit = 1 + + let results = try? modelContext.fetch(descriptor) + + if let log = results?.first { + return SetsRepsWeight(sets: log.sets, reps: log.reps, weight: log.weight) + } else { + return SetsRepsWeight(sets: 3, reps: 10, weight: 40) + } + } +} + +struct SetsRepsWeight { + let sets: Int + let reps: Int + let weight: Int } diff --git a/Workouts/Views/Workouts/WorkoutsView.swift b/Workouts/Views/Workouts/WorkoutsView.swift index 4a4b418..5b0ecbe 100644 --- a/Workouts/Views/Workouts/WorkoutsView.swift +++ b/Workouts/Views/Workouts/WorkoutsView.swift @@ -100,10 +100,10 @@ struct WorkoutsView: View { workout: workout, exercise: exercise, date: Date(), + order: assignment.order, sets: assignment.sets, reps: assignment.reps, - weight: assignment.weight, - completed: false + weight: assignment.weight ) modelContext.insert(workoutLog) } else { diff --git a/Worksouts Watch App/Assets.xcassets/AccentColor.colorset/Contents.json b/Worksouts Watch App/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Worksouts Watch App/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Worksouts Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json b/Worksouts Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..49c81cd --- /dev/null +++ b/Worksouts Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Worksouts Watch App/Assets.xcassets/Contents.json b/Worksouts Watch App/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Worksouts Watch App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Worksouts Watch App/ContentView.swift b/Worksouts Watch App/ContentView.swift new file mode 100644 index 0000000..caf9e45 --- /dev/null +++ b/Worksouts Watch App/ContentView.swift @@ -0,0 +1,27 @@ +// +// ContentView.swift +// Workouts +// +// Created by rzen on 7/15/25 at 7:09 PM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + + +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/Worksouts Watch App/Preview Content/Preview Assets.xcassets/Contents.json b/Worksouts Watch App/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Worksouts Watch App/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Worksouts Watch App/Worksouts Watch App.entitlements b/Worksouts Watch App/Worksouts Watch App.entitlements new file mode 100644 index 0000000..bbd35f7 --- /dev/null +++ b/Worksouts Watch App/Worksouts Watch App.entitlements @@ -0,0 +1,16 @@ + + + + + aps-environment + development + com.apple.developer.icloud-container-identifiers + + iCloud.com.dev.rzen.indie.Workouts + + com.apple.developer.icloud-services + + CloudKit + + + diff --git a/Worksouts Watch App/WorksoutsApp.swift b/Worksouts Watch App/WorksoutsApp.swift new file mode 100644 index 0000000..30678bf --- /dev/null +++ b/Worksouts Watch App/WorksoutsApp.swift @@ -0,0 +1,20 @@ +// +// WorksoutsApp.swift +// Workouts +// +// Created by rzen on 7/15/25 at 7:09 PM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + + +import SwiftUI + +@main +struct Worksouts_Watch_AppApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +}