add ActiveWorkoutSession logic, refactor Home, add additional sample data, add isDebug
This commit is contained in:
@@ -1,60 +1,158 @@
|
||||
//
|
||||
// ActiveWorkout.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 07.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ActiveWorkoutSession: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@State var workout: Workout
|
||||
@Default(\.isWorkingOut) var isWorkingOut
|
||||
|
||||
@State var workoutSession: WorkoutSession?
|
||||
@State var currentExercise: Int = 0
|
||||
@Query(sort: \WorkoutSession.name) private var workoutSessions: [WorkoutSession]
|
||||
@State private var activeWorkoutSession: WorkoutSession?
|
||||
@Default(\.activeWorkoutSessionId) var activeWorkoutSessionId
|
||||
|
||||
let startDate = Date() - 100
|
||||
@Query(sort: \Workout.name) private var workouts: [Workout]
|
||||
@State private var activeWorkout: Workout?
|
||||
@Default(\.activeWorkoutId) var activeWorkoutId
|
||||
|
||||
@State private var currentTime = Date()
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
Button("Close") { dismiss() }
|
||||
Text("\(workout.name)")
|
||||
Text("Elapsed Time: \(elapsedTimeText(startDate: startDate))")
|
||||
.onReceive(timer) { time in
|
||||
currentTime = time // Updates the current time every second
|
||||
}
|
||||
var body: some View {
|
||||
VStack {
|
||||
List {
|
||||
// TODO: Unwrap Sets
|
||||
ForEach(workout.workoutItems) { workoutItem in
|
||||
HStack {
|
||||
Text("\(workoutItem.reps)")
|
||||
Text("\(workoutItem.name)")
|
||||
Section(footer: Text(activeWorkoutSession?.creationDate.ISO8601Format() ?? "Unknown Date")) {
|
||||
NavigationLink(destination: {
|
||||
ItemPicker<Workout>(items: workouts, selectedItem: $activeWorkout)
|
||||
}) {
|
||||
Text(activeWorkout?.name ?? "Select your next Workout")
|
||||
}
|
||||
.onChange(of: activeWorkout) { _, newWorkout in
|
||||
if let workout = newWorkout {
|
||||
activeWorkoutId = workout.id.uuidString
|
||||
activeWorkoutSession?.workout = workout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let activeWorkout = activeWorkout {
|
||||
Section(header: Text("Exercises")) {
|
||||
ForEach(activeWorkout.workoutItems.sorted(by: { $0.position < $1.position })) { workoutItem in
|
||||
HStack {
|
||||
Text(String(workoutItem.reps))
|
||||
Text(workoutItem.name)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
// TODO: Implement a sheet view; don't use ExerciseDetail since its purpose is editing
|
||||
}) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
Image(systemName: "play.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.font(.title)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, .green)
|
||||
|
||||
}
|
||||
|
||||
if true { // This condition should be more meaningful.
|
||||
VStack {
|
||||
HStack {
|
||||
Text(String(workoutSessions.count))
|
||||
Button(action: {
|
||||
// save(workoutSession: activeWorkoutSession)
|
||||
}) {
|
||||
Text("Save Session")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -- Workout Controls
|
||||
Group {
|
||||
if activeWorkoutSession?.workout != nil {
|
||||
if isWorkingOut {
|
||||
// MARK: -- Stop Workout
|
||||
VStack {
|
||||
ProgressView("", value: 10, total: 100)
|
||||
TimerView(isActive: $isWorkingOut)
|
||||
.font(.title)
|
||||
.bold()
|
||||
Button(action: {
|
||||
isWorkingOut = false
|
||||
activeWorkoutSession?.startWorkoutSession()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "stop.fill")
|
||||
Text("Stop Workout")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.bold()
|
||||
.tint(.red)
|
||||
}
|
||||
} else {
|
||||
// MARK: -- Start Workout
|
||||
Button(action: {
|
||||
isWorkingOut = true
|
||||
activeWorkoutSession?.stopWorkoutSession()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "play.fill")
|
||||
Text("Start Workout")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.bold()
|
||||
.tint(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Workout Session")
|
||||
.onAppear {
|
||||
// Load the active workout session and workout onAppear
|
||||
if let activeWorkoutSession = getItem(from: workoutSessions, by: activeWorkoutSessionId) {
|
||||
self.activeWorkoutSession = activeWorkoutSession
|
||||
if let workout = getItem(from: workouts, by: activeWorkoutId) {
|
||||
self.activeWorkout = workout
|
||||
}
|
||||
} else {
|
||||
createNewWorkoutSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func elapsedTimeText(startDate: Date) -> String {
|
||||
let elapsedTime = currentTime.timeIntervalSince(startDate)
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.hour, .minute, .second]
|
||||
formatter.unitsStyle = .positional // For HH:mm:ss format
|
||||
return formatter.string(from: elapsedTime) ?? "00:00:00"
|
||||
private func createNewWorkoutSession() {
|
||||
let newWorkoutSession = WorkoutSession()
|
||||
activeWorkoutSessionId = newWorkoutSession.id.uuidString
|
||||
if let selectedWorkout = getItem(from: workouts, by: activeWorkoutId) {
|
||||
newWorkoutSession.workout = selectedWorkout
|
||||
}
|
||||
self.activeWorkoutSession = newWorkoutSession
|
||||
}
|
||||
|
||||
private func getItem<Item: Nameable>(from array: [Item], by id: String) -> Item? {
|
||||
let filteredItems = array.filter { $0.id == UUID(uuidString: id) }
|
||||
return filteredItems.count == 1 ? filteredItems.first : nil
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ActiveWorkoutSession(workout: Workout.sampleData)
|
||||
NavigationStack {
|
||||
ActiveWorkoutSession()
|
||||
}
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
|
||||
#Preview("No Workout Selected") {
|
||||
NavigationStack {
|
||||
ActiveWorkoutSession()
|
||||
}
|
||||
.onAppear {
|
||||
Defaults.shared.isWorkingOut = false
|
||||
}
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
|
||||
#Preview("No Workout Data available") {
|
||||
NavigationStack {
|
||||
ActiveWorkoutSession()
|
||||
}
|
||||
}
|
||||
|
||||
60
WorkoutsPlus/Components/ItemPicker.swift
Normal file
60
WorkoutsPlus/Components/ItemPicker.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// ItemPicker.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 10.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ItemPicker<Item: Nameable>: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var searchText = ""
|
||||
var filteredItems: [Item] {
|
||||
if searchText.isEmpty {
|
||||
return items
|
||||
} else {
|
||||
return items.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
}
|
||||
|
||||
var items: [Item]
|
||||
@Binding var selectedItem: Item?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(filteredItems) { item in
|
||||
HStack {
|
||||
Text(item.name)
|
||||
Spacer()
|
||||
if item == selectedItem {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
// This .contentShape makes the whole row tappable
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
selectedItem = item
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchText)
|
||||
.overlay {
|
||||
if filteredItems.isEmpty {
|
||||
ContentUnavailableView.search(text: searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@Previewable @State var selectedWorkout: Workout? = nil
|
||||
NavigationStack {
|
||||
ItemPicker<Workout>(items: Workout.sampleData, selectedItem: $selectedWorkout)
|
||||
}
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
32
WorkoutsPlus/Components/TimerView.swift
Normal file
32
WorkoutsPlus/Components/TimerView.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// TimerView.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 12.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TimerView: View {
|
||||
@Binding var isActive: Bool
|
||||
@State private var time = 0
|
||||
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
Text("\(time)")
|
||||
.onReceive(timer) { _ in
|
||||
if isActive {
|
||||
self.time += 1
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
self.timer.upstream.connect().cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
TimerView(isActive: .constant(true))
|
||||
}
|
||||
@@ -9,15 +9,21 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public class Defaults: ObservableObject {
|
||||
@AppStorage("isDebug") public var isDebug = true
|
||||
|
||||
@AppStorage("isFirstAppStart") public var isFirstAppStart = true
|
||||
@AppStorage("isOnboarding") public var isOnboarding = true
|
||||
@AppStorage("isTrainerMode") public var isTrainerMode = false
|
||||
@AppStorage("isWorkingOut") public var isWorkingOut = false
|
||||
|
||||
@AppStorage("userId") public var userId = UUID().uuidString
|
||||
@AppStorage("defaultWorkoutId") public var defaultWorkoutId: String = ""
|
||||
|
||||
@AppStorage("sets") public var sets = 8
|
||||
@AppStorage("reps") public var reps = 8
|
||||
|
||||
@AppStorage("isWorkingOut") public var isWorkingOut = false
|
||||
@AppStorage("activeWorkoutId") public var activeWorkoutId: String?
|
||||
@AppStorage("activeWorkoutSessionId") public var activeWorkoutSessionId: String = ""
|
||||
@AppStorage("activeWorkoutId") public var activeWorkoutId: String = ""
|
||||
|
||||
public static let shared = Defaults()
|
||||
}
|
||||
|
||||
@@ -10,50 +10,113 @@ import SwiftData
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@Default(\.isOnboarding) var isOnboarding
|
||||
@Default(\.isWorkingOut) var isWorkingOut
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottom) {
|
||||
TabView {
|
||||
WorkoutLog()
|
||||
// Text("Training Plans")
|
||||
// .tabItem {
|
||||
// Image(systemName: "calendar.badge.clock")
|
||||
// Text("Plans & Goals")
|
||||
// }
|
||||
WorkoutLibrary()
|
||||
.tabItem {
|
||||
Image(systemName: "figure.run.square.stack")
|
||||
Text("Workouts")
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 50, height: 50)
|
||||
.padding(.horizontal)
|
||||
.foregroundStyle(.blue)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Felix Förtsch")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
Text("You got this!")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
// TODO: Food Log (Foto + Kalender + Macros
|
||||
// TODO: Goal page
|
||||
// ActivityLog()
|
||||
// .frame(height: 200)
|
||||
}
|
||||
ExerciseLibrary()
|
||||
.tabItem {
|
||||
Image(systemName: "figure.run")
|
||||
Text("Exercises")
|
||||
}
|
||||
Section {
|
||||
NavigationLink(destination: ActiveWorkoutSession()) {
|
||||
if (isWorkingOut) {
|
||||
HStack {
|
||||
Label("Back to Session", systemImage: "memories")
|
||||
.symbolEffect(.rotate)
|
||||
Text("35 min")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} else {
|
||||
Label("Start Workout Session", systemImage: "play")
|
||||
}
|
||||
}
|
||||
DebugList()
|
||||
.tabItem {
|
||||
Image(systemName: "hammer")
|
||||
Text("Debug")
|
||||
NavigationLink(destination: WorkoutLog()) {
|
||||
Label("Workout Log", systemImage: "calendar.badge.clock")
|
||||
}
|
||||
Settings()
|
||||
.tabItem {
|
||||
Image(systemName: "gear")
|
||||
Text("Settings")
|
||||
}
|
||||
Section {
|
||||
NavigationLink(destination: WorkoutLibrary()) {
|
||||
Label("Workouts", systemImage: "figure.run.square.stack")
|
||||
}
|
||||
|
||||
NavigationLink(destination: ExerciseLibrary()) {
|
||||
Label("Exercises", systemImage: "figure.run")
|
||||
}
|
||||
}
|
||||
Section {
|
||||
|
||||
NavigationLink(destination: Settings()) {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
Section(header: Text("Debug")) {
|
||||
NavigationLink(destination: DebugList()) {
|
||||
Label("Debug", systemImage: "hammer")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isOnboarding) { Onboarding() }
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
if true {
|
||||
MiniPlayer()
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
HStack {
|
||||
ZStack {
|
||||
Image(systemName: "hexagon.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 35, height: 35)
|
||||
.foregroundStyle(systemColors.randomElement()!)
|
||||
Image(systemName: fitnessIcons.randomElement()!)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
Text("Workout+")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.sheet(isPresented: $isOnboarding) { Onboarding() }
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
#Preview("No Workout Session") {
|
||||
ContentView()
|
||||
.onAppear {
|
||||
Defaults.shared.isWorkingOut = false
|
||||
}
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
|
||||
#Preview("With Active Workout Session") {
|
||||
ContentView()
|
||||
.onAppear {
|
||||
Defaults.shared.isWorkingOut = true
|
||||
}
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ struct DebugList: View {
|
||||
@Query(sort: \Exercise.name) private var exercises: [Exercise]
|
||||
@Query(sort: \Workout.name) private var workouts: [Workout]
|
||||
@Query(sort: \WorkoutItem.name) private var workoutItems: [WorkoutItem]
|
||||
@Query(sort: \WorkoutSession.name) private var workoutSessions: [WorkoutSession]
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
@@ -36,7 +37,13 @@ struct DebugList: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(header: Text("Workout Sessions")) {
|
||||
ForEach(workoutSessions) { workoutSession in
|
||||
DebugListItem(item: workoutSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Debug")
|
||||
.tabItem {
|
||||
Image(systemName: "wrench")
|
||||
Text("Exercise Debug")
|
||||
|
||||
@@ -15,7 +15,7 @@ struct ExampleView: View {
|
||||
@State private var showPopoverSource: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
List {
|
||||
NavigationLink(destination: Text("Detail View")) {
|
||||
Text("NavigationLink")
|
||||
|
||||
@@ -19,7 +19,7 @@ class SampleData {
|
||||
}
|
||||
|
||||
private init() {
|
||||
let schema = Schema([WorkoutItem.self, Exercise.self, Workout.self])
|
||||
let schema = WorkoutsPlusApp.swiftDataSchema
|
||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
||||
|
||||
do {
|
||||
@@ -31,15 +31,25 @@ class SampleData {
|
||||
}
|
||||
|
||||
func insertSampleData() {
|
||||
for exercise in Exercise.sampleData {
|
||||
for exercise in Exercise.sampleDataRecommendedRoutine {
|
||||
context.insert(exercise)
|
||||
}
|
||||
|
||||
for workoutItem in WorkoutItem.sampleData {
|
||||
for exercise in Exercise.sampleDataRings {
|
||||
context.insert(exercise)
|
||||
}
|
||||
|
||||
for workoutItem in WorkoutItem.sampleDataRecommendedRoutine {
|
||||
context.insert(workoutItem)
|
||||
}
|
||||
|
||||
context.insert(Workout.sampleData)
|
||||
for workoutItem in WorkoutItem.sampleDataRings {
|
||||
context.insert(workoutItem)
|
||||
}
|
||||
|
||||
for workout in Workout.sampleData {
|
||||
context.insert(workout)
|
||||
}
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
|
||||
@@ -40,5 +40,7 @@ struct ExerciseDetail: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ExerciseDetail(exercise: Exercise.sampleData.first!)
|
||||
NavigationStack {
|
||||
ExerciseDetail(exercise: Exercise.sampleDataRecommendedRoutine.first!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,39 +29,39 @@ struct ExerciseLibrary: View {
|
||||
// TODO: Add search bar to the top
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
List {
|
||||
ForEach(filteredItems) { exercise in
|
||||
NavigationLink {
|
||||
ExerciseDetail(exercise: exercise)
|
||||
} label: {
|
||||
Text(exercise.name)
|
||||
}
|
||||
|
||||
Group {
|
||||
List {
|
||||
ForEach(filteredItems) { exercise in
|
||||
NavigationLink {
|
||||
ExerciseDetail(exercise: exercise)
|
||||
} label: {
|
||||
Text(exercise.name)
|
||||
}
|
||||
.onDelete(perform: deleteExercise)
|
||||
if isAddingExercise {
|
||||
TextField("New Exercise", text: $newExerciseName, onCommit: {
|
||||
newExercise.name = newExerciseName
|
||||
save(exercise: newExercise)
|
||||
isAddingExercise = false
|
||||
})
|
||||
.textInputAutocapitalization(.words)
|
||||
.focused($isInputFieldFocused)
|
||||
}
|
||||
AddItemButton(label: "Exercise", action: addExercise)
|
||||
}
|
||||
.searchable(text: $searchText)
|
||||
.onDelete(perform: deleteExercise)
|
||||
if isAddingExercise {
|
||||
TextField("New Exercise", text: $newExerciseName, onCommit: {
|
||||
newExercise.name = newExerciseName
|
||||
save(exercise: newExercise)
|
||||
isAddingExercise = false
|
||||
})
|
||||
.textInputAutocapitalization(.words)
|
||||
.focused($isInputFieldFocused)
|
||||
}
|
||||
AddItemButton(label: "Exercise", action: addExercise)
|
||||
}
|
||||
.navigationBarTitle("Exercises")
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
EditButton()
|
||||
}
|
||||
.searchable(text: $searchText)
|
||||
}
|
||||
.navigationTitle("Exercises")
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func addExercise() {
|
||||
withAnimation {
|
||||
newExercise = Exercise("")
|
||||
@@ -89,12 +89,17 @@ struct ExerciseLibrary: View {
|
||||
}
|
||||
|
||||
#Preview("With Sample Data") {
|
||||
ExerciseLibrary()
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
NavigationStack {
|
||||
ExerciseLibrary()
|
||||
}
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
|
||||
}
|
||||
|
||||
#Preview("Empty Database") {
|
||||
ExerciseLibrary()
|
||||
.modelContainer(for: WorkoutItem.self, inMemory: true)
|
||||
NavigationStack {
|
||||
ExerciseLibrary()
|
||||
}
|
||||
.modelContainer(for: WorkoutItem.self, inMemory: true)
|
||||
}
|
||||
|
||||
|
||||
129
WorkoutsPlus/Home/ActivityLog.swift
Normal file
129
WorkoutsPlus/Home/ActivityLog.swift
Normal file
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// ActivityLog.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 09.09.24.
|
||||
//
|
||||
// https://www.artemnovichkov.com/blog/github-contribution-graph-swift-charts
|
||||
// https://github.com/artemnovichkov/awesome-swift-charts
|
||||
|
||||
import SwiftUI
|
||||
import Charts
|
||||
|
||||
struct ActivityLog: View {
|
||||
@State var activities: [Activity] = Activity.generate()
|
||||
|
||||
var body: some View {
|
||||
Chart(activities) { contribution in
|
||||
RectangleMark(
|
||||
xStart: .value("Start week", contribution.date, unit: .weekOfYear),
|
||||
xEnd: .value("End week", contribution.date, unit: .weekOfYear),
|
||||
yStart: .value("Start weekday", weekday(for: contribution.date)),
|
||||
yEnd: .value("End weekday", weekday(for: contribution.date) + 1)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4).inset(by: 2))
|
||||
.foregroundStyle(by: .value("Count", contribution.count))
|
||||
}
|
||||
.chartPlotStyle { content in
|
||||
content
|
||||
.aspectRatio(aspectRatio, contentMode: .fit)
|
||||
}
|
||||
.chartForegroundStyleScale(range: Gradient(colors: colors))
|
||||
.chartXAxis {
|
||||
AxisMarks(position: .top, values: .stride(by: .month)) {
|
||||
AxisValueLabel(format: .dateTime.month())
|
||||
.foregroundStyle(Color(.label))
|
||||
}
|
||||
}
|
||||
.chartYAxis {
|
||||
AxisMarks(position: .leading, values: [1, 3, 5]) { value in
|
||||
if let value = value.as(Int.self) {
|
||||
AxisValueLabel {
|
||||
// Symbols from Calendar.current starting with Monday
|
||||
// Text(shortWeekdaySymbols[value - 1])
|
||||
}
|
||||
.foregroundStyle(Color(.label))
|
||||
}
|
||||
}
|
||||
}
|
||||
.chartYScale(domain: .automatic(includesZero: false, reversed: true))
|
||||
.chartLegend {
|
||||
HStack(spacing: 4) {
|
||||
Text("Less")
|
||||
ForEach(legendColors, id: \.self) { color in
|
||||
color
|
||||
.frame(width: 10, height: 10)
|
||||
.cornerRadius(2)
|
||||
}
|
||||
Text("More")
|
||||
}
|
||||
.padding(4)
|
||||
.foregroundStyle(Color(.label))
|
||||
.font(.caption2)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private func weekday(for date: Date) -> Int {
|
||||
let weekday = Calendar.current.component(.weekday, from: date)
|
||||
let adjustedWeekday = (weekday == 1) ? 7 : (weekday - 1)
|
||||
return adjustedWeekday
|
||||
}
|
||||
|
||||
private var aspectRatio: Double {
|
||||
if activities.isEmpty {
|
||||
return 1
|
||||
}
|
||||
let firstDate = activities.first!.date
|
||||
let lastDate = activities.last!.date
|
||||
let firstWeek = Calendar.current.component(.weekOfYear, from: firstDate)
|
||||
let lastWeek = Calendar.current.component(.weekOfYear, from: lastDate)
|
||||
return Double(lastWeek - firstWeek + 1) / 7
|
||||
}
|
||||
|
||||
private var colors: [Color] {
|
||||
(0...10).map { index in
|
||||
if index == 0 {
|
||||
return Color(.systemGray5)
|
||||
}
|
||||
return Color(.systemGreen).opacity(Double(index) / 10)
|
||||
}
|
||||
}
|
||||
|
||||
private var legendColors: [Color] {
|
||||
Array(stride(from: 0, to: colors.count, by: 2).map { colors[$0] })
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ActivityLog()
|
||||
}
|
||||
|
||||
struct Activity: Identifiable {
|
||||
|
||||
let date: Date
|
||||
let count: Int
|
||||
|
||||
var id: Date {
|
||||
date
|
||||
}
|
||||
}
|
||||
|
||||
extension Activity {
|
||||
|
||||
static func generate() -> [Activity] {
|
||||
var contributions: [Activity] = []
|
||||
let toDate = Date.now
|
||||
let fromDate = Calendar.current.date(byAdding: .day, value: -60, to: toDate)!
|
||||
|
||||
var currentDate = fromDate
|
||||
while currentDate <= toDate {
|
||||
let contribution = Activity(date: currentDate, count: .random(in: 0...10))
|
||||
contributions.append(contribution)
|
||||
currentDate = Calendar.current.date(byAdding: .day, value: 1, to: currentDate)!
|
||||
}
|
||||
|
||||
return contributions
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,20 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
// Maybe: https://github.com/LeoNatan/LNPopupController/tree/master
|
||||
// TODO:The Miniplayer is the same view that we use for the Live Activity
|
||||
// .safeAreaInset(edge: .bottom) { }
|
||||
struct MiniPlayer: View {
|
||||
@Default(\.isWorkingOut) var isWorkingOut
|
||||
@Default(\.activeWorkoutId) var selectedWorkoutId
|
||||
|
||||
// @Query private var workouts: [Workout]
|
||||
|
||||
@State private var isFullScreenCoverPresented = false
|
||||
@State private var workout = Workout.sampleData
|
||||
@State private var workoutSession: WorkoutSession?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -31,7 +39,7 @@ struct MiniPlayer: View {
|
||||
Button(action: {
|
||||
withAnimation() {
|
||||
// TODO: This button has to do something with the workout (pause it, next exercise, etc)
|
||||
isWorkingOut.toggle()
|
||||
|
||||
}}) {
|
||||
Image(systemName: "pause.circle.fill")
|
||||
.font(.title)
|
||||
@@ -43,7 +51,7 @@ struct MiniPlayer: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.fullScreenCover(isPresented: $isFullScreenCoverPresented) {
|
||||
ActiveWorkoutSession(workout: workout)
|
||||
// ActiveWorkoutSession(workout: workout, workoutSession: workoutSession)
|
||||
}
|
||||
} else {
|
||||
Button(action: {
|
||||
@@ -53,7 +61,7 @@ struct MiniPlayer: View {
|
||||
Text("Start Workout")
|
||||
.font(.headline)
|
||||
// TODO: Replace this with the upcoming/planned workout
|
||||
Text("→ Recommended Routine")
|
||||
Text(selectedWorkoutId)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@@ -61,7 +69,11 @@ struct MiniPlayer: View {
|
||||
Button(action: {
|
||||
withAnimation() {
|
||||
// TODO: This button "quick starts" the workout (skips over the ActiveWorkoutSession fullscreen cover)
|
||||
isWorkingOut.toggle()
|
||||
// if let workoutId = selectedWorkoutId {
|
||||
// If you selectedWorkoutId is set, get the corresponding workout. This idea is generally okay, but feels off (DRY?)
|
||||
// workout = workouts.filter({ $0.id == UUID(uuidString: workoutId) }).first
|
||||
// }
|
||||
|
||||
}}) {
|
||||
Image(systemName: "play.circle.fill")
|
||||
.font(.title)
|
||||
@@ -73,7 +85,7 @@ struct MiniPlayer: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.fullScreenCover(isPresented: $isFullScreenCoverPresented) {
|
||||
ActiveWorkoutSession(workout: workout)
|
||||
// ActiveWorkoutSession(workout: workout, workoutSession: workoutSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +95,7 @@ struct MiniPlayer: View {
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 10)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 65)
|
||||
// .padding(.bottom, 65)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct WorkoutLog: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query(sort: \WorkoutSession.name) private var workoutSessions: [WorkoutSession]
|
||||
|
||||
@Default(\.isWorkingOut) var isWorkingOut
|
||||
|
||||
var body: some View {
|
||||
NavigationView() {
|
||||
List() {
|
||||
List {
|
||||
Section(header: Text("Dummies")) {
|
||||
NavigationLink(destination: Text("WorkoutLogDetails")) {
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "figure.run")
|
||||
@@ -67,22 +71,39 @@ struct WorkoutLog: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Workout Logs")
|
||||
Section(header: Text("Workout Sessions")) {
|
||||
ForEach(workoutSessions) { session in
|
||||
VStack(alignment: .leading) {
|
||||
Text(session.creationDate.ISO8601Format())
|
||||
if let workout = session.workout {
|
||||
Text(workout.name)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}.onDelete(perform: deleteWorkoutSession)
|
||||
}
|
||||
|
||||
}
|
||||
.tabItem {
|
||||
Image(systemName: "pencil.and.list.clipboard")
|
||||
Text("Log")
|
||||
.navigationTitle("Workout Logs")
|
||||
}
|
||||
|
||||
private func deleteWorkoutSession(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(workoutSessions[index])
|
||||
}
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Active WorkoutSession") {
|
||||
WorkoutLog()
|
||||
.onAppear {
|
||||
Defaults.shared.isWorkingOut = true
|
||||
}
|
||||
NavigationStack {
|
||||
WorkoutLog()
|
||||
.onAppear {
|
||||
Defaults.shared.isWorkingOut = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("No Active WorkoutSession") {
|
||||
|
||||
@@ -26,7 +26,22 @@ final class Exercise: Nameable {
|
||||
}
|
||||
|
||||
extension Exercise {
|
||||
static let sampleData: [Exercise] = [
|
||||
static let sampleDataRecommendedRoutine: [Exercise] = [
|
||||
Exercise("Shoulder Band Warm-up"),
|
||||
Exercise("Squat Sky Reaches"),
|
||||
Exercise("GMB Wrist Prep"),
|
||||
Exercise("Dead Bugs"),
|
||||
Exercise("Pull-up Progression"),
|
||||
Exercise("Dip Progression"),
|
||||
Exercise("Squat Progression"),
|
||||
Exercise("Hinge Progression"),
|
||||
Exercise("Row Progression"),
|
||||
Exercise("Push-up Progression"),
|
||||
Exercise("Handstand Practice"),
|
||||
Exercise("Support Practice")
|
||||
]
|
||||
|
||||
static let sampleDataRings: [Exercise] = [
|
||||
Exercise("Dips"),
|
||||
Exercise("Chin-ups"),
|
||||
Exercise("Push-ups"),
|
||||
@@ -34,6 +49,6 @@ extension Exercise {
|
||||
Exercise("Hanging Knee Raises"),
|
||||
Exercise("Pistol Squats"),
|
||||
Exercise("Hanging Leg Curls"),
|
||||
Exercise("Sissy Squats")
|
||||
Exercise("Sissy Squats"),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol Nameable: Identifiable {
|
||||
protocol Nameable: Identifiable, Hashable {
|
||||
var id: UUID { get }
|
||||
var name: String { get }
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Workout: Nameable {
|
||||
final class Workout: Nameable, Hashable {
|
||||
static var systemImage = "figure.run.square.stack"
|
||||
|
||||
var id = UUID()
|
||||
@@ -50,6 +50,8 @@ final class Workout: Nameable {
|
||||
exercise.position = index
|
||||
}
|
||||
}
|
||||
|
||||
func isSelected(workout: Workout) -> Bool { self.id == workout.id }
|
||||
}
|
||||
|
||||
extension Workout {
|
||||
@@ -116,14 +118,18 @@ extension Workout {
|
||||
self.workoutItems = exercises
|
||||
}
|
||||
|
||||
static let sampleData: Workout = {
|
||||
var workout = Workout(name: "Recommended Routine")
|
||||
|
||||
for workoutItem in WorkoutItem.sampleData {
|
||||
workout.add(workoutItem: workoutItem)
|
||||
static let sampleData: [Workout] = {
|
||||
var rr = Workout(name: "Recommended Routine")
|
||||
for workoutItem in WorkoutItem.sampleDataRecommendedRoutine {
|
||||
rr.add(workoutItem: workoutItem)
|
||||
}
|
||||
|
||||
return workout
|
||||
var rings = Workout(name: "Fully Body Rings")
|
||||
for workoutItem in WorkoutItem.sampleDataRings {
|
||||
rings.add(workoutItem: workoutItem)
|
||||
}
|
||||
|
||||
return [rr, rings]
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ final class WorkoutItem: Nameable, Positionable {
|
||||
// The only relevant delete is delete Workout -> Delete Workoutitems
|
||||
// { didSet { self.name = exercise?.name ?? "self.name" } }
|
||||
|
||||
// TODO: Deload items -> Think about how to model deload/sick/rest/holiday week
|
||||
|
||||
init(_ reps: Int, _ exercise: String) {
|
||||
self.workoutItemType = .exercise
|
||||
|
||||
@@ -67,18 +69,28 @@ extension WorkoutItem {
|
||||
}
|
||||
|
||||
extension WorkoutItem {
|
||||
static let sampleData: [WorkoutItem] = {
|
||||
static let sampleDataRecommendedRoutine: [WorkoutItem] = {
|
||||
var exercises = [WorkoutItem]()
|
||||
|
||||
for exercise in Exercise.sampleData {
|
||||
for exercise in Exercise.sampleDataRecommendedRoutine {
|
||||
exercises.append(WorkoutItem(from: exercise))
|
||||
}
|
||||
|
||||
var set = WorkoutItem(workoutItems: [
|
||||
WorkoutItem(from: Exercise("Set item 1")),
|
||||
WorkoutItem(from: Exercise("Set item 2"))
|
||||
])
|
||||
exercises.append(set)
|
||||
// var set = WorkoutItem(workoutItems: [
|
||||
// WorkoutItem(from: Exercise("Set item 1")),
|
||||
// WorkoutItem(from: Exercise("Set item 2"))
|
||||
// ])
|
||||
// exercises.append(set)
|
||||
|
||||
return exercises
|
||||
}()
|
||||
|
||||
static let sampleDataRings: [WorkoutItem] = {
|
||||
var exercises = [WorkoutItem]()
|
||||
|
||||
for exercise in Exercise.sampleDataRings {
|
||||
exercises.append(WorkoutItem(from: exercise))
|
||||
}
|
||||
|
||||
return exercises
|
||||
}()
|
||||
|
||||
@@ -9,27 +9,50 @@ import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class WorkoutSession {
|
||||
var workout: Workout
|
||||
|
||||
// Time
|
||||
var startDate = Date.now
|
||||
var stopDate: Date? = nil
|
||||
var duration: TimeInterval? = nil
|
||||
func stopWorkout() {
|
||||
self.stopDate = Date.now
|
||||
if let stopDate = stopDate {
|
||||
self.duration = stopDate.timeIntervalSince(startDate)
|
||||
final class WorkoutSession: Nameable {
|
||||
var id = UUID()
|
||||
var name = ""
|
||||
var workout: Workout? {
|
||||
didSet {
|
||||
self.name = workout?.name ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
// Time
|
||||
var creationDate = Date.now
|
||||
private var startDate: Date? = nil
|
||||
private var stopDate: Date? = nil
|
||||
private var duration: TimeInterval? = nil
|
||||
|
||||
// Exercise Progress
|
||||
var currentExercise = 0
|
||||
|
||||
func isActive() -> Bool {
|
||||
return startDate != nil && stopDate == nil
|
||||
}
|
||||
|
||||
func startWorkoutSession() {
|
||||
self.startDate = Date.now
|
||||
}
|
||||
|
||||
func stopWorkoutSession() {
|
||||
guard let startDate else { return }
|
||||
self.stopDate = Date.now
|
||||
self.duration = self.stopDate!.timeIntervalSince(startDate)
|
||||
}
|
||||
|
||||
func elapsedTime(since startDate: Date) -> String {
|
||||
let elapsedTime = Date.now.timeIntervalSince(startDate)
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.hour, .minute, .second]
|
||||
formatter.unitsStyle = .positional
|
||||
return formatter.string(from: elapsedTime) ?? "00:00:00"
|
||||
}
|
||||
|
||||
// var isCompleted: Bool
|
||||
// var isPaused: Bool
|
||||
// var isCancelled: Bool
|
||||
// var isDeleted: Bool
|
||||
// var isSynced: Bool
|
||||
init (workout: Workout) {
|
||||
self.workout = workout
|
||||
}
|
||||
init () { }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
// TODO: Inspiration holen von https://github.com/SvenTiigi/WhatsNewKit
|
||||
struct Onboarding: View {
|
||||
@Default(\.isFirstAppStart) var isFirstAppStart
|
||||
@Default(\.isOnboarding) var isOnboarding
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
## Workouts
|
||||
- time-based: 60 s
|
||||
- rep-based: 3x
|
||||
- rep-based: 3x -> TODO: Russian System (333 3333 33333, 444 4444 444444, usw -> https://www.youtube.com/watch?v=GZmtjlPTU1g) abbilden (ob System flexibel genug ist)
|
||||
- Loops
|
||||
|
||||
## Trainingspläne
|
||||
|
||||
@@ -7,30 +7,42 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: Import/Export Workouts/Exercises
|
||||
// https://www.avanderlee.com/swift/json-parsing-decoding/
|
||||
struct Settings: View {
|
||||
@Default(\.isDebug) var isDebug
|
||||
@Default(\.isFirstAppStart) var isFirstAppStart
|
||||
@Default(\.isOnboarding) var isOnboarding
|
||||
@Default(\.isTrainerMode) var isTrainerMode
|
||||
@Default(\.userId) var userId
|
||||
@Default(\.reps) var reps
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("User")) {
|
||||
Section(
|
||||
header: Text("User"),
|
||||
footer: Text("Trainer Mode enables you to add and manage trainees.")) {
|
||||
Text(userId)
|
||||
TextField("name", text: $userId)
|
||||
Toggle(isOn: $isTrainerMode) {
|
||||
Text("Trainer Mode")
|
||||
}
|
||||
}
|
||||
Section(header: Text("Defaults")) {
|
||||
StepperListItem(itemName: "Rep Count", itemValue: $reps)
|
||||
}
|
||||
Text(String(reps))
|
||||
Section(header: Text("Danger Zone")) {
|
||||
|
||||
Toggle(isOn: $isDebug) {
|
||||
Text("isDebug")
|
||||
}
|
||||
Toggle(isOn: $isOnboarding) {
|
||||
Text("isOnboarding")
|
||||
Text("isOnboarding ")
|
||||
}
|
||||
Button("Reset App", role: .destructive, action: resetApp)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
|
||||
private func resetApp() {
|
||||
@@ -40,5 +52,7 @@ struct Settings: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
Settings()
|
||||
NavigationStack {
|
||||
Settings()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,6 @@ struct AddWorkout: View {
|
||||
#Preview {
|
||||
Color.clear
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
AddWorkout(workout: Workout.sampleData)
|
||||
AddWorkout(workout: Workout.sampleData.first!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ struct WorkoutDetail: View {
|
||||
AddItemButton(label: "Exercise", action: presentWorkoutItemLibrarySheet)
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("\(workout.name)")
|
||||
.navigationTitle("\(workout.name)")
|
||||
.toolbar {
|
||||
// TODO: Add proper Sharing for workouts.
|
||||
ToolbarItem() { ShareLink(item: URL(filePath: "felixfoertsch.de")!) }
|
||||
@@ -88,14 +88,14 @@ struct WorkoutDetail: View {
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
WorkoutDetail(workout: Workout.sampleData)
|
||||
WorkoutDetail(workout: Workout.sampleData.first!)
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Debug") {
|
||||
TabView {
|
||||
WorkoutDetail(workout: Workout.sampleData)
|
||||
WorkoutDetail(workout: Workout.sampleData.first!)
|
||||
.tabItem {
|
||||
Image(systemName: "figure.run.square.stack")
|
||||
Text("Workouts")
|
||||
|
||||
@@ -57,14 +57,14 @@ struct WorkoutIconSelector: View {
|
||||
}
|
||||
.overlay {
|
||||
if filteredIcons.isEmpty {
|
||||
ContentUnavailableView.search
|
||||
ContentUnavailableView.search(text: searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationView() {
|
||||
WorkoutIconSelector(workout: Workout.sampleData)
|
||||
NavigationStack() {
|
||||
WorkoutIconSelector(workout: Workout.sampleData.first!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,11 +56,11 @@ struct WorkoutItemLibrarySheet: View {
|
||||
}
|
||||
|
||||
#Preview("With Sample Data") {
|
||||
WorkoutItemLibrarySheet(workout: Workout.sampleData)
|
||||
WorkoutItemLibrarySheet(workout: Workout.sampleData.first!)
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
|
||||
#Preview("Empty Database") {
|
||||
WorkoutItemLibrarySheet(workout: Workout.sampleData)
|
||||
WorkoutItemLibrarySheet(workout: Workout.sampleData.first!)
|
||||
.modelContainer(for: Exercise.self, inMemory: true)
|
||||
}
|
||||
|
||||
@@ -19,15 +19,13 @@ struct WorkoutLibrary: View {
|
||||
|
||||
@State private var searchText: String = ""
|
||||
var filteredItems: [Workout] {
|
||||
if searchText.isEmpty {
|
||||
return workouts
|
||||
} else {
|
||||
return workouts.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
if searchText.isEmpty { return workouts }
|
||||
else { return workouts.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
|
||||
Group {
|
||||
List {
|
||||
ForEach(filteredItems) { workout in
|
||||
@@ -53,13 +51,12 @@ struct WorkoutLibrary: View {
|
||||
}
|
||||
.searchable(text: $searchText)
|
||||
}
|
||||
.navigationBarTitle("Workouts")
|
||||
.navigationTitle("Workouts")
|
||||
.toolbar {
|
||||
ToolbarItem() {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addWorkout() {
|
||||
@@ -71,7 +68,6 @@ struct WorkoutLibrary: View {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Brauchen wir das?
|
||||
private func save(workout: Workout) {
|
||||
withAnimation {
|
||||
newWorkout.name = newWorkoutName
|
||||
@@ -94,12 +90,16 @@ struct WorkoutLibrary: View {
|
||||
}
|
||||
|
||||
#Preview("With Sample Data") {
|
||||
WorkoutLibrary()
|
||||
NavigationStack {
|
||||
WorkoutLibrary()
|
||||
}
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
|
||||
#Preview("Empty Database") {
|
||||
WorkoutLibrary()
|
||||
NavigationStack {
|
||||
WorkoutLibrary()
|
||||
}
|
||||
.modelContainer(for: Workout.self, inMemory: true)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,7 @@ import SwiftData
|
||||
@main
|
||||
struct WorkoutsPlusApp: App {
|
||||
var sharedModelContainer: ModelContainer = {
|
||||
let schema = Schema([
|
||||
WorkoutItem.self,
|
||||
Exercise.self,
|
||||
Workout.self
|
||||
])
|
||||
let schema = WorkoutsPlusApp.swiftDataSchema
|
||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
||||
|
||||
do {
|
||||
@@ -32,3 +28,11 @@ struct WorkoutsPlusApp: App {
|
||||
.modelContainer(sharedModelContainer)
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutsPlusApp {
|
||||
static let swiftDataSchema = Schema([
|
||||
Exercise.self,
|
||||
Workout.self,
|
||||
WorkoutItem.self,
|
||||
WorkoutSession.self])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user