wip
This commit is contained in:
@ -13,52 +13,33 @@ import SwiftData
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query private var items: [Item]
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
List {
|
||||
ForEach(items) { item in
|
||||
NavigationLink {
|
||||
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
|
||||
} label: {
|
||||
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
|
||||
}
|
||||
TabView {
|
||||
Text("Placeholder")
|
||||
.tabItem {
|
||||
Label("Workout", systemImage: "figure.strengthtraining.traditional")
|
||||
}
|
||||
.onDelete(perform: deleteItems)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
EditButton()
|
||||
}
|
||||
ToolbarItem {
|
||||
Button(action: addItem) {
|
||||
Label("Add Item", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
} detail: {
|
||||
Text("Select an item")
|
||||
}
|
||||
}
|
||||
|
||||
private func addItem() {
|
||||
withAnimation {
|
||||
let newItem = Item(timestamp: Date())
|
||||
modelContext.insert(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteItems(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(items[index])
|
||||
|
||||
// Reports Tab
|
||||
NavigationStack {
|
||||
Text("Reports Placeholder")
|
||||
.navigationTitle("Reports")
|
||||
}
|
||||
.tabItem {
|
||||
Label("Reports", systemImage: "chart.bar")
|
||||
}
|
||||
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
.modelContainer(for: Item.self, inMemory: true)
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
//
|
||||
// Item.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/11/25 at 5:04 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Item {
|
||||
var timestamp: Date
|
||||
|
||||
init(timestamp: Date) {
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
}
|
33
Workouts/Models/Exercise.swift
Normal file
33
Workouts/Models/Exercise.swift
Normal file
@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Exercise: ListableItem {
|
||||
@Attribute(.unique) 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?
|
||||
|
||||
@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, setup: String, descr: String, sets: Int, reps: Int, weight: Int) {
|
||||
self.name = name
|
||||
self.setup = setup
|
||||
self.descr = descr
|
||||
self.sets = sets
|
||||
self.reps = reps
|
||||
self.weight = weight
|
||||
}
|
||||
}
|
16
Workouts/Models/ExerciseType.swift
Normal file
16
Workouts/Models/ExerciseType.swift
Normal file
@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class ExerciseType: ListableItem {
|
||||
@Attribute(.unique) var name: String = ""
|
||||
var descr: String = ""
|
||||
|
||||
@Relationship(deleteRule: .nullify)
|
||||
var exercises: [Exercise]? = []
|
||||
|
||||
init(name: String, descr: String) {
|
||||
self.name = name
|
||||
self.descr = descr
|
||||
}
|
||||
}
|
21
Workouts/Models/Muscle.swift
Normal file
21
Workouts/Models/Muscle.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Muscle: ListableItem {
|
||||
@Attribute(.unique) 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) {
|
||||
self.name = name
|
||||
self.descr = descr
|
||||
self.muscleGroup = muscleGroup
|
||||
}
|
||||
}
|
16
Workouts/Models/MuscleGroup.swift
Normal file
16
Workouts/Models/MuscleGroup.swift
Normal file
@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class MuscleGroup: ListableItem {
|
||||
@Attribute(.unique) var name: String = ""
|
||||
var descr: String = ""
|
||||
|
||||
@Relationship(deleteRule: .nullify)
|
||||
var muscles: [Muscle]? = []
|
||||
|
||||
init(name: String, descr: String) {
|
||||
self.name = name
|
||||
self.descr = descr
|
||||
}
|
||||
}
|
19
Workouts/Models/Split.swift
Normal file
19
Workouts/Models/Split.swift
Normal file
@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Split: ListableItem {
|
||||
@Attribute(.unique) var name: String = ""
|
||||
var intro: String = ""
|
||||
|
||||
@Relationship(deleteRule: .cascade, inverse: \SplitExerciseAssignment.split)
|
||||
var exercises: [SplitExerciseAssignment]? = []
|
||||
|
||||
@Relationship(deleteRule: .nullify, inverse: \Workout.split)
|
||||
var workouts: [Workout]? = []
|
||||
|
||||
init(name: String, intro: String) {
|
||||
self.name = name
|
||||
self.intro = intro
|
||||
}
|
||||
}
|
25
Workouts/Models/SplitExerciseAssignment.swift
Normal file
25
Workouts/Models/SplitExerciseAssignment.swift
Normal file
@ -0,0 +1,25 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class SplitExerciseAssignment {
|
||||
var order: Int = 0
|
||||
var sets: Int = 0
|
||||
var reps: Int = 0
|
||||
var weight: Int = 0
|
||||
|
||||
@Relationship(deleteRule: .nullify)
|
||||
var split: Split?
|
||||
|
||||
@Relationship(deleteRule: .nullify)
|
||||
var exercise: Exercise?
|
||||
|
||||
init(order: Int, sets: Int, reps: Int, weight: Int, split: Split, exercise: Exercise) {
|
||||
self.order = order
|
||||
self.sets = sets
|
||||
self.reps = reps
|
||||
self.weight = weight
|
||||
self.split = split
|
||||
self.exercise = exercise
|
||||
}
|
||||
}
|
20
Workouts/Models/Workout.swift
Normal file
20
Workouts/Models/Workout.swift
Normal file
@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Workout {
|
||||
var start: Date = Date()
|
||||
var end: Date?
|
||||
|
||||
@Relationship(deleteRule: .nullify)
|
||||
var split: Split?
|
||||
|
||||
@Relationship(deleteRule: .cascade, inverse: \WorkoutLog.workout)
|
||||
var logs: [WorkoutLog]? = []
|
||||
|
||||
init(start: Date, end: Date? = nil, split: Split?) {
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.split = split
|
||||
}
|
||||
}
|
27
Workouts/Models/WorkoutLog.swift
Normal file
27
Workouts/Models/WorkoutLog.swift
Normal file
@ -0,0 +1,27 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class WorkoutLog {
|
||||
var date: Date = Date()
|
||||
var sets: Int = 0
|
||||
var reps: Int = 0
|
||||
var weight: Int = 0
|
||||
var completed: Bool = false
|
||||
|
||||
@Relationship(deleteRule: .nullify)
|
||||
var workout: Workout?
|
||||
|
||||
@Relationship(deleteRule: .nullify)
|
||||
var exercise: Exercise?
|
||||
|
||||
init(date: Date, sets: Int, reps: Int, weight: Int, completed: Bool, workout: Workout, exercise: Exercise) {
|
||||
self.date = date
|
||||
self.sets = sets
|
||||
self.reps = reps
|
||||
self.weight = weight
|
||||
self.completed = completed
|
||||
self.workout = workout
|
||||
self.exercise = exercise
|
||||
}
|
||||
}
|
12
Workouts/Protocols/ListableItem.swift
Normal file
12
Workouts/Protocols/ListableItem.swift
Normal file
@ -0,0 +1,12 @@
|
||||
//
|
||||
// ListableItem.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 10:40 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
protocol ListableItem {
|
||||
var name: String { get set }
|
||||
}
|
165
Workouts/Schema/InitialData.swift
Normal file
165
Workouts/Schema/InitialData.swift
Normal file
@ -0,0 +1,165 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
struct InitialData {
|
||||
static let logger = AppLogger(subsystem: "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, setup: data.setup, descr: data.descr,
|
||||
sets: data.sets, reps: data.reps, weight: data.weight)
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
16
Workouts/Schema/SchemaV1.swift
Normal file
16
Workouts/Schema/SchemaV1.swift
Normal file
@ -0,0 +1,16 @@
|
||||
import SwiftData
|
||||
|
||||
enum SchemaV1: VersionedSchema {
|
||||
static var versionIdentifier: Schema.Version = .init(1, 0, 0)
|
||||
|
||||
static var models: [any PersistentModel.Type] = [
|
||||
Exercise.self,
|
||||
ExerciseType.self,
|
||||
Muscle.self,
|
||||
MuscleGroup.self,
|
||||
Split.self,
|
||||
SplitExerciseAssignment.self,
|
||||
Workout.self,
|
||||
WorkoutLog.self
|
||||
]
|
||||
}
|
13
Workouts/Schema/SchemaVersion.swift
Normal file
13
Workouts/Schema/SchemaVersion.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import SwiftData
|
||||
|
||||
enum SchemaVersion: Int, CaseIterable {
|
||||
static var allCases: [SchemaVersion] = [
|
||||
.v1
|
||||
]
|
||||
|
||||
case v1
|
||||
|
||||
static var current: SchemaVersion {
|
||||
.v1
|
||||
}
|
||||
}
|
40
Workouts/Schema/WorkoutsContainer.swift
Normal file
40
Workouts/Schema/WorkoutsContainer.swift
Normal file
@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
final class WorkoutsContainer {
|
||||
static let logger = AppLogger(subsystem: "Workouts", category: "WorkoutsContainer")
|
||||
|
||||
static func create(shouldCreateDefaults: inout Bool) -> ModelContainer {
|
||||
let schema = Schema(versionedSchema: SchemaV1.self)
|
||||
let container = try! ModelContainer(for: schema, migrationPlan: WorkoutsMigrationPlan.self)
|
||||
|
||||
let context = ModelContext(container)
|
||||
let descriptor = FetchDescriptor<Workout>()
|
||||
let results = try! context.fetch(descriptor)
|
||||
|
||||
if results.isEmpty {
|
||||
shouldCreateDefaults = true
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static var preview: ModelContainer {
|
||||
let schema = Schema(versionedSchema: SchemaV1.self)
|
||||
|
||||
let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
||||
|
||||
do {
|
||||
let container = try ModelContainer(for: schema, configurations: [configuration])
|
||||
let context = ModelContext(container)
|
||||
|
||||
// Create default data for previews
|
||||
InitialData.create(modelContext: context)
|
||||
|
||||
return container
|
||||
} catch {
|
||||
fatalError("Failed to create preview ModelContainer: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
11
Workouts/Schema/WorkoutsMigrationPlan.swift
Normal file
11
Workouts/Schema/WorkoutsMigrationPlan.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import SwiftData
|
||||
|
||||
struct WorkoutsMigrationPlan: SchemaMigrationPlan {
|
||||
static var schemas: [VersionedSchema.Type] = [
|
||||
SchemaV1.self
|
||||
]
|
||||
|
||||
static var stages: [MigrationStage] = [
|
||||
// Add migration stages here in the future
|
||||
]
|
||||
}
|
37
Workouts/Utils/AppLogger.swift
Normal file
37
Workouts/Utils/AppLogger.swift
Normal file
@ -0,0 +1,37 @@
|
||||
import OSLog
|
||||
|
||||
struct AppLogger {
|
||||
private let logger: Logger
|
||||
private let subsystem: String
|
||||
private let category: String
|
||||
|
||||
init(subsystem: String, category: String) {
|
||||
self.subsystem = subsystem
|
||||
self.category = category
|
||||
self.logger = Logger(subsystem: subsystem, category: category)
|
||||
}
|
||||
|
||||
func timestamp () -> String {
|
||||
Date.now.formatDateET(format: "yyyy-MM-dd HH:mm:ss")
|
||||
}
|
||||
|
||||
func formattedMessage (_ message: String) -> String {
|
||||
"\(timestamp()) [\(subsystem):\(category)] \(message)"
|
||||
}
|
||||
|
||||
func debug(_ message: String) {
|
||||
logger.debug("\(formattedMessage(message))")
|
||||
}
|
||||
|
||||
func info(_ message: String) {
|
||||
logger.info("\(formattedMessage(message))")
|
||||
}
|
||||
|
||||
func warning(_ message: String) {
|
||||
logger.warning("\(formattedMessage(message))")
|
||||
}
|
||||
|
||||
func error(_ message: String) {
|
||||
logger.error("\(formattedMessage(message))")
|
||||
}
|
||||
}
|
15
Workouts/Utils/Badge.swift
Normal file
15
Workouts/Utils/Badge.swift
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// Badge.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 5:42 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUICore
|
||||
|
||||
struct Badge: Hashable {
|
||||
var text: String
|
||||
var color: Color
|
||||
}
|
14
Workouts/Utils/Date+Extensions.swift
Normal file
14
Workouts/Utils/Date+Extensions.swift
Normal file
@ -0,0 +1,14 @@
|
||||
import Foundation
|
||||
|
||||
extension Date {
|
||||
func formatDateET(format: String = "MMMM, d yyyy @ h:mm a z") -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.timeZone = TimeZone(identifier: "America/New_York")
|
||||
formatter.dateFormat = format
|
||||
return formatter.string(from: self)
|
||||
}
|
||||
|
||||
static var ISO8601: String {
|
||||
"yyyy-MM-dd'T'HH:mm:ssZ"
|
||||
}
|
||||
}
|
50
Workouts/Utils/ListItem.swift
Normal file
50
Workouts/Utils/ListItem.swift
Normal file
@ -0,0 +1,50 @@
|
||||
//
|
||||
// ListItem.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 10:42 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ListItem: View {
|
||||
var title: String
|
||||
var subtitle: String?
|
||||
var count: Int?
|
||||
var badges: [Badge]? = []
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack (alignment: .leading) {
|
||||
Text("\(title)")
|
||||
HStack {
|
||||
if let subtitle = subtitle {
|
||||
Text("\(subtitle)")
|
||||
.font(.footnote)
|
||||
}
|
||||
if let badges = badges {
|
||||
ForEach (badges, id: \.self) { badge in
|
||||
Text("\(badge.text)")
|
||||
.bold()
|
||||
.padding([.leading,.trailing], 5)
|
||||
.cornerRadius(4)
|
||||
.background(badge.color)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let count = count {
|
||||
Spacer()
|
||||
Text("\(count)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
//
|
||||
// ExerciseTypeAddEditView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 11:33 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ExerciseTypeAddEditView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Bindable var model: ExerciseType
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section (header: Text("Nname")) {
|
||||
TextField("Name", text: $model.name)
|
||||
.bold()
|
||||
}
|
||||
|
||||
Section(header: Text("Description")) {
|
||||
TextEditor(text: $model.descr)
|
||||
.frame(minHeight: 100)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
try? modelContext.save()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
//
|
||||
// ExerciseTypeListView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 11:27 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ExerciseTypeListView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var items: [ExerciseType]
|
||||
|
||||
@State var itemToEdit: ExerciseType? = nil
|
||||
@State var itemToDelete: ExerciseType? = nil
|
||||
|
||||
private func save () {
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
List {
|
||||
ForEach (items) { item in
|
||||
ListItem(title: item.name)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button (role: .destructive) {
|
||||
itemToDelete = item
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
Button {
|
||||
itemToEdit = item
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
.tint(.indigo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Exercise Types")
|
||||
.sheet(item: $itemToEdit) {item in
|
||||
ExerciseTypeAddEditView(model: item)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Delete?",
|
||||
isPresented: Binding<Bool>(
|
||||
get: { itemToDelete != nil },
|
||||
set: { if !$0 { itemToDelete = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Delete", role: .destructive) {
|
||||
if let itemToDelete = itemToDelete {
|
||||
modelContext.delete(itemToDelete)
|
||||
try? modelContext.save()
|
||||
self.itemToDelete = nil
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
itemToDelete = nil
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
Workouts/Views/Settings/Exercises/ExerciseAddEditView.swift
Normal file
13
Workouts/Views/Settings/Exercises/ExerciseAddEditView.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ExerciseAddEditView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Bindable var model: Exercise
|
||||
|
||||
var body: some View {
|
||||
Text("Add/Edit Exercise")
|
||||
.navigationTitle("Exercise")
|
||||
}
|
||||
}
|
82
Workouts/Views/Settings/Exercises/ExercisesListView.swift
Normal file
82
Workouts/Views/Settings/Exercises/ExercisesListView.swift
Normal file
@ -0,0 +1,82 @@
|
||||
//
|
||||
// ExercisesListView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 4:30 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ExercisesListView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var groups: [ExerciseType]
|
||||
|
||||
@State var itemToEdit: Exercise? = nil
|
||||
@State var itemToDelete: Exercise? = nil
|
||||
|
||||
private func save () {
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
ForEach (groups) { group in
|
||||
let items = group.exercises ?? []
|
||||
let itemCount = items.count
|
||||
if itemCount > 0 {
|
||||
Section (header: Text("\(group.name) (\(itemCount))")) {
|
||||
ForEach (items) { item in
|
||||
ListItem(title: item.name)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button (role: .destructive) {
|
||||
itemToDelete = item
|
||||
} label: {
|
||||
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
Button {
|
||||
itemToEdit = item
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
.tint(.indigo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Exercises")
|
||||
.sheet(item: $itemToEdit) { item in
|
||||
ExerciseAddEditView(model: item)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Delete?",
|
||||
isPresented: Binding<Bool>(
|
||||
get: { itemToDelete != nil },
|
||||
set: { if !$0 { itemToDelete = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Delete", role: .destructive) {
|
||||
if let itemToDelete = itemToDelete {
|
||||
modelContext.delete(itemToDelete)
|
||||
try? modelContext.save()
|
||||
self.itemToDelete = nil
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
itemToDelete = nil
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import SwiftUI
|
||||
|
||||
//
|
||||
// MuscleGroupAddEditView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 12:14 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
struct MuscleGroupAddEditView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Bindable var model: MuscleGroup
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section (header: Text("Name")) {
|
||||
TextField("Name", text: $model.name)
|
||||
.bold()
|
||||
}
|
||||
|
||||
Section(header: Text("Description")) {
|
||||
TextEditor(text: $model.descr)
|
||||
.frame(minHeight: 100)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
try? modelContext.save()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
//
|
||||
// MuscleGroupsListView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 12:14 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct MuscleGroupsListView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var items: [MuscleGroup]
|
||||
|
||||
@State var itemToEdit: MuscleGroup? = nil
|
||||
@State var itemToDelete: MuscleGroup? = nil
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
List {
|
||||
ForEach (items) { item in
|
||||
ListItem(title: item.name, count: item.muscles?.count)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button (role: .destructive) {
|
||||
itemToDelete = item
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
Button {
|
||||
itemToEdit = item
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
.tint(.indigo)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Muscle Groups")
|
||||
}
|
||||
.sheet(item: $itemToEdit) {item in
|
||||
MuscleGroupAddEditView(model: item)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Delete?",
|
||||
isPresented: Binding<Bool>(
|
||||
get: { itemToDelete != nil },
|
||||
set: { if !$0 { itemToDelete = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Delete", role: .destructive) {
|
||||
if let itemToDelete = itemToDelete {
|
||||
modelContext.delete(itemToDelete)
|
||||
try? modelContext.save()
|
||||
self.itemToDelete = nil
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
itemToDelete = nil
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
59
Workouts/Views/Settings/Muscles/MuscleAddEditView.swift
Normal file
59
Workouts/Views/Settings/Muscles/MuscleAddEditView.swift
Normal file
@ -0,0 +1,59 @@
|
||||
//
|
||||
// MuscleAddEditView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 11:55 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct MuscleAddEditView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var muscleGroups: [MuscleGroup]
|
||||
@Bindable var model: Muscle
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
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)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
try? modelContext.save()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
79
Workouts/Views/Settings/Muscles/MusclesListView.swift
Normal file
79
Workouts/Views/Settings/Muscles/MusclesListView.swift
Normal file
@ -0,0 +1,79 @@
|
||||
//
|
||||
// MuscleGroupsListView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 12:14 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct MusclesListView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var groups: [MuscleGroup]
|
||||
|
||||
@State var itemToEdit: Muscle? = nil
|
||||
@State var itemToDelete: Muscle? = nil
|
||||
|
||||
private func save () {
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
ForEach (groups) { group in
|
||||
Section (header: Text("\(group.name) (\(group.muscles?.count ?? 0))")) {
|
||||
let items = group.muscles ?? []
|
||||
ForEach (items) { item in
|
||||
ListItem(title: item.name)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button (role: .destructive) {
|
||||
itemToDelete = item
|
||||
} label: {
|
||||
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
Button {
|
||||
itemToEdit = item
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
.tint(.indigo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Muscles")
|
||||
.sheet(item: $itemToEdit) { item in
|
||||
MuscleAddEditView(model: item)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Delete?",
|
||||
isPresented: Binding<Bool>(
|
||||
get: { itemToDelete != nil },
|
||||
set: { if !$0 { itemToDelete = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Delete", role: .destructive) {
|
||||
if let itemToDelete = itemToDelete {
|
||||
modelContext.delete(itemToDelete)
|
||||
try? modelContext.save()
|
||||
self.itemToDelete = nil
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
itemToDelete = nil
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
77
Workouts/Views/Settings/SettingsView.swift
Normal file
77
Workouts/Views/Settings/SettingsView.swift
Normal file
@ -0,0 +1,77 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 10:24 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
|
||||
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 {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section (header: Text("Lists")) {
|
||||
NavigationLink(destination: SplitsListView()) {
|
||||
HStack {
|
||||
Text("Splits")
|
||||
Spacer()
|
||||
Text("\(splitsCount ?? 0)")
|
||||
.font(.caption)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
}
|
78
Workouts/Views/Settings/Splits/SplitAddEditView.swift
Normal file
78
Workouts/Views/Settings/Splits/SplitAddEditView.swift
Normal file
@ -0,0 +1,78 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SplitAddEditView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Bindable var model: Split
|
||||
|
||||
@State var itemToEdit: SplitExerciseAssignment? = nil
|
||||
@State var itemToDelete: SplitExerciseAssignment? = nil
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section (header: Text("Name")) {
|
||||
TextField("Name", text: $model.name)
|
||||
.bold()
|
||||
}
|
||||
|
||||
Section(header: Text("Description")) {
|
||||
TextEditor(text: $model.intro)
|
||||
.frame(minHeight: 100)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
Section(header: Text("Exercises")) {
|
||||
let item = model
|
||||
if let assignments = item.exercises, !assignments.isEmpty {
|
||||
ForEach(assignments, id: \.id) { item in
|
||||
List {
|
||||
ListItem(
|
||||
title: item.exercise?.name ?? "Unnamed",
|
||||
subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs"
|
||||
)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
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")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
|
||||
}) {
|
||||
Label("Add Exercise", systemImage: "plus.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
try? modelContext.save()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
78
Workouts/Views/Settings/Splits/SplitsListView.swift
Normal file
78
Workouts/Views/Settings/Splits/SplitsListView.swift
Normal file
@ -0,0 +1,78 @@
|
||||
//
|
||||
// SplitsListView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 10:27 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct SplitsListView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Query(sort: [SortDescriptor(\Split.name)]) var items: [Split]
|
||||
|
||||
@State var itemToEdit: Split? = nil
|
||||
@State var itemToDelete: Split? = nil
|
||||
|
||||
private func save () {
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
List {
|
||||
ForEach (items) { item in
|
||||
ListItem(
|
||||
title: item.name,
|
||||
count: item.exercises?.count
|
||||
)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button (role: .destructive) {
|
||||
itemToDelete = item
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
Button {
|
||||
itemToEdit = item
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
.tint(.indigo)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Muscle Groups")
|
||||
}
|
||||
.sheet(item: $itemToEdit) {item in
|
||||
SplitAddEditView(model: item)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Delete?",
|
||||
isPresented: Binding<Bool>(
|
||||
get: { itemToDelete != nil },
|
||||
set: { if !$0 { itemToDelete = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Delete", role: .destructive) {
|
||||
if let itemToDelete = itemToDelete {
|
||||
modelContext.delete(itemToDelete)
|
||||
try? modelContext.save()
|
||||
self.itemToDelete = nil
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
itemToDelete = nil
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,23 +13,21 @@ import SwiftData
|
||||
|
||||
@main
|
||||
struct WorkoutsApp: App {
|
||||
var sharedModelContainer: ModelContainer = {
|
||||
let schema = Schema([
|
||||
Item.self,
|
||||
])
|
||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
||||
|
||||
do {
|
||||
return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
||||
} catch {
|
||||
fatalError("Could not create ModelContainer: \(error)")
|
||||
let container: ModelContainer
|
||||
|
||||
init() {
|
||||
var shouldCreateDefaults = false
|
||||
self.container = WorkoutsContainer.create(shouldCreateDefaults: &shouldCreateDefaults)
|
||||
|
||||
if shouldCreateDefaults {
|
||||
InitialData.create(modelContext: ModelContext(container))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
.modelContainer(sharedModelContainer)
|
||||
.modelContainer(container)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user