add Workout, Exercise and their Library, Add, and Details views
This commit is contained in:
@@ -13,14 +13,14 @@ struct ContentView: View {
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
WorkoutLibraryView()
|
||||
WorkoutLibrary()
|
||||
.tabItem {
|
||||
Image(systemName: "gauge.with.needle.fill")
|
||||
Image(systemName: "figure.run.square.stack")
|
||||
Text("Workouts")
|
||||
}
|
||||
ExerciseLibraryView()
|
||||
ExerciseLibrary()
|
||||
.tabItem {
|
||||
Image(systemName: "figure.run.square.stack.fill")
|
||||
Image(systemName: "figure.run")
|
||||
Text("Exercises")
|
||||
}
|
||||
Text("Settings")
|
||||
@@ -34,5 +34,5 @@ struct ContentView: View {
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
.modelContainer(for: [Exercise.self, Workout.self], inMemory: true)
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
//
|
||||
// Item.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Exercise {
|
||||
var name: String
|
||||
var timestamp: Date
|
||||
|
||||
init(name: String = "", timestamp: Date = Date.now) {
|
||||
self.name = name
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
41
WorkoutsPlus/Exercise/AddExercise.swift
Normal file
41
WorkoutsPlus/Exercise/AddExercise.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// AddExercise.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 18.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AddExercise: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Bindable var exercise: Exercise
|
||||
|
||||
var body : some View {
|
||||
Form {
|
||||
TextField("Workout Name", text: $exercise.name)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
modelContext.delete(exercise)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
Color.clear
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
AddExercise(exercise: Exercise(""))
|
||||
}
|
||||
}
|
||||
30
WorkoutsPlus/Exercise/Exercise.swift
Normal file
30
WorkoutsPlus/Exercise/Exercise.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Item.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Exercise {
|
||||
var name: String
|
||||
static var systemImage = "figure.run"
|
||||
|
||||
var timestamp: Date
|
||||
|
||||
init(_ name: String = "", timestamp: Date = Date.now) {
|
||||
self.name = name
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
static let sampleData = [
|
||||
Exercise("Pull-up"),
|
||||
Exercise("Push-up"),
|
||||
Exercise("Dips"),
|
||||
Exercise("Rows"),
|
||||
Exercise("Split Squat")
|
||||
]
|
||||
}
|
||||
45
WorkoutsPlus/Exercise/ExerciseDetail.swift
Normal file
45
WorkoutsPlus/Exercise/ExerciseDetail.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// ExerciseDetailsView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ExerciseDetail: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Bindable var exercise: Exercise
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
TextField("Exercise Name", text: $exercise.name)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Save") {
|
||||
saveItem()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Exercise Details")
|
||||
}
|
||||
|
||||
private func saveItem() {
|
||||
if modelContext.hasChanges {
|
||||
do {
|
||||
try modelContext.save()
|
||||
} catch {
|
||||
print("Failed to save exercise: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ExerciseDetail(exercise: Exercise("New Exercise"))
|
||||
}
|
||||
96
WorkoutsPlus/Exercise/ExerciseLibrary.swift
Normal file
96
WorkoutsPlus/Exercise/ExerciseLibrary.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// ExerciseLibraryView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ExerciseLibrary: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query(sort: \Exercise.name) private var exercises: [Exercise]
|
||||
|
||||
@State private var newExercise: Exercise?
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
Group {
|
||||
if !exercises.isEmpty {
|
||||
List {
|
||||
ForEach(exercises) { exercise in
|
||||
NavigationLink {
|
||||
ExerciseDetail(exercise: exercise)
|
||||
} label: {
|
||||
Text(exercise.name)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteExercise)
|
||||
}
|
||||
} else {
|
||||
ContentUnavailableView {
|
||||
Label("No Exercises", systemImage: Exercise.systemImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Exercises")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
EditButton()
|
||||
}
|
||||
ToolbarItem {
|
||||
Button(action: addExercise) {
|
||||
Label("Add Exercise", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $newExercise) { exercise in
|
||||
NavigationStack {
|
||||
AddExercise(exercise: exercise)
|
||||
}
|
||||
// TODO: It's possible to add a boolean here ("Terms accepted y/n"). Maybe add this for empty string
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
} detail: {
|
||||
// TODO: What does this Detail do?
|
||||
Text("Select a workout")
|
||||
.navigationTitle("Movie")
|
||||
}
|
||||
}
|
||||
|
||||
private func addExercise() {
|
||||
withAnimation {
|
||||
let item = Exercise("")
|
||||
modelContext.insert(item)
|
||||
newExercise = item
|
||||
}
|
||||
}
|
||||
|
||||
private func saveExercise(exercise: Exercise) {
|
||||
if !exercise.name.isEmpty {
|
||||
modelContext.insert(exercise)
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteExercise(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(exercises[index])
|
||||
}
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("With Sample Data") {
|
||||
ExerciseLibrary()
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
|
||||
#Preview("Empty Database") {
|
||||
ExerciseLibrary()
|
||||
.modelContainer(for: Exercise.self, inMemory: true)
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
//
|
||||
// ExerciseDetailsView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ExerciseDetailsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
var exercise: Exercise?
|
||||
var isPresentedAsSheet: Bool = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
TextField("Exercise Name", text: Binding(
|
||||
get: { exercise?.name ?? "" },
|
||||
set: { newName in
|
||||
if exercise != nil {
|
||||
exercise?.name = newName
|
||||
}
|
||||
}
|
||||
))
|
||||
.toolbar {
|
||||
if (isPresentedAsSheet) {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Save") {
|
||||
saveItem()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveItem() {
|
||||
if modelContext.hasChanges {
|
||||
do {
|
||||
try modelContext.save()
|
||||
} catch {
|
||||
print("Failed to save exercise: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ExerciseDetailsView(exercise: Exercise(name: "New Exercises Preview"))
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
//
|
||||
// ExerciseLibraryView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ExerciseLibraryView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query private var exercises: [Exercise]
|
||||
|
||||
@State private var isPresentingNewItemSheet = false
|
||||
|
||||
let initialDataSet = [
|
||||
"Pull-up",
|
||||
"Push-up",
|
||||
"Dips",
|
||||
"Rows",
|
||||
"Split Squat"
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
ForEach(exercises.sorted(by: { $0.name < $1.name })) { item in
|
||||
NavigationLink(destination: ExerciseDetailsView(exercise: item)) {
|
||||
Text(item.name)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteExercise)
|
||||
}
|
||||
.onAppear {
|
||||
if exercises.isEmpty {
|
||||
loadInitialData(exercises: initialDataSet)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
createNewExercise()
|
||||
} label: {
|
||||
Label("Add Exercise", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isPresentingNewItemSheet) {
|
||||
let newItem = Exercise(name: "")
|
||||
NavigationView {
|
||||
ExerciseDetailsView(exercise: newItem, isPresentedAsSheet: true)
|
||||
.onDisappear {
|
||||
if !newItem.name.isEmpty {
|
||||
saveExercise(item: newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createNewExercise() {
|
||||
isPresentingNewItemSheet = true
|
||||
}
|
||||
|
||||
private func saveExercise(item: Exercise) {
|
||||
modelContext.insert(item)
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
private func deleteExercise(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(exercises[index])
|
||||
}
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadInitialData(exercises: [String]) {
|
||||
var items: [Exercise] = []
|
||||
|
||||
for exercise in exercises {
|
||||
let item = Exercise(name: exercise)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
for item in items {
|
||||
modelContext.insert(item)
|
||||
}
|
||||
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ExerciseLibraryView()
|
||||
.modelContainer(for: Exercise.self, inMemory: true)
|
||||
}
|
||||
48
WorkoutsPlus/SampleData.swift
Normal file
48
WorkoutsPlus/SampleData.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// SampleData.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 17.08.24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@MainActor // With your annotation, you’re declaring that all code in this class must run on the main actor, including access to the mainContext property. Since all the SwiftUI code in an app runs on the main actor by default, you’ve satisfied the condition.
|
||||
class SampleData {
|
||||
static let shared = SampleData()
|
||||
|
||||
let modelContainer: ModelContainer
|
||||
|
||||
var context: ModelContext {
|
||||
modelContainer.mainContext
|
||||
}
|
||||
|
||||
private init() {
|
||||
let schema = Schema([Exercise.self, Workout.self])
|
||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
||||
|
||||
do {
|
||||
modelContainer = try ModelContainer(for: schema, configurations: [modelConfiguration])
|
||||
insertSampleData()
|
||||
} catch {
|
||||
fatalError("Could not create ModelContainer: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func insertSampleData() {
|
||||
for exercise in Exercise.sampleData {
|
||||
context.insert(exercise)
|
||||
}
|
||||
|
||||
for workout in Workout.sampleData {
|
||||
context.insert(workout)
|
||||
}
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
print("Sample data context failed to save.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
//
|
||||
// Item.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Item {
|
||||
var name: String
|
||||
var timestamp: Date
|
||||
|
||||
init(name: String = "", timestamp: Date = Date.now) {
|
||||
self.name = name
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
//
|
||||
// ExerciseDetailsView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ItemDetailsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
var item: Item?
|
||||
var isPresentedAsSheet: Bool = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
TextField("Exercise Name", text: Binding(
|
||||
get: { item?.name ?? "" },
|
||||
set: { newName in
|
||||
if item != nil {
|
||||
item?.name = newName
|
||||
}
|
||||
}
|
||||
))
|
||||
.toolbar {
|
||||
if (isPresentedAsSheet) {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Save") {
|
||||
saveItem()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveItem() {
|
||||
if modelContext.hasChanges {
|
||||
do {
|
||||
try modelContext.save()
|
||||
} catch {
|
||||
print("Failed to save item: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let sampleItem = Item(name: "Sample Item")
|
||||
ItemDetailsView(item: sampleItem)
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ItemLibrary: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query private var items: [Item]
|
||||
|
||||
@State private var isPresentingNewItemSheet = false
|
||||
|
||||
let initialDataSet = [
|
||||
"Pull-up",
|
||||
"Push-up",
|
||||
"Dips",
|
||||
"Rows",
|
||||
"Split Squat"
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
ForEach(items.sorted(by: { $0.name < $1.name })) { item in
|
||||
NavigationLink(destination: ItemDetailsView(item: item)) {
|
||||
Text(item.name)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteItem)
|
||||
}
|
||||
.onAppear {
|
||||
if items.isEmpty {
|
||||
loadInitialData(exercises: initialDataSet)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
createNewItem()
|
||||
} label: {
|
||||
Label("Add Exercise", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isPresentingNewItemSheet) {
|
||||
let newItem = Item(name: "")
|
||||
NavigationView {
|
||||
ItemDetailsView(item: newItem, isPresentedAsSheet: true)
|
||||
.onDisappear {
|
||||
if !newItem.name.isEmpty {
|
||||
saveItem(item: newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createNewItem() {
|
||||
isPresentingNewItemSheet = true
|
||||
}
|
||||
|
||||
private func saveItem(item: Item) {
|
||||
modelContext.insert(item)
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
private func deleteItem(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(items[index])
|
||||
}
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadInitialData(exercises: [String]) {
|
||||
var items: [Item] = []
|
||||
|
||||
for exercise in exercises {
|
||||
let item = Item(name: exercise)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
for item in items {
|
||||
modelContext.insert(item)
|
||||
}
|
||||
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ItemLibrary()
|
||||
.modelContainer(for: Item.self, inMemory: true)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// Item.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Workout {
|
||||
var name: String
|
||||
var timestamp: Date
|
||||
|
||||
private var exercises: [Exercise] = []
|
||||
|
||||
init(name: String = "", timestamp: Date = Date.now) {
|
||||
self.name = name
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
func addExercise(_ exercise: Exercise) {
|
||||
exercises.append(exercise)
|
||||
}
|
||||
}
|
||||
41
WorkoutsPlus/Workout/AddWorkout.swift
Normal file
41
WorkoutsPlus/Workout/AddWorkout.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// AddWorkoutView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 17.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AddWorkout: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Bindable var workout: Workout
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
TextField("Workout Name", text: $workout.name)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
modelContext.delete(workout)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
Color.clear
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
AddWorkout(workout: Workout(name: ""))
|
||||
}
|
||||
}
|
||||
63
WorkoutsPlus/Workout/Workout.swift
Normal file
63
WorkoutsPlus/Workout/Workout.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Item.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Workout {
|
||||
var isEdited = false
|
||||
|
||||
var name: String {
|
||||
didSet { isEdited = true }
|
||||
}
|
||||
static var systemImage = "figure.run.square.stack"
|
||||
|
||||
// Other properties and methods
|
||||
var timestamp: Date
|
||||
|
||||
var exercises: [Exercise] = []
|
||||
|
||||
init(name: String = "", timestamp: Date = Date.now) {
|
||||
self.name = name
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
private init(name: String, exercises: [Exercise]) {
|
||||
self.name = name
|
||||
self.timestamp = Date.now
|
||||
self.exercises = exercises
|
||||
}
|
||||
|
||||
func addExercise(_ exercise: Exercise) {
|
||||
exercises.append(exercise)
|
||||
}
|
||||
|
||||
static let sampleData = [
|
||||
Workout(name: "RR", exercises: [
|
||||
Exercise("Warm-up"),
|
||||
Exercise("Pull-up Progression"), Exercise("Squat Progression"),
|
||||
Exercise("Dip Progression"), Exercise("Hinge Progression"),
|
||||
Exercise("Row Progression"), Exercise("Push-up Progression"),
|
||||
Exercise("Core Trilet")]),
|
||||
Workout(name: "Minimalist", exercises:[
|
||||
Exercise("Push"), Exercise("Pull"),
|
||||
Exercise("Legs"), Exercise("Core")]),
|
||||
Workout(name: "Rings", exercises: [
|
||||
Exercise("Dips"), Exercise("Chin-ups"),
|
||||
Exercise("Push-ups"), Exercise("Inverted Rows"),
|
||||
Exercise("Hanging Knee Raises"), Exercise("Pistol Squats"), Exercise("Hanging Leg Curls"),
|
||||
Exercise("Sissy Squats")]),
|
||||
Workout(name: "Intervalltraining", exercises: [
|
||||
Exercise("400 m schnell"), Exercise("200 m langsam"),
|
||||
Exercise("400 m schnell"), Exercise("200 m langsam"),
|
||||
Exercise("400 m schnell"), Exercise("200 m langsam"),
|
||||
Exercise("400 m schnell"), Exercise("200 m langsam"),
|
||||
Exercise("400 m schnell"), Exercise("200 m langsam"),
|
||||
Exercise("400 m schnell"), Exercise("200 m langsam")])
|
||||
]
|
||||
}
|
||||
57
WorkoutsPlus/Workout/WorkoutDetail.swift
Normal file
57
WorkoutsPlus/Workout/WorkoutDetail.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// WorkoutDetailsView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct WorkoutDetail: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Bindable var workout: Workout
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
Section(header: Text("Workout Name")) {
|
||||
TextField("Workout Name", text: $workout.name)
|
||||
}
|
||||
Section(header: Text("Exercises")) {
|
||||
List {
|
||||
ForEach(workout.exercises) { exercise in
|
||||
Text(exercise.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Edit \(workout.name)")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveWorkout() {
|
||||
if modelContext.hasChanges {
|
||||
do {
|
||||
try modelContext.save()
|
||||
} catch {
|
||||
print("Failed to save workout: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
WorkoutDetail(workout: Workout.sampleData[0])
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
}
|
||||
96
WorkoutsPlus/Workout/WorkoutLibrary.swift
Normal file
96
WorkoutsPlus/Workout/WorkoutLibrary.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// WorkoutLibraryView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct WorkoutLibrary: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query(sort: \Workout.name) private var workouts: [Workout]
|
||||
|
||||
@State private var newWorkout: Workout?
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
Group {
|
||||
if !workouts.isEmpty {
|
||||
List {
|
||||
ForEach(workouts) { workout in
|
||||
NavigationLink {
|
||||
WorkoutDetail(workout: workout)
|
||||
} label: {
|
||||
Text(workout.name)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteWorkout)
|
||||
}
|
||||
} else {
|
||||
ContentUnavailableView {
|
||||
Label("No Workouts", systemImage: "figure.run.square.stack")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Workouts")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
EditButton()
|
||||
}
|
||||
ToolbarItem {
|
||||
Button(action: addWorkout) {
|
||||
Label("Add Workout", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $newWorkout) { workout in
|
||||
NavigationStack {
|
||||
AddWorkout(workout: workout)
|
||||
}
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
} detail: {
|
||||
// TODO: What does this Detail do?
|
||||
Text("Select a workout")
|
||||
.navigationTitle("Movie")
|
||||
}
|
||||
}
|
||||
|
||||
private func addWorkout() {
|
||||
withAnimation {
|
||||
let item = Workout(name: "")
|
||||
modelContext.insert(item)
|
||||
newWorkout = item
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Brauchen wir das?
|
||||
private func saveWorkout(workout: Workout) {
|
||||
if !workout.name.isEmpty {
|
||||
modelContext.insert(workout)
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteWorkout(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(workouts[index])
|
||||
}
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("With Sample Data") {
|
||||
WorkoutLibrary()
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
|
||||
#Preview("Empty Database") {
|
||||
WorkoutLibrary()
|
||||
.modelContainer(for: Workout.self, inMemory: true)
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
//
|
||||
// ExerciseDetailsView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WorkoutDetailsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
var workout: Workout?
|
||||
var isPresentedAsSheet: Bool = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
TextField("Workout Name", text: Binding(
|
||||
get: { workout?.name ?? "" },
|
||||
set: { newName in
|
||||
if workout != nil {
|
||||
workout?.name = newName
|
||||
}
|
||||
}
|
||||
))
|
||||
.toolbar {
|
||||
if (isPresentedAsSheet) {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Save") {
|
||||
saveWorkout()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveWorkout() {
|
||||
if modelContext.hasChanges {
|
||||
do {
|
||||
try modelContext.save()
|
||||
} catch {
|
||||
print("Failed to save workout: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
WorkoutDetailsView(workout: Workout(name: ""))
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
//
|
||||
// WorkoutLibraryView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct WorkoutLibraryView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query private var workouts: [Workout]
|
||||
|
||||
@State private var isPresentingNewItemSheet = false
|
||||
|
||||
let initialDataSet = [
|
||||
"RR",
|
||||
"Minimalist"
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
ForEach(workouts.sorted(by: { $0.name < $1.name })) { workout in
|
||||
NavigationLink(destination: WorkoutDetailsView(workout: workout)) {
|
||||
Text(workout.name)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteWorkout)
|
||||
}
|
||||
.onAppear {
|
||||
if workouts.isEmpty {
|
||||
// TODO: This behaviour puts something into [workouts] whenever(!) it is empty. It's not bound to the first ever start of the app or anything. Check if that's the behaviour we want.
|
||||
loadInitialData(workouts: initialDataSet)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
createNewWorkout()
|
||||
} label: {
|
||||
Label("Add Workout", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isPresentingNewItemSheet) {
|
||||
let newWorkout = Workout(name: "")
|
||||
NavigationView {
|
||||
WorkoutDetailsView(workout: newWorkout, isPresentedAsSheet: true)
|
||||
.onDisappear {
|
||||
if !newWorkout.name.isEmpty {
|
||||
saveWorkout(workout: newWorkout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createNewWorkout() {
|
||||
isPresentingNewItemSheet = true
|
||||
}
|
||||
|
||||
private func saveWorkout(workout: Workout) {
|
||||
modelContext.insert(workout)
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
private func deleteWorkout(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(workouts[index])
|
||||
}
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadInitialData(workouts: [String]) {
|
||||
var items: [Workout] = []
|
||||
|
||||
for exercise in workouts {
|
||||
let item = Workout(name: exercise)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
for item in items {
|
||||
modelContext.insert(item)
|
||||
}
|
||||
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
WorkoutLibraryView()
|
||||
.modelContainer(for: Workout.self, inMemory: true)
|
||||
}
|
||||
Reference in New Issue
Block a user