wip
This commit is contained in:
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
A45FA1FE2E27171B00581607 /* Worksouts Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = A45FA1F12E27171A00581607 /* Worksouts Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
A45FA1FE2E27171B00581607 /* Worksouts Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = A45FA1F12E27171A00581607 /* Worksouts Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
A45FA2732E29B12500581607 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = A45FA2722E29B12500581607 /* Yams */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -70,6 +71,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A45FA2732E29B12500581607 /* Yams in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -123,6 +125,7 @@
|
|||||||
);
|
);
|
||||||
name = Workouts;
|
name = Workouts;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
A45FA2722E29B12500581607 /* Yams */,
|
||||||
);
|
);
|
||||||
productName = Workouts;
|
productName = Workouts;
|
||||||
productReference = A45FA0912E21B3DD00581607 /* Workouts.app */;
|
productReference = A45FA0912E21B3DD00581607 /* Workouts.app */;
|
||||||
@ -177,6 +180,9 @@
|
|||||||
);
|
);
|
||||||
mainGroup = A45FA0882E21B3DC00581607;
|
mainGroup = A45FA0882E21B3DC00581607;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
packageReferences = (
|
||||||
|
A45FA26B2E297F5B00581607 /* XCRemoteSwiftPackageReference "Yams" */,
|
||||||
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = A45FA0922E21B3DD00581607 /* Products */;
|
productRefGroup = A45FA0922E21B3DD00581607 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@ -505,6 +511,25 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
A45FA26B2E297F5B00581607 /* XCRemoteSwiftPackageReference "Yams" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/jpsim/Yams";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 6.0.2;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
A45FA2722E29B12500581607 /* Yams */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = A45FA26B2E297F5B00581607 /* XCRemoteSwiftPackageReference "Yams" */;
|
||||||
|
productName = Yams;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = A45FA0892E21B3DC00581607 /* Project object */;
|
rootObject = A45FA0892E21B3DC00581607 /* Project object */;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "81702b8f7fa6fc73458821cd4dfa7dde6684a9d1d4b6d63ecde0b6b832d8d75e",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "yams",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/jpsim/Yams",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "f4d4d6827d36092d151ad7f6fef1991c1b7192f6",
|
||||||
|
"version" : "6.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
@ -17,9 +17,14 @@ struct ContentView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
TabView {
|
TabView {
|
||||||
|
SplitsView()
|
||||||
|
.tabItem {
|
||||||
|
Label("Workouts", systemImage: "figure.strengthtraining.traditional")
|
||||||
|
}
|
||||||
|
|
||||||
WorkoutsView()
|
WorkoutsView()
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Workout", systemImage: "figure.strengthtraining.traditional")
|
Label("Logs", systemImage: "list.bullet.clipboard.fill")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@Model
|
|
||||||
final class Exercise {
|
|
||||||
var name: String = ""
|
|
||||||
var descr: String = ""
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify, inverse: \ExerciseType.exercises)
|
|
||||||
var type: ExerciseType?
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify, inverse: \Muscle.exercises)
|
|
||||||
var muscles: [Muscle]? = []
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify, inverse: \SplitExerciseAssignment.exercise)
|
|
||||||
var splits: [SplitExerciseAssignment]? = []
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify, inverse: \WorkoutLog.exercise)
|
|
||||||
var logs: [WorkoutLog]? = []
|
|
||||||
|
|
||||||
init(name: String, descr: String) {
|
|
||||||
self.name = name
|
|
||||||
self.descr = descr
|
|
||||||
}
|
|
||||||
|
|
||||||
static let unnamed = "Unnamed Exercise"
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Exercise: EditableEntity {
|
|
||||||
static func createNew() -> Exercise {
|
|
||||||
return Exercise(name: "", descr: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
static var navigationTitle: String {
|
|
||||||
return "Exercises"
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
static func formView(for model: Exercise) -> some View {
|
|
||||||
EntityAddEditView(model: model) { $model in
|
|
||||||
// This internal view is necessary to use @Query within the form.
|
|
||||||
ExerciseFormView(model: $model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate struct ExerciseFormView: View {
|
|
||||||
@Binding var model: Exercise
|
|
||||||
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var exerciseTypes: [ExerciseType]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Section(header: Text("Name")) {
|
|
||||||
TextField("Name", text: $model.name)
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Exercise Type")) {
|
|
||||||
Picker("Type", selection: $model.type) {
|
|
||||||
Text("Select a type").tag(nil as ExerciseType?)
|
|
||||||
ForEach(exerciseTypes) { type in
|
|
||||||
Text(type.name).tag(type as ExerciseType?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Description")) {
|
|
||||||
TextEditor(text: $model.descr)
|
|
||||||
.frame(minHeight: 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
66
Workouts/Models/ExerciseList.swift
Normal file
66
Workouts/Models/ExerciseList.swift
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import Foundation
|
||||||
|
import Yams
|
||||||
|
|
||||||
|
struct ExerciseList: Codable {
|
||||||
|
let name: String
|
||||||
|
let source: String
|
||||||
|
let exercises: [ExerciseItem]
|
||||||
|
|
||||||
|
struct ExerciseItem: Codable, Identifiable {
|
||||||
|
let name: String
|
||||||
|
let descr: String
|
||||||
|
let type: String
|
||||||
|
|
||||||
|
var id: String { name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExerciseListLoader {
|
||||||
|
static func loadExerciseLists() -> [String: ExerciseList] {
|
||||||
|
var exerciseLists: [String: ExerciseList] = [:]
|
||||||
|
|
||||||
|
guard let resourcePath = Bundle.main.resourcePath else {
|
||||||
|
print("Could not find resource path")
|
||||||
|
return exerciseLists
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let resourceURL = URL(fileURLWithPath: resourcePath)
|
||||||
|
let yamlFiles = try fileManager.contentsOfDirectory(at: resourceURL, includingPropertiesForKeys: nil)
|
||||||
|
.filter { $0.pathExtension == "yaml" && $0.lastPathComponent.hasSuffix(".exercises.yaml") }
|
||||||
|
|
||||||
|
for yamlFile in yamlFiles {
|
||||||
|
let fileName = yamlFile.lastPathComponent
|
||||||
|
do {
|
||||||
|
let yamlString = try String(contentsOf: yamlFile, encoding: .utf8)
|
||||||
|
if let exerciseList = try Yams.load(yaml: yamlString) as? [String: Any],
|
||||||
|
let name = exerciseList["name"] as? String,
|
||||||
|
let source = exerciseList["source"] as? String,
|
||||||
|
let exercisesData = exerciseList["exercises"] as? [[String: Any]] {
|
||||||
|
|
||||||
|
var exercises: [ExerciseList.ExerciseItem] = []
|
||||||
|
|
||||||
|
for exerciseData in exercisesData {
|
||||||
|
if let name = exerciseData["name"] as? String,
|
||||||
|
let descr = exerciseData["descr"] as? String,
|
||||||
|
let type = exerciseData["type"] as? String {
|
||||||
|
let exercise = ExerciseList.ExerciseItem(name: name, descr: descr, type: type)
|
||||||
|
exercises.append(exercise)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let exerciseList = ExerciseList(name: name, source: source, exercises: exercises)
|
||||||
|
exerciseLists[fileName] = exerciseList
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error loading YAML file \(fileName): \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error listing directory contents: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return exerciseLists
|
||||||
|
}
|
||||||
|
}
|
@ -1,48 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@Model
|
|
||||||
final class ExerciseType {
|
|
||||||
var name: String = ""
|
|
||||||
var descr: String = ""
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify)
|
|
||||||
var exercises: [Exercise]? = []
|
|
||||||
|
|
||||||
init(name: String, descr: String) {
|
|
||||||
self.name = name
|
|
||||||
self.descr = descr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - EditableEntity Conformance
|
|
||||||
|
|
||||||
extension ExerciseType: EditableEntity {
|
|
||||||
var count: Int? {
|
|
||||||
return self.exercises?.count
|
|
||||||
}
|
|
||||||
|
|
||||||
static func createNew() -> ExerciseType {
|
|
||||||
return ExerciseType(name: "", descr: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
static var navigationTitle: String {
|
|
||||||
return "Exercise Types"
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
static func formView(for model: ExerciseType) -> some View {
|
|
||||||
EntityAddEditView(model: model) { $model in
|
|
||||||
Section(header: Text("Name")) {
|
|
||||||
TextField("Name", text: $model.name)
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Description")) {
|
|
||||||
TextEditor(text: $model.descr)
|
|
||||||
.frame(minHeight: 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@Model
|
|
||||||
final class Muscle {
|
|
||||||
var name: String = ""
|
|
||||||
|
|
||||||
var descr: String = ""
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify, inverse: \MuscleGroup.muscles)
|
|
||||||
var muscleGroup: MuscleGroup?
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify)
|
|
||||||
var exercises: [Exercise]? = []
|
|
||||||
|
|
||||||
init(name: String, descr: String, muscleGroup: MuscleGroup? = nil) {
|
|
||||||
self.name = name
|
|
||||||
self.descr = descr
|
|
||||||
self.muscleGroup = muscleGroup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - EditableEntity Conformance
|
|
||||||
|
|
||||||
extension Muscle: EditableEntity {
|
|
||||||
var count: Int? {
|
|
||||||
return self.exercises?.count
|
|
||||||
}
|
|
||||||
|
|
||||||
static func createNew() -> Muscle {
|
|
||||||
return Muscle(name: "", descr: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
static var navigationTitle: String {
|
|
||||||
return "Muscles"
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
static func formView(for model: Muscle) -> some View {
|
|
||||||
EntityAddEditView(model: model) { $model in
|
|
||||||
MuscleFormView(model: $model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private Form View
|
|
||||||
|
|
||||||
fileprivate struct MuscleFormView: View {
|
|
||||||
@Binding var model: Muscle
|
|
||||||
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var muscleGroups: [MuscleGroup]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Section(header: Text("Name")) {
|
|
||||||
TextField("Name", text: $model.name)
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Muscle Group")) {
|
|
||||||
Picker("Muscle Group", selection: $model.muscleGroup) {
|
|
||||||
Text("Select a muscle group").tag(nil as MuscleGroup?)
|
|
||||||
ForEach(muscleGroups) { group in
|
|
||||||
Text(group.name).tag(group as MuscleGroup?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Description")) {
|
|
||||||
TextEditor(text: $model.descr)
|
|
||||||
.frame(minHeight: 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@Model
|
|
||||||
final class MuscleGroup {
|
|
||||||
var name: String = ""
|
|
||||||
var descr: String = ""
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify)
|
|
||||||
var muscles: [Muscle]? = []
|
|
||||||
|
|
||||||
init(name: String, descr: String) {
|
|
||||||
self.name = name
|
|
||||||
self.descr = descr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - EditableEntity Conformance
|
|
||||||
|
|
||||||
extension MuscleGroup: EditableEntity {
|
|
||||||
static func createNew() -> MuscleGroup {
|
|
||||||
return MuscleGroup(name: "", descr: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
static var navigationTitle: String {
|
|
||||||
return "Muscle Groups"
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
static func formView(for model: MuscleGroup) -> some View {
|
|
||||||
EntityAddEditView(model: model) { $model in
|
|
||||||
Section(header: Text("Name")) {
|
|
||||||
TextField("Name", text: $model.name)
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Description")) {
|
|
||||||
TextEditor(text: $model.descr)
|
|
||||||
.frame(minHeight: 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var count: Int? {
|
|
||||||
return muscles?.count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,27 +5,13 @@ import SwiftUI
|
|||||||
@Model
|
@Model
|
||||||
final class Split {
|
final class Split {
|
||||||
var name: String = ""
|
var name: String = ""
|
||||||
var intro: String = ""
|
|
||||||
var color: String = "indigo"
|
var color: String = "indigo"
|
||||||
var systemImage: String = "dumbbell.fill"
|
var systemImage: String = "dumbbell.fill"
|
||||||
|
var order: Int = 0
|
||||||
|
|
||||||
// Returns the SwiftUI Color for the stored color name
|
// Returns the SwiftUI Color for the stored color name
|
||||||
func getColor() -> Color {
|
func getColor () -> Color {
|
||||||
switch color {
|
return Color.color(from: self.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)
|
@Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split)
|
||||||
@ -34,11 +20,11 @@ final class Split {
|
|||||||
@Relationship(deleteRule: .nullify, inverse: \Workout.split)
|
@Relationship(deleteRule: .nullify, inverse: \Workout.split)
|
||||||
var workouts: [Workout]? = []
|
var workouts: [Workout]? = []
|
||||||
|
|
||||||
init(name: String, intro: String, color: String = "indigo", systemImage: String = "dumbbell.fill") {
|
init(name: String, color: String = "indigo", systemImage: String = "dumbbell.fill", order: Int = 0) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.intro = intro
|
|
||||||
self.color = color
|
self.color = color
|
||||||
self.systemImage = systemImage
|
self.systemImage = systemImage
|
||||||
|
self.order = order
|
||||||
}
|
}
|
||||||
|
|
||||||
static let unnamed = "Unnamed Split"
|
static let unnamed = "Unnamed Split"
|
||||||
@ -52,7 +38,7 @@ extension Split: EditableEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func createNew() -> Split {
|
static func createNew() -> Split {
|
||||||
return Split(name: "", intro: "")
|
return Split(name: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var navigationTitle: String {
|
static var navigationTitle: String {
|
||||||
@ -72,10 +58,6 @@ 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
|
|
||||||
|
|
||||||
// Available colors for splits
|
// Available colors for splits
|
||||||
private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
|
private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
|
||||||
|
|
||||||
@ -88,15 +70,10 @@ fileprivate struct SplitFormView: View {
|
|||||||
.bold()
|
.bold()
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Description")) {
|
|
||||||
TextEditor(text: $model.intro)
|
|
||||||
.frame(minHeight: 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Appearance")) {
|
Section(header: Text("Appearance")) {
|
||||||
Picker("Color", selection: $model.color) {
|
Picker("Color", selection: $model.color) {
|
||||||
ForEach(availableColors, id: \.self) { colorName in
|
ForEach(availableColors, id: \.self) { colorName in
|
||||||
let tempSplit = Split(name: "", intro: "", color: colorName)
|
let tempSplit = Split(name: "", color: colorName)
|
||||||
HStack {
|
HStack {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(tempSplit.getColor())
|
.fill(tempSplit.getColor())
|
||||||
@ -121,75 +98,7 @@ fileprivate struct SplitFormView: View {
|
|||||||
|
|
||||||
Section(header: Text("Exercises")) {
|
Section(header: Text("Exercises")) {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
NavigationStack {
|
SplitExercisesListView(model: model)
|
||||||
Form {
|
|
||||||
List {
|
|
||||||
if let assignments = model.exercises, !assignments.isEmpty {
|
|
||||||
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(
|
|
||||||
title: item.exercise?.name ?? Exercise.unnamed,
|
|
||||||
text: item.setup.isEmpty ? nil : item.setup,
|
|
||||||
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs"
|
|
||||||
)
|
|
||||||
.swipeActions {
|
|
||||||
Button(role: .destructive) {
|
|
||||||
itemToDelete = item
|
|
||||||
} label: {
|
|
||||||
Label("Delete", systemImage: "trash")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
itemToEdit = item
|
|
||||||
} label: {
|
|
||||||
Label("Edit", systemImage: "pencil")
|
|
||||||
}
|
|
||||||
.tint(.indigo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("No exercises added yet.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("Exercises")
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button(action: { showingAddSheet.toggle() }) {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet (isPresented: $showingAddSheet) {
|
|
||||||
ExercisePickerView { exercise in
|
|
||||||
itemToEdit = SplitExerciseAssignment(
|
|
||||||
order: 0,
|
|
||||||
sets: 3,
|
|
||||||
reps: 10,
|
|
||||||
weight: 40,
|
|
||||||
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) {
|
|
||||||
if let item = itemToDelete {
|
|
||||||
withAnimation {
|
|
||||||
model.exercises?.removeAll { $0.id == item.id }
|
|
||||||
itemToDelete = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} label: {
|
} label: {
|
||||||
ListItem(
|
ListItem(
|
||||||
text: "Exercises",
|
text: "Exercises",
|
||||||
|
@ -3,25 +3,21 @@ import SwiftData
|
|||||||
|
|
||||||
@Model
|
@Model
|
||||||
final class SplitExerciseAssignment {
|
final class SplitExerciseAssignment {
|
||||||
|
var exerciseName: String = ""
|
||||||
var order: Int = 0
|
var order: Int = 0
|
||||||
var sets: Int = 0
|
var sets: Int = 0
|
||||||
var reps: Int = 0
|
var reps: Int = 0
|
||||||
var weight: Int = 0
|
var weight: Int = 0
|
||||||
var setup: String = ""
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify)
|
@Relationship(deleteRule: .nullify)
|
||||||
var split: Split?
|
var split: Split?
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify)
|
init(split: Split, exerciseName: String, order: Int, sets: Int, reps: Int, weight: Int) {
|
||||||
var exercise: Exercise?
|
self.split = split
|
||||||
|
self.exerciseName = exerciseName
|
||||||
init(order: Int, sets: Int, reps: Int, weight: Int, setup: String = "", split: Split, exercise: Exercise) {
|
|
||||||
self.order = order
|
self.order = order
|
||||||
self.sets = sets
|
self.sets = sets
|
||||||
self.reps = reps
|
self.reps = reps
|
||||||
self.weight = weight
|
self.weight = weight
|
||||||
self.setup = setup
|
|
||||||
self.split = split
|
|
||||||
self.exercise = exercise
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,16 +9,14 @@ final class WorkoutLog {
|
|||||||
var weight: Int = 0
|
var weight: Int = 0
|
||||||
var status: WorkoutStatus? = WorkoutStatus.notStarted
|
var status: WorkoutStatus? = WorkoutStatus.notStarted
|
||||||
var order: Int = 0
|
var order: Int = 0
|
||||||
|
var exerciseName: String = ""
|
||||||
|
|
||||||
var completed: Bool = false
|
var completed: Bool = false
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify)
|
@Relationship(deleteRule: .nullify)
|
||||||
var workout: Workout?
|
var workout: Workout?
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify)
|
init(workout: Workout, exerciseName: String, date: Date, order: Int = 0, sets: Int, reps: Int, weight: Int, status: WorkoutStatus = .notStarted, completed: Bool = false) {
|
||||||
var exercise: Exercise?
|
|
||||||
|
|
||||||
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.date = date
|
||||||
self.order = order
|
self.order = order
|
||||||
self.sets = sets
|
self.sets = sets
|
||||||
@ -26,7 +24,7 @@ final class WorkoutLog {
|
|||||||
self.weight = weight
|
self.weight = weight
|
||||||
self.status = status
|
self.status = status
|
||||||
self.workout = workout
|
self.workout = workout
|
||||||
self.exercise = exercise
|
self.exerciseName = exerciseName
|
||||||
self.completed = completed
|
self.completed = completed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
name: Beginner
|
||||||
|
source: Planet Fitness
|
||||||
|
exercises:
|
||||||
- name: Lat Pull Down
|
- name: Lat Pull Down
|
||||||
setup: 'Seat: 3, Thigh Pad: 4'
|
setup: 'Seat: 3, Thigh Pad: 4'
|
||||||
descr: Sit upright with your knees secured under the pad. Grip the bar wider than
|
descr: Sit upright with your knees secured under the pad. Grip the bar wider than
|
135
Workouts/Resources/_attic_/muscles.yaml
Normal file
135
Workouts/Resources/_attic_/muscles.yaml
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
- name: Pectoralis Major
|
||||||
|
muscleGroup: Chest
|
||||||
|
descr: Large chest muscle located on the front of the upper torso, responsible for arm flexion and pushing.
|
||||||
|
- name: Pectoralis Minor
|
||||||
|
muscleGroup: Chest
|
||||||
|
descr: Thin, triangular muscle beneath the pectoralis major, stabilizes the scapula.
|
||||||
|
- name: Latissimus Dorsi
|
||||||
|
muscleGroup: Back
|
||||||
|
descr: Broad muscle on the mid to lower back, responsible for pulling and shoulder extension.
|
||||||
|
- name: Trapezius
|
||||||
|
muscleGroup: Back
|
||||||
|
descr: Large muscle covering the upper back and neck, involved in shoulder movement and posture.
|
||||||
|
- name: Rhomboids
|
||||||
|
muscleGroup: Back
|
||||||
|
descr: Muscles between the shoulder blades, responsible for scapular retraction.
|
||||||
|
- name: Erector Spinae
|
||||||
|
muscleGroup: Back
|
||||||
|
descr: Long vertical muscles along the spine that maintain posture and extend the back.
|
||||||
|
- name: Deltoid (Anterior)
|
||||||
|
muscleGroup: Shoulders
|
||||||
|
descr: Front portion of the shoulder muscle, raises the arm forward.
|
||||||
|
- name: Deltoid (Lateral)
|
||||||
|
muscleGroup: Shoulders
|
||||||
|
descr: Middle portion of the shoulder muscle, raises the arm to the side.
|
||||||
|
- name: Deltoid (Posterior)
|
||||||
|
muscleGroup: Shoulders
|
||||||
|
descr: Rear portion of the shoulder muscle, moves the arm backward.
|
||||||
|
- name: Rotator Cuff Muscles
|
||||||
|
muscleGroup: Shoulders
|
||||||
|
descr: Group of four muscles surrounding the shoulder joint, stabilizing and rotating the arm.
|
||||||
|
- name: Biceps Brachii
|
||||||
|
muscleGroup: Arms
|
||||||
|
descr: Front upper arm muscle, responsible for elbow flexion and forearm rotation.
|
||||||
|
- name: Triceps Brachii
|
||||||
|
muscleGroup: Arms
|
||||||
|
descr: Back upper arm muscle, responsible for elbow extension.
|
||||||
|
- name: Brachialis
|
||||||
|
muscleGroup: Arms
|
||||||
|
descr: Muscle beneath the biceps, assists in elbow flexion.
|
||||||
|
- name: Brachioradialis
|
||||||
|
muscleGroup: Arms
|
||||||
|
descr: Forearm muscle on the thumb side, aids in elbow flexion.
|
||||||
|
- name: Rectus Abdominis
|
||||||
|
muscleGroup: Abdominals
|
||||||
|
descr: Vertical muscle on the front of the abdomen, responsible for trunk flexion (the 'six-pack').
|
||||||
|
- name: Transverse Abdominis
|
||||||
|
muscleGroup: Abdominals
|
||||||
|
descr: Deepest abdominal muscle, wraps around the torso to stabilize the core.
|
||||||
|
- name: Internal Obliques
|
||||||
|
muscleGroup: Abdominals
|
||||||
|
descr: Muscles located beneath the external obliques, involved in trunk rotation and lateral flexion.
|
||||||
|
- name: External Obliques
|
||||||
|
muscleGroup: Abdominals
|
||||||
|
descr: Muscles on the sides of the abdomen, responsible for trunk twisting and side bending.
|
||||||
|
- name: Gluteus Maximus
|
||||||
|
muscleGroup: Glutes
|
||||||
|
descr: Largest glute muscle located in the buttocks, responsible for hip extension and rotation.
|
||||||
|
- name: Gluteus Medius
|
||||||
|
muscleGroup: Glutes
|
||||||
|
descr: Muscle on the outer surface of the pelvis, important for hip abduction and stability.
|
||||||
|
- name: Gluteus Minimus
|
||||||
|
muscleGroup: Glutes
|
||||||
|
descr: Smallest glute muscle, located beneath the medius, assists in hip abduction.
|
||||||
|
- name: Rectus Femoris
|
||||||
|
muscleGroup: Quadriceps
|
||||||
|
descr: Front thigh muscle, part of the quadriceps group, helps extend the knee and flex the hip.
|
||||||
|
- name: Vastus Lateralis
|
||||||
|
muscleGroup: Quadriceps
|
||||||
|
descr: Outer thigh muscle, part of the quadriceps, involved in knee extension.
|
||||||
|
- name: Vastus Medialis
|
||||||
|
muscleGroup: Quadriceps
|
||||||
|
descr: Inner thigh muscle, part of the quadriceps, assists in knee extension and stabilization.
|
||||||
|
- name: Vastus Intermedius
|
||||||
|
muscleGroup: Quadriceps
|
||||||
|
descr: Deep thigh muscle beneath rectus femoris, assists in knee extension.
|
||||||
|
- name: Biceps Femoris
|
||||||
|
muscleGroup: Hamstrings
|
||||||
|
descr: Muscle on the back of the thigh, responsible for knee flexion and hip extension.
|
||||||
|
- name: Semitendinosus
|
||||||
|
muscleGroup: Hamstrings
|
||||||
|
descr: Medial hamstring muscle, assists in knee flexion and internal rotation.
|
||||||
|
- name: Semimembranosus
|
||||||
|
muscleGroup: Hamstrings
|
||||||
|
descr: Deep medial hamstring muscle, also assists in knee flexion and hip extension.
|
||||||
|
- name: Gastrocnemius
|
||||||
|
muscleGroup: Calves
|
||||||
|
descr: Large calf muscle visible on the back of the lower leg, responsible for plantar flexion of the foot.
|
||||||
|
- name: Soleus
|
||||||
|
muscleGroup: Calves
|
||||||
|
descr: Flat muscle beneath the gastrocnemius, aids in plantar flexion when the knee is bent.
|
||||||
|
- name: Flexor Carpi Radialis
|
||||||
|
muscleGroup: Forearms
|
||||||
|
descr: Muscle on the front of the forearm, flexes and abducts the wrist.
|
||||||
|
- name: Flexor Carpi Ulnaris
|
||||||
|
muscleGroup: Forearms
|
||||||
|
descr: Forearm muscle that flexes and adducts the wrist.
|
||||||
|
- name: Extensor Carpi Radialis
|
||||||
|
muscleGroup: Forearms
|
||||||
|
descr: Posterior forearm muscle that extends and abducts the wrist.
|
||||||
|
- name: Pronator Teres
|
||||||
|
muscleGroup: Forearms
|
||||||
|
descr: Muscle running across the forearm that pronates the forearm (palm down).
|
||||||
|
- name: Sternocleidomastoid
|
||||||
|
muscleGroup: Neck
|
||||||
|
descr: Prominent neck muscle responsible for rotating and flexing the head.
|
||||||
|
- name: Splenius Capitis
|
||||||
|
muscleGroup: Neck
|
||||||
|
descr: Back of neck muscle that extends and rotates the head.
|
||||||
|
- name: Scalenes
|
||||||
|
muscleGroup: Neck
|
||||||
|
descr: Lateral neck muscles that assist in neck flexion and elevate the ribs during breathing.
|
||||||
|
- name: Iliopsoas
|
||||||
|
muscleGroup: Hip Flexors
|
||||||
|
descr: Deep muscle group connecting the lower spine to the femur, main hip flexor.
|
||||||
|
- name: Rectus Femoris
|
||||||
|
muscleGroup: Hip Flexors
|
||||||
|
descr: Also part of the quadriceps, helps flex the hip and extend the knee.
|
||||||
|
- name: Sartorius
|
||||||
|
muscleGroup: Hip Flexors
|
||||||
|
descr: Longest muscle in the body, crosses the thigh to flex and rotate the hip and knee.
|
||||||
|
- name: Adductor Longus
|
||||||
|
muscleGroup: Adductors
|
||||||
|
descr: Medial thigh muscle that adducts the leg and assists with hip flexion.
|
||||||
|
- name: Adductor Brevis
|
||||||
|
muscleGroup: Adductors
|
||||||
|
descr: Short adductor muscle that helps pull the thigh inward.
|
||||||
|
- name: Adductor Magnus
|
||||||
|
muscleGroup: Adductors
|
||||||
|
descr: Large, deep inner thigh muscle that performs hip adduction and extension.
|
||||||
|
- name: Gracilis
|
||||||
|
muscleGroup: Adductors
|
||||||
|
descr: Thin inner thigh muscle that assists in adduction and knee flexion.
|
||||||
|
- name: Tensor Fasciae Latae
|
||||||
|
muscleGroup: Abductors
|
||||||
|
descr: Lateral hip muscle that abducts and medially rotates the thigh.
|
69
Workouts/Resources/pf-starter.exercises.yaml
Normal file
69
Workouts/Resources/pf-starter.exercises.yaml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
name: Starter Set
|
||||||
|
source: Planet Fitness
|
||||||
|
exercises:
|
||||||
|
- name: Lat Pull Down
|
||||||
|
descr: Sit upright with your knees secured under the pad. Grip the bar wider than
|
||||||
|
shoulder-width. Pull the bar down to your chest, squeezing your shoulder blades
|
||||||
|
together. Avoid leaning back excessively or using momentum.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Seated Row
|
||||||
|
descr: With your chest firmly against the pad, grip the handles and pull straight
|
||||||
|
back while keeping your elbows close to your body. Focus on retracting your shoulder
|
||||||
|
blades and avoid rounding your back.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Shoulder Press
|
||||||
|
descr: Sit with your back against the pad, grip the handles just outside shoulder-width.
|
||||||
|
Press upward without locking out your elbows. Keep your neck relaxed and avoid
|
||||||
|
shrugging your shoulders.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Chest Press
|
||||||
|
descr: Adjust the seat so the handles are at mid-chest height. Push forward until arms
|
||||||
|
are nearly extended, then return slowly. Keep wrists straight and dont let your elbows
|
||||||
|
drop too low.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Tricep Press
|
||||||
|
descr: With elbows close to your sides, press the handles downward in a controlled
|
||||||
|
motion. Avoid flaring your elbows or using your shoulders to assist the motion.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Arm Curl
|
||||||
|
descr: Position your arms over the pad and grip the handles. Curl the weight upward
|
||||||
|
while keeping your upper arms stationary. Avoid using momentum and fully control
|
||||||
|
the lowering phase.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Abdominal
|
||||||
|
descr: Sit with the pads resting against your chest. Contract your abs to curl forward,
|
||||||
|
keeping your lower back in contact with the pad. Avoid pulling with your arms
|
||||||
|
or hips.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Rotary
|
||||||
|
descr: Rotate your torso from side to side in a controlled motion, keeping your
|
||||||
|
hips still. Focus on using your obliques to generate the twist, not momentum or
|
||||||
|
the arms.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Leg Press
|
||||||
|
descr: Place your feet shoulder-width on the platform. Press upward through your
|
||||||
|
heels without locking your knees. Keep your back flat against the pad throughout
|
||||||
|
the motion.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Leg Extension
|
||||||
|
descr: Sit upright and align your knees with the pivot point. Extend your legs to
|
||||||
|
a straightened position, then lower with control. Avoid jerky movements or lifting
|
||||||
|
your hips off the seat.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Leg Curl
|
||||||
|
descr: Lie face down or sit depending on the version. Curl your legs toward your
|
||||||
|
glutes, focusing on hamstring engagement. Avoid arching your back or using momentum.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Adductor
|
||||||
|
descr: Sit with legs placed outside the pads. Bring your legs together using inner
|
||||||
|
thigh muscles. Control the motion both in and out, avoiding fast swings.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Abductor
|
||||||
|
descr: Sit with legs inside the pads and push outward to engage outer thighs and
|
||||||
|
glutes. Avoid leaning forward and keep the motion controlled throughout.
|
||||||
|
type: Machine-Based
|
||||||
|
- name: Calfs
|
||||||
|
descr: Place the balls of your feet on the platform with heels hanging off. Raise
|
||||||
|
your heels by contracting your calves, then slowly lower them below the platform
|
||||||
|
level for a full stretch.
|
||||||
|
type: Machine-Based
|
@ -1,167 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct DataLoader {
|
|
||||||
static let logger = AppLogger(
|
|
||||||
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
|
|
||||||
category: "InitialData"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Data structures for JSON decoding
|
|
||||||
private struct ExerciseTypeData: Codable {
|
|
||||||
let name: String
|
|
||||||
let descr: String
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct MuscleGroupData: Codable {
|
|
||||||
let name: String
|
|
||||||
let descr: String
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct MuscleData: Codable {
|
|
||||||
let name: String
|
|
||||||
let descr: String
|
|
||||||
let muscleGroup: String
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ExerciseData: Codable {
|
|
||||||
let name: String
|
|
||||||
let setup: String
|
|
||||||
let descr: String
|
|
||||||
let sets: Int
|
|
||||||
let reps: Int
|
|
||||||
let weight: Int
|
|
||||||
let type: String
|
|
||||||
let muscles: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct SplitExerciseAssignmentData: Codable {
|
|
||||||
let exercise: String
|
|
||||||
let weight: Int
|
|
||||||
let sets: Int
|
|
||||||
let reps: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct SplitData: Codable {
|
|
||||||
let name: String
|
|
||||||
let intro: String
|
|
||||||
let splitExerciseAssignments: [SplitExerciseAssignmentData]
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
static func create(modelContext: ModelContext) {
|
|
||||||
logger.info("Creating initial data from JSON files")
|
|
||||||
|
|
||||||
// Load and insert data
|
|
||||||
do {
|
|
||||||
// Dictionaries to store references
|
|
||||||
var exerciseTypes: [String: ExerciseType] = [:]
|
|
||||||
var muscleGroups: [String: MuscleGroup] = [:]
|
|
||||||
var muscles: [String: Muscle] = [:]
|
|
||||||
var exercises: [String: Exercise] = [:]
|
|
||||||
|
|
||||||
// 1. Load Exercise Types
|
|
||||||
let exerciseTypeData = try loadJSON(forResource: "exercise-types", type: [ExerciseTypeData].self)
|
|
||||||
for typeData in exerciseTypeData {
|
|
||||||
let exerciseType = ExerciseType(name: typeData.name, descr: typeData.descr)
|
|
||||||
exerciseTypes[typeData.name] = exerciseType
|
|
||||||
modelContext.insert(exerciseType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Load Muscle Groups
|
|
||||||
let muscleGroupData = try loadJSON(forResource: "muscle-groups", type: [MuscleGroupData].self)
|
|
||||||
for groupData in muscleGroupData {
|
|
||||||
let muscleGroup = MuscleGroup(name: groupData.name, descr: groupData.descr)
|
|
||||||
muscleGroups[groupData.name] = muscleGroup
|
|
||||||
modelContext.insert(muscleGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Load Muscles
|
|
||||||
let muscleData = try loadJSON(forResource: "muscles", type: [MuscleData].self)
|
|
||||||
for data in muscleData {
|
|
||||||
// Find the muscle group for this muscle
|
|
||||||
if let muscleGroup = muscleGroups[data.muscleGroup] {
|
|
||||||
let muscle = Muscle(name: data.name, descr: data.descr, muscleGroup: muscleGroup)
|
|
||||||
muscles[data.name] = muscle
|
|
||||||
modelContext.insert(muscle)
|
|
||||||
} else {
|
|
||||||
logger.warning("Muscle group not found for muscle: \(data.name)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Load Exercises
|
|
||||||
let exerciseData = try loadJSON(forResource: "exercises", type: [ExerciseData].self)
|
|
||||||
for data in exerciseData {
|
|
||||||
let exercise = Exercise(name: data.name, descr: data.descr)
|
|
||||||
|
|
||||||
// Set exercise type
|
|
||||||
if let type = exerciseTypes[data.type] {
|
|
||||||
exercise.type = type
|
|
||||||
} else {
|
|
||||||
logger.warning("Exercise type not found: \(data.type) for exercise: \(data.name)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set muscles
|
|
||||||
var exerciseMuscles: [Muscle] = []
|
|
||||||
for muscleName in data.muscles {
|
|
||||||
if let muscle = muscles[muscleName] {
|
|
||||||
exerciseMuscles.append(muscle)
|
|
||||||
} else {
|
|
||||||
logger.warning("Muscle not found: \(muscleName) for exercise: \(data.name)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exercise.muscles = exerciseMuscles
|
|
||||||
|
|
||||||
exercises[data.name] = exercise
|
|
||||||
modelContext.insert(exercise)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Load Splits and Exercise Assignments
|
|
||||||
let splitData = try loadJSON(forResource: "splits", type: [SplitData].self)
|
|
||||||
for data in splitData {
|
|
||||||
let split = Split(name: data.name, intro: data.intro)
|
|
||||||
modelContext.insert(split)
|
|
||||||
|
|
||||||
// Create exercise assignments for this split
|
|
||||||
for (index, assignment) in data.splitExerciseAssignments.enumerated() {
|
|
||||||
if let exercise = exercises[assignment.exercise] {
|
|
||||||
let splitAssignment = SplitExerciseAssignment(
|
|
||||||
order: index + 1, // 1-based ordering
|
|
||||||
sets: assignment.sets,
|
|
||||||
reps: assignment.reps,
|
|
||||||
weight: assignment.weight,
|
|
||||||
split: split,
|
|
||||||
exercise: exercise
|
|
||||||
)
|
|
||||||
modelContext.insert(splitAssignment)
|
|
||||||
} else {
|
|
||||||
logger.warning("Exercise not found: \(assignment.exercise) for split: \(data.name)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save all the inserted data
|
|
||||||
try modelContext.save()
|
|
||||||
logger.info("Initial data loaded successfully from JSON files")
|
|
||||||
} catch {
|
|
||||||
logger.error("Failed to load initial data from JSON files: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method to load and decode JSON from a file
|
|
||||||
private static func loadJSON<T: Decodable>(forResource name: String, type: T.Type) throws -> T {
|
|
||||||
guard let url = Bundle.main.url(forResource: name, withExtension: "json") else {
|
|
||||||
logger.error("Could not find JSON file: \(name).json")
|
|
||||||
throw NSError(domain: "InitialData", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not find JSON file: \(name).json"])
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let data = try Data(contentsOf: url)
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
return try decoder.decode(T.self, from: data)
|
|
||||||
} catch {
|
|
||||||
logger.error("Failed to decode JSON file \(name).json: \(error.localizedDescription)")
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,10 +4,6 @@ enum SchemaV1: VersionedSchema {
|
|||||||
static var versionIdentifier: Schema.Version = .init(1, 0, 0)
|
static var versionIdentifier: Schema.Version = .init(1, 0, 0)
|
||||||
|
|
||||||
static var models: [any PersistentModel.Type] = [
|
static var models: [any PersistentModel.Type] = [
|
||||||
Exercise.self,
|
|
||||||
ExerciseType.self,
|
|
||||||
Muscle.self,
|
|
||||||
MuscleGroup.self,
|
|
||||||
Split.self,
|
Split.self,
|
||||||
SplitExerciseAssignment.self,
|
SplitExerciseAssignment.self,
|
||||||
Workout.self,
|
Workout.self,
|
||||||
|
@ -4,10 +4,6 @@ enum SchemaV2: VersionedSchema {
|
|||||||
static var versionIdentifier: Schema.Version = .init(1, 0, 1)
|
static var versionIdentifier: Schema.Version = .init(1, 0, 1)
|
||||||
|
|
||||||
static var models: [any PersistentModel.Type] = [
|
static var models: [any PersistentModel.Type] = [
|
||||||
Exercise.self,
|
|
||||||
ExerciseType.self,
|
|
||||||
Muscle.self,
|
|
||||||
MuscleGroup.self,
|
|
||||||
Split.self,
|
Split.self,
|
||||||
SplitExerciseAssignment.self,
|
SplitExerciseAssignment.self,
|
||||||
Workout.self,
|
Workout.self,
|
||||||
|
@ -4,10 +4,6 @@ enum SchemaV3: VersionedSchema {
|
|||||||
static var versionIdentifier: Schema.Version = .init(1, 0, 2)
|
static var versionIdentifier: Schema.Version = .init(1, 0, 2)
|
||||||
|
|
||||||
static var models: [any PersistentModel.Type] = [
|
static var models: [any PersistentModel.Type] = [
|
||||||
Exercise.self,
|
|
||||||
ExerciseType.self,
|
|
||||||
Muscle.self,
|
|
||||||
MuscleGroup.self,
|
|
||||||
Split.self,
|
Split.self,
|
||||||
SplitExerciseAssignment.self,
|
SplitExerciseAssignment.self,
|
||||||
Workout.self,
|
Workout.self,
|
||||||
|
@ -9,7 +9,7 @@ final class WorkoutsContainer {
|
|||||||
|
|
||||||
static func create() -> ModelContainer {
|
static func create() -> ModelContainer {
|
||||||
// Using the current models directly without migration plan to avoid reference errors
|
// Using the current models directly without migration plan to avoid reference errors
|
||||||
let schema = Schema(SchemaV2.models)
|
let schema = Schema(SchemaV1.models)
|
||||||
let configuration = ModelConfiguration(cloudKitDatabase: .automatic)
|
let configuration = ModelConfiguration(cloudKitDatabase: .automatic)
|
||||||
let container = try! ModelContainer(for: schema, configurations: configuration)
|
let container = try! ModelContainer(for: schema, configurations: configuration)
|
||||||
return container
|
return container
|
||||||
@ -20,13 +20,10 @@ final class WorkoutsContainer {
|
|||||||
let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
|
let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let schema = Schema(SchemaV2.models)
|
let schema = Schema(SchemaV1.models)
|
||||||
let container = try ModelContainer(for: schema, configurations: configuration)
|
let container = try ModelContainer(for: schema, configurations: configuration)
|
||||||
let context = ModelContext(container)
|
let context = ModelContext(container)
|
||||||
|
|
||||||
// Create default data for previews
|
|
||||||
DataLoader.create(modelContext: context)
|
|
||||||
|
|
||||||
return container
|
return container
|
||||||
} catch {
|
} catch {
|
||||||
fatalError("Failed to create preview ModelContainer: \(error.localizedDescription)")
|
fatalError("Failed to create preview ModelContainer: \(error.localizedDescription)")
|
||||||
|
30
Workouts/Utils/Color+color.swift
Normal file
30
Workouts/Utils/Color+color.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// Color+color.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Created by rzen on 7/17/25 at 10:41 AM.
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUICore
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
static func color (from: String) -> Color {
|
||||||
|
switch from {
|
||||||
|
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 .black
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,14 +18,9 @@ enum AppStorageKeys {
|
|||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
|
||||||
@State private var showingPopulateData = false
|
|
||||||
@State private var showingClearAllDataConfirmation = false
|
@State private var showingClearAllDataConfirmation = false
|
||||||
|
|
||||||
var splitsCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Split>()) }
|
var splitsCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Split>()) }
|
||||||
var musclesCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Muscle>()) }
|
|
||||||
var muscleGroupsCount: Int? { try? modelContext.fetchCount(FetchDescriptor<MuscleGroup>()) }
|
|
||||||
var exerciseTypeCount: Int? { try? modelContext.fetchCount(FetchDescriptor<ExerciseType>()) }
|
|
||||||
var exercisesCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Exercise>()) }
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@ -40,67 +35,9 @@ struct SettingsView: View {
|
|||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(destination: MuscleGroupsListView()) {
|
|
||||||
HStack {
|
|
||||||
Text("Muscle Groups")
|
|
||||||
Spacer()
|
|
||||||
Text("\(muscleGroupsCount ?? 0)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NavigationLink(destination: MusclesListView()) {
|
|
||||||
HStack {
|
|
||||||
Text("Muscles")
|
|
||||||
Spacer()
|
|
||||||
Text("\(musclesCount ?? 0)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NavigationLink(destination: ExerciseTypeListView()) {
|
|
||||||
HStack {
|
|
||||||
Text("Exercise Types")
|
|
||||||
Spacer()
|
|
||||||
Text("\(exerciseTypeCount ?? 0)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NavigationLink(destination: ExercisesListView()) {
|
|
||||||
HStack {
|
|
||||||
Text("Exercises")
|
|
||||||
Spacer()
|
|
||||||
Text("\(exercisesCount ?? 0)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Developer")) {
|
Section(header: Text("Developer")) {
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
showingPopulateData = true
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
Label("Populate Data", systemImage: "plus")
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.confirmationDialog(
|
|
||||||
"Populate Data?",
|
|
||||||
isPresented: $showingPopulateData,
|
|
||||||
titleVisibility: .hidden
|
|
||||||
) {
|
|
||||||
Button("Populate Data") {
|
|
||||||
DataLoader.create(modelContext: modelContext)
|
|
||||||
}
|
|
||||||
Button("Cancel", role: .cancel) {}
|
|
||||||
// } message: {
|
|
||||||
// Text("This action cannot be undone. All data will be permanently deleted.")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showingClearAllDataConfirmation = true
|
showingClearAllDataConfirmation = true
|
||||||
}) {
|
}) {
|
||||||
@ -139,10 +76,6 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
private func clearAllData () {
|
private func clearAllData () {
|
||||||
do {
|
do {
|
||||||
try deleteAllObjects(ofType: ExerciseType.self, from: modelContext)
|
|
||||||
try deleteAllObjects(ofType: Exercise.self, from: modelContext)
|
|
||||||
try deleteAllObjects(ofType: Muscle.self, from: modelContext)
|
|
||||||
try deleteAllObjects(ofType: MuscleGroup.self, from: modelContext)
|
|
||||||
try deleteAllObjects(ofType: Split.self, from: modelContext)
|
try deleteAllObjects(ofType: Split.self, from: modelContext)
|
||||||
try deleteAllObjects(ofType: SplitExerciseAssignment.self, from: modelContext)
|
try deleteAllObjects(ofType: SplitExerciseAssignment.self, from: modelContext)
|
||||||
try deleteAllObjects(ofType: Workout.self, from: modelContext)
|
try deleteAllObjects(ofType: Workout.self, from: modelContext)
|
||||||
@ -154,30 +87,6 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExercisesListView: View {
|
|
||||||
var body: some View {
|
|
||||||
EntityListView<Exercise>(sort: [SortDescriptor(\Exercise.name)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ExerciseTypeListView: View {
|
|
||||||
var body: some View {
|
|
||||||
EntityListView<ExerciseType>(sort: [SortDescriptor(\ExerciseType.name)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MuscleGroupsListView: View {
|
|
||||||
var body: some View {
|
|
||||||
EntityListView<MuscleGroup>(sort: [SortDescriptor(\MuscleGroup.name)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MusclesListView: View {
|
|
||||||
var body: some View {
|
|
||||||
EntityListView<Muscle>(sort: [SortDescriptor(\Muscle.name)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SplitsListView: View {
|
struct SplitsListView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
EntityListView<Split>(sort: [SortDescriptor(\Split.name)])
|
EntityListView<Split>(sort: [SortDescriptor(\Split.name)])
|
||||||
|
@ -12,15 +12,24 @@ import SwiftUI
|
|||||||
struct SplitExerciseAssignmentAddEditView: View {
|
struct SplitExerciseAssignmentAddEditView: View {
|
||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@State private var showingExercisePicker = false
|
||||||
|
|
||||||
@State var model: SplitExerciseAssignment
|
@State var model: SplitExerciseAssignment
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
Section (header: Text("Setup")) {
|
Section(header: Text("Exercise")) {
|
||||||
TextEditor(text: $model.setup)
|
Button(action: {
|
||||||
.frame(minHeight: 60)
|
showingExercisePicker = true
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text(model.exerciseName.isEmpty ? "Select Exercise" : model.exerciseName)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section (header: Text("Sets/Reps")) {
|
Section (header: Text("Sets/Reps")) {
|
||||||
@ -44,7 +53,12 @@ struct SplitExerciseAssignmentAddEditView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("\(model.exercise?.name ?? Exercise.unnamed)")
|
.sheet(isPresented: $showingExercisePicker) {
|
||||||
|
ExercisePickerView { exerciseName in
|
||||||
|
model.exerciseName = exerciseName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(model.exerciseName.isEmpty ? "New Exercise" : model.exerciseName)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
|
64
Workouts/Views/Splits/SplitAddEditView.swift
Normal file
64
Workouts/Views/Splits/SplitAddEditView.swift
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
//
|
||||||
|
// SplitAddEditView.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Created by rzen on 7/18/25 at 9:42 AM.
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SplitAddEditView: View {
|
||||||
|
@State var model: Split
|
||||||
|
|
||||||
|
private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
|
||||||
|
|
||||||
|
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 {
|
||||||
|
Form {
|
||||||
|
Section(header: Text("Name")) {
|
||||||
|
TextField("Name", text: $model.name)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Appearance")) {
|
||||||
|
Picker("Color", selection: $model.color) {
|
||||||
|
ForEach(availableColors, id: \.self) { colorName in
|
||||||
|
let tempSplit = Split(name: "", 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 {
|
||||||
|
SplitExercisesListView(model: model)
|
||||||
|
} label: {
|
||||||
|
ListItem(
|
||||||
|
text: "Exercises",
|
||||||
|
count: model.exercises?.count ?? 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
Workouts/Views/Splits/SplitExercisesListView.swift
Normal file
108
Workouts/Views/Splits/SplitExercisesListView.swift
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// SplitExercisesListView.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Created by rzen on 7/18/25 at 8:38 AM.
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SplitExercisesListView: View {
|
||||||
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
var model: Split
|
||||||
|
|
||||||
|
@State private var showingAddSheet: Bool = false
|
||||||
|
@State private var itemToEdit: SplitExerciseAssignment? = nil
|
||||||
|
@State private var itemToDelete: SplitExerciseAssignment? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
List {
|
||||||
|
if let assignments = model.exercises, !assignments.isEmpty {
|
||||||
|
let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.exerciseName < $1.exerciseName : $0.order < $1.order })
|
||||||
|
|
||||||
|
ForEach(sortedAssignments) { item in
|
||||||
|
ListItem(
|
||||||
|
title: item.exerciseName,
|
||||||
|
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs \(item.order)"
|
||||||
|
)
|
||||||
|
.swipeActions {
|
||||||
|
Button {
|
||||||
|
itemToDelete = item
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "circle")
|
||||||
|
}
|
||||||
|
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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Text("No exercises added yet.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("\(model.name)")
|
||||||
|
}
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
102
Workouts/Views/Splits/SplitsView.swift
Normal file
102
Workouts/Views/Splits/SplitsView.swift
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
//
|
||||||
|
// SplitsView.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Created by rzen on 7/17/25 at 6:55 PM.
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
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 private var showingAddSheet: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
ScrollView {
|
||||||
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
|
||||||
|
ForEach(splits) { split 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet (isPresented: $showingAddSheet) {
|
||||||
|
SplitAddEditView(model: Split(name: "New Split"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
89
Workouts/Views/Workouts/CalendarListItem.swift
Normal file
89
Workouts/Views/Workouts/CalendarListItem.swift
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
//
|
||||||
|
// CalendarListItem.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Created by rzen on 7/18/25 at 8:44 AM.
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CalendarListItem: View {
|
||||||
|
var date: Date
|
||||||
|
var title: String
|
||||||
|
var subtitle: String?
|
||||||
|
var count: Int?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack (alignment: .top) {
|
||||||
|
ZStack {
|
||||||
|
VStack {
|
||||||
|
Text("\(date.abbreviatedWeekday)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("\(date.dayOfMonth)")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Text("\(date.abbreviatedMonth)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding([.trailing], 10)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
VStack (alignment: .leading) {
|
||||||
|
Text("\(title)")
|
||||||
|
.font(.headline)
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Date {
|
||||||
|
private static let monthFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale.current
|
||||||
|
formatter.dateFormat = "MMM"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let dayFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale.current
|
||||||
|
formatter.dateFormat = "d"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let weekdayFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale.current
|
||||||
|
formatter.dateFormat = "E"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
var abbreviatedMonth: String {
|
||||||
|
Date.monthFormatter.string(from: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dayOfMonth: String {
|
||||||
|
Date.dayFormatter.string(from: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
var abbreviatedWeekday: String {
|
||||||
|
Date.weekdayFormatter.string(from: self)
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
//
|
//
|
||||||
// SplitPickerView.swift
|
// ExercisePickerView.swift
|
||||||
// Workouts
|
// Workouts
|
||||||
//
|
//
|
||||||
// Created by rzen on 7/13/25 at 7:17 PM.
|
// Created by rzen on 7/13/25 at 7:17 PM.
|
||||||
//
|
//
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
//
|
//
|
||||||
@ -11,37 +11,65 @@ import SwiftUI
|
|||||||
import SwiftData
|
import SwiftData
|
||||||
|
|
||||||
struct ExercisePickerView: View {
|
struct ExercisePickerView: View {
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@State private var exerciseLists: [String: ExerciseList] = [:]
|
||||||
|
@State private var selectedListName: String? = nil
|
||||||
|
|
||||||
@Query(sort: [SortDescriptor(\ExerciseType.name)]) private var exerciseTypes: [ExerciseType]
|
var onExerciseSelected: (String) -> Void
|
||||||
|
|
||||||
// @Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
|
|
||||||
|
|
||||||
var onExerciseSelected: (Exercise) -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
VStack {
|
Group {
|
||||||
Form {
|
if selectedListName == nil {
|
||||||
ForEach (exerciseTypes) { exerciseType in
|
// Show list of exercise list files
|
||||||
if let exercises = exerciseType.exercises, !exercises.isEmpty {
|
|
||||||
let sortedExercises = exercises.sorted(by: { $0.name < $1.name })
|
|
||||||
Section (header: Text("\(exerciseType.name)")) {
|
|
||||||
List {
|
List {
|
||||||
ForEach(sortedExercises) { exercise in
|
ForEach(Array(exerciseLists.keys.sorted()), id: \.self) { fileName in
|
||||||
|
if let list = exerciseLists[fileName] {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
onExerciseSelected(exercise)
|
selectedListName = fileName
|
||||||
|
}) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(list.name)
|
||||||
|
.font(.headline)
|
||||||
|
Text(list.source)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("\(list.exercises.count) exercises")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Exercise Lists")
|
||||||
|
} else if let fileName = selectedListName, let list = exerciseLists[fileName] {
|
||||||
|
// Show exercises in the selected list
|
||||||
|
List {
|
||||||
|
ForEach(list.exercises) { exercise in
|
||||||
|
Button(action: {
|
||||||
|
onExerciseSelected(exercise.name)
|
||||||
dismiss()
|
dismiss()
|
||||||
}) {
|
}) {
|
||||||
ListItem(text: exercise.name)
|
VStack(alignment: .leading) {
|
||||||
|
Text(exercise.name)
|
||||||
|
.font(.headline)
|
||||||
|
Text(exercise.type)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.padding(.vertical, 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navigationTitle(list.name)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Back") {
|
||||||
|
selectedListName = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,5 +81,12 @@ struct ExercisePickerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
loadExerciseLists()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadExerciseLists() {
|
||||||
|
exerciseLists = ExerciseListLoader.loadExerciseLists()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,8 @@ struct SplitPickerView: View {
|
|||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@Query(sort: [SortDescriptor(\Split.name)]) private var splits: [Split]
|
@Query(sort: [SortDescriptor(\Split.name)]) private var splits: [Split]
|
||||||
@Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
|
|
||||||
|
|
||||||
var onSplitSelected: (Split) -> Void
|
var onSplitSelected: (Split) -> Void
|
||||||
// var onExerciseSelected: (Exercise) -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
|
@ -49,7 +49,7 @@ struct WorkoutEditView: View {
|
|||||||
// List {
|
// List {
|
||||||
// ForEach (workoutLogs) { log in
|
// ForEach (workoutLogs) { log in
|
||||||
// ListItem(
|
// ListItem(
|
||||||
// title: log.exercise?.name ?? Exercise.unnamed,
|
// title: log.exerciseName,
|
||||||
// subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
|
// subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
|
||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
|
@ -21,7 +21,7 @@ struct WorkoutLogEditView: View {
|
|||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
Section (header: Text("Exercise")) {
|
Section (header: Text("Exercise")) {
|
||||||
Text("\(workoutLog.exercise?.name ?? Exercise.unnamed)")
|
Text(workoutLog.exerciseName)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,20 +86,20 @@ struct WorkoutLogEditView: View {
|
|||||||
let split = workoutLog.workout?.split
|
let split = workoutLog.workout?.split
|
||||||
|
|
||||||
// Find the matching exercise in split.exercises by name
|
// Find the matching exercise in split.exercises by name
|
||||||
// if let exercises = split?.exercises {
|
if let exercises = split?.exercises {
|
||||||
// for exerciseAssignment in exercises {
|
for exerciseAssignment in exercises {
|
||||||
// if exerciseAssignment.exercise.name == workoutLog.exercise.name {
|
if exerciseAssignment.exerciseName == workoutLog.exerciseName {
|
||||||
// // Update the sets, reps, and weight in the split exercise assignment
|
// Update the sets, reps, and weight in the split exercise assignment
|
||||||
// exerciseAssignment.sets = workoutLog.sets
|
exerciseAssignment.sets = workoutLog.sets
|
||||||
// exerciseAssignment.reps = workoutLog.reps
|
exerciseAssignment.reps = workoutLog.reps
|
||||||
// exerciseAssignment.weight = workoutLog.weight
|
exerciseAssignment.weight = workoutLog.weight
|
||||||
//
|
|
||||||
// // Save the changes to the split
|
// Save the changes to the split
|
||||||
// try? modelContext.save()
|
try? modelContext.save()
|
||||||
// break
|
break
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ struct WorkoutLogView: View {
|
|||||||
var sortedWorkoutLogs: [WorkoutLog] {
|
var sortedWorkoutLogs: [WorkoutLog] {
|
||||||
if let logs = workout.logs {
|
if let logs = workout.logs {
|
||||||
logs.sorted(by: {
|
logs.sorted(by: {
|
||||||
$0.order == $1.order ? $0.exercise!.name < $1.exercise!.name : $0.order < $1.order
|
$0.order == $1.order ? $0.exerciseName < $1.exerciseName : $0.order < $1.order
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
[]
|
[]
|
||||||
@ -41,7 +41,7 @@ struct WorkoutLogView: View {
|
|||||||
|
|
||||||
CheckboxListItem(
|
CheckboxListItem(
|
||||||
status: workoutLogStatus,
|
status: workoutLogStatus,
|
||||||
title: log.exercise?.name ?? Exercise.unnamed,
|
title: log.exerciseName,
|
||||||
subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
|
subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -85,11 +85,12 @@ struct WorkoutLogView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||||
Button(role: .destructive) {
|
Button {
|
||||||
itemToDelete = log
|
itemToDelete = log
|
||||||
} label: {
|
} label: {
|
||||||
Label("Delete", systemImage: "trash")
|
Label("Delete", systemImage: "trash")
|
||||||
}
|
}
|
||||||
|
.tint(.secondary)
|
||||||
Button {
|
Button {
|
||||||
itemToEdit = log
|
itemToEdit = log
|
||||||
} label: {
|
} label: {
|
||||||
@ -110,11 +111,11 @@ struct WorkoutLogView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingAddSheet) {
|
.sheet(isPresented: $showingAddSheet) {
|
||||||
ExercisePickerView { exercise in
|
ExercisePickerView { exerciseName in
|
||||||
let setsRepsWeight = getSetsRepsWeight(exercise, in: modelContext)
|
let setsRepsWeight = getSetsRepsWeight(exerciseName, in: modelContext)
|
||||||
let workoutLog = WorkoutLog(
|
let workoutLog = WorkoutLog(
|
||||||
workout: workout,
|
workout: workout,
|
||||||
exercise: exercise,
|
exerciseName: exerciseName,
|
||||||
date: Date(),
|
date: Date(),
|
||||||
sets: setsRepsWeight.sets,
|
sets: setsRepsWeight.sets,
|
||||||
reps: setsRepsWeight.reps,
|
reps: setsRepsWeight.reps,
|
||||||
@ -149,20 +150,18 @@ struct WorkoutLogView: View {
|
|||||||
itemToDelete = nil
|
itemToDelete = nil
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text("Are you sure you want to delete workout started \(itemToDelete?.exercise?.name ?? "this item")?")
|
Text("Are you sure you want to delete workout started \(itemToDelete?.exerciseName ?? "this item")?")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSetsRepsWeight(_ exercise: Exercise, in modelContext: ModelContext) -> SetsRepsWeight {
|
func getSetsRepsWeight(_ exerciseName: String, in modelContext: ModelContext) -> SetsRepsWeight {
|
||||||
// Use a single expression predicate that works with SwiftData
|
// Use a single expression predicate that works with SwiftData
|
||||||
let exerciseID = exercise.persistentModelID
|
print("Searching for exercise name: \(exerciseName)")
|
||||||
|
|
||||||
print("Searching for exercise ID: \(exerciseID)")
|
|
||||||
|
|
||||||
var descriptor = FetchDescriptor<WorkoutLog>(
|
var descriptor = FetchDescriptor<WorkoutLog>(
|
||||||
predicate: #Predicate<WorkoutLog> { log in
|
predicate: #Predicate<WorkoutLog> { log in
|
||||||
log.exercise?.persistentModelID == exerciseID
|
log.exerciseName == exerciseName
|
||||||
},
|
},
|
||||||
sortBy: [SortDescriptor(\WorkoutLog.date, order: .reverse)]
|
sortBy: [SortDescriptor(\WorkoutLog.date, order: .reverse)]
|
||||||
)
|
)
|
||||||
|
@ -34,7 +34,8 @@ struct WorkoutsView: View {
|
|||||||
List {
|
List {
|
||||||
ForEach (workouts) { workout in
|
ForEach (workouts) { workout in
|
||||||
NavigationLink(destination: WorkoutLogView(workout: workout)) {
|
NavigationLink(destination: WorkoutLogView(workout: workout)) {
|
||||||
ListItem(
|
CalendarListItem(
|
||||||
|
date: workout.start,
|
||||||
title: workout.split?.name ?? Split.unnamed,
|
title: workout.split?.name ?? Split.unnamed,
|
||||||
subtitle: workout.label
|
subtitle: workout.label
|
||||||
)
|
)
|
||||||
@ -95,13 +96,11 @@ struct WorkoutsView: View {
|
|||||||
SplitPickerView { split in
|
SplitPickerView { split in
|
||||||
let workout = Workout(start: Date(), split: split)
|
let workout = Workout(start: Date(), split: split)
|
||||||
modelContext.insert(workout)
|
modelContext.insert(workout)
|
||||||
|
|
||||||
if let exercises = split.exercises {
|
if let exercises = split.exercises {
|
||||||
for assignment in exercises {
|
for assignment in exercises {
|
||||||
if let exercise = assignment.exercise {
|
|
||||||
let workoutLog = WorkoutLog(
|
let workoutLog = WorkoutLog(
|
||||||
workout: workout,
|
workout: workout,
|
||||||
exercise: exercise,
|
exerciseName: assignment.exerciseName,
|
||||||
date: Date(),
|
date: Date(),
|
||||||
order: assignment.order,
|
order: assignment.order,
|
||||||
sets: assignment.sets,
|
sets: assignment.sets,
|
||||||
@ -109,9 +108,6 @@ struct WorkoutsView: View {
|
|||||||
weight: assignment.weight
|
weight: assignment.weight
|
||||||
)
|
)
|
||||||
modelContext.insert(workoutLog)
|
modelContext.insert(workoutLog)
|
||||||
} else {
|
|
||||||
logger.debug("An exercise entity for a split is nil")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try? modelContext.save()
|
try? modelContext.save()
|
||||||
|
Reference in New Issue
Block a user