From 02e29370948c1ee68f00b9121b89fb702a4a4e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Wed, 13 Nov 2024 13:59:39 +0100 Subject: [PATCH] add ModelContainerPreview conecept, move SampleData, fix Exercise, ExerciseUnit, ExerciseType --- .../Features/Exercise/AddExercise.swift | 10 ++- WorkoutsPlus/Features/Exercise/Exercise.swift | 29 ------- ...rciseDetail.swift => ExerciseEditor.swift} | 41 ++++++---- .../Features/Exercise/ExerciseLibrary.swift | 10 ++- .../Features/Exercise/ExerciseType.swift | 8 -- .../Features/Exercise/ExerciseUnit.swift | 8 -- .../PreviewHelper/ModelContainerPreview.swift | 78 +++++++++++++++++++ .../SampleData/SampleData+Exercise.swift | 36 +++++++++ .../SampleData/SampleData+ExerciseType.swift | 16 ++++ .../SampleData/SampleData+ExerciseUnit.swift | 16 ++++ .../SampleData.swift | 22 ++++++ 11 files changed, 211 insertions(+), 63 deletions(-) rename WorkoutsPlus/Features/Exercise/{ExerciseDetail.swift => ExerciseEditor.swift} (69%) create mode 100644 WorkoutsPlus/PreviewHelper/ModelContainerPreview.swift create mode 100644 WorkoutsPlus/SampleData/SampleData+Exercise.swift create mode 100644 WorkoutsPlus/SampleData/SampleData+ExerciseType.swift create mode 100644 WorkoutsPlus/SampleData/SampleData+ExerciseUnit.swift rename WorkoutsPlus/{Configuration => SampleData}/SampleData.swift (71%) diff --git a/WorkoutsPlus/Features/Exercise/AddExercise.swift b/WorkoutsPlus/Features/Exercise/AddExercise.swift index 15772d5..b1d8139 100644 --- a/WorkoutsPlus/Features/Exercise/AddExercise.swift +++ b/WorkoutsPlus/Features/Exercise/AddExercise.swift @@ -17,8 +17,8 @@ struct AddExercise: View { @State private var name: String = "" @State private var description: String = "" - @State private var type = ExerciseType("") - @State private var unit = ExerciseUnit("", symbol: "") + @State private var type: ExerciseType? + @State private var unit: ExerciseUnit? @State private var isPartOfProgression: Bool = false @@ -41,12 +41,14 @@ struct AddExercise: View { • Sprint → Time or Distance """)) { Picker("Exercise Type", selection: $type) { + Text("None").tag(nil as ExerciseType?) ForEach(exerciseTypes, id: \.self) { type in Text("\(type.name)").tag(type as ExerciseType?) } } .pickerStyle(NavigationLinkPickerStyle()) Picker("Exercise Unit", selection: $unit) { + Text("None").tag(nil as ExerciseUnit?) ForEach(exerciseUnits, id: \.self) { unit in Text("\(unit.name) (\(unit.symbol))").tag(unit as ExerciseUnit?) } @@ -66,7 +68,7 @@ struct AddExercise: View { ToolbarItem(placement: .confirmationAction) { Button("Save") { withAnimation { - save() + saveNew() dismiss() } } @@ -76,7 +78,7 @@ struct AddExercise: View { } } - private func save() { + private func saveNew() { let exerciseToSave = Exercise(name) modelContext.insert(exerciseToSave) diff --git a/WorkoutsPlus/Features/Exercise/Exercise.swift b/WorkoutsPlus/Features/Exercise/Exercise.swift index 09b89a0..48a4c7c 100644 --- a/WorkoutsPlus/Features/Exercise/Exercise.swift +++ b/WorkoutsPlus/Features/Exercise/Exercise.swift @@ -37,32 +37,3 @@ final class Exercise: Nameable { self.name = name } } - -extension 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"), - Exercise("Inverted Rows"), - Exercise("Hanging Knee Raises"), - Exercise("Pistol Squats"), - Exercise("Hanging Leg Curls"), - Exercise("Sissy Squats") - ] - -} diff --git a/WorkoutsPlus/Features/Exercise/ExerciseDetail.swift b/WorkoutsPlus/Features/Exercise/ExerciseEditor.swift similarity index 69% rename from WorkoutsPlus/Features/Exercise/ExerciseDetail.swift rename to WorkoutsPlus/Features/Exercise/ExerciseEditor.swift index 89ea371..345cff2 100644 --- a/WorkoutsPlus/Features/Exercise/ExerciseDetail.swift +++ b/WorkoutsPlus/Features/Exercise/ExerciseEditor.swift @@ -8,11 +8,13 @@ import SwiftUI import SwiftData -struct ExerciseDetail: View { +struct ExerciseEditor: View { @Environment(\.dismiss) private var dismiss @Environment(\.modelContext) private var modelContext @State var exercise: Exercise + + @State private var name: String = "" @State private var description: String = "" @State private var isPartOfProgression: Bool = false @@ -25,7 +27,7 @@ struct ExerciseDetail: View { var body: some View { Form { Section { - TextField("Exercise Name", text: $exercise.name) + TextField("Exercise Name", text: $name) TextEditorWithPlaceholder(text: $description, placeholder: "Description (optional)") } Section(footer: Text(""" @@ -35,12 +37,14 @@ struct ExerciseDetail: View { • Sprint → Time or Distance """)) { Picker("Exercise Type", selection: $type) { + Text("None").tag(nil as ExerciseType?) ForEach(exerciseTypes, id: \.self) { type in Text("\(type.name)").tag(type as ExerciseType?) } } .pickerStyle(NavigationLinkPickerStyle()) Picker("Exercise Unit", selection: $unit) { + Text("None").tag(nil as ExerciseUnit?) ForEach(exerciseUnits, id: \.self) { unit in Text("\(unit.name) (\(unit.symbol))").tag(unit as ExerciseUnit?) } @@ -59,26 +63,37 @@ struct ExerciseDetail: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Save") { - saveItem() + saveExisting() dismiss() } } } - } - - private func saveItem() { - if modelContext.hasChanges { - do { - try modelContext.save() - } catch { - print("Failed to save exercise: \(error.localizedDescription)") + .onAppear() { + name = exercise.name + description = exercise.exerciseDescription + isPartOfProgression = exercise.isPartOfProgression + if let type = exercise.type { + self.type = type + } + if let unit = exercise.unit { + self.unit = unit } } } + + private func saveExisting() { + exercise.name = name + exercise.exerciseDescription = description + exercise.isPartOfProgression = isPartOfProgression + exercise.type = type + exercise.unit = unit + } } #Preview { - NavigationStack { - ExerciseDetail(exercise: Exercise("Squat Sky Reaches")) + ModelContainerPreview(ModelContainer.sample) { + NavigationStack { + ExerciseEditor(exercise: Exercise.sampleDataRecommendedRoutine.first!) + } } } diff --git a/WorkoutsPlus/Features/Exercise/ExerciseLibrary.swift b/WorkoutsPlus/Features/Exercise/ExerciseLibrary.swift index 76b8d8c..ad13856 100644 --- a/WorkoutsPlus/Features/Exercise/ExerciseLibrary.swift +++ b/WorkoutsPlus/Features/Exercise/ExerciseLibrary.swift @@ -31,7 +31,7 @@ struct ExerciseLibrary: View { List { Section { ForEach(filteredItems) { exercise in - NavigationLink(destination: ExerciseDetail(exercise: exercise), label: {Text(exercise.name)}) + NavigationLink(destination: ExerciseEditor(exercise: exercise), label: {Text(exercise.name)}) } .onDelete(perform: deleteExercise) if filteredItems.isEmpty { @@ -77,6 +77,14 @@ struct ExerciseLibrary: View { } } +#Preview { + ModelContainerPreview(ModelContainer.sample) { + NavigationStack { + ExerciseLibrary() + } + } +} + #Preview("With Sample Data") { NavigationStack { ExerciseLibrary() diff --git a/WorkoutsPlus/Features/Exercise/ExerciseType.swift b/WorkoutsPlus/Features/Exercise/ExerciseType.swift index d3d08fc..82273f5 100644 --- a/WorkoutsPlus/Features/Exercise/ExerciseType.swift +++ b/WorkoutsPlus/Features/Exercise/ExerciseType.swift @@ -19,11 +19,3 @@ final class ExerciseType: Identifiable { self.name = name } } - -extension ExerciseType { - static let getTypes = [ - ExerciseType("Kilograms"), - ExerciseType("Kilometers"), - ExerciseType("Meters"), - ] -} diff --git a/WorkoutsPlus/Features/Exercise/ExerciseUnit.swift b/WorkoutsPlus/Features/Exercise/ExerciseUnit.swift index c964e14..628de88 100644 --- a/WorkoutsPlus/Features/Exercise/ExerciseUnit.swift +++ b/WorkoutsPlus/Features/Exercise/ExerciseUnit.swift @@ -21,11 +21,3 @@ final class ExerciseUnit: Unit, Identifiable { self.symbol = symbol } } - -extension ExerciseUnit { - static let getUnits = [ - ExerciseUnit("Kilograms", symbol: "kg"), - ExerciseUnit("Kilometers", symbol: "km"), - ExerciseUnit("Meters", symbol: "m"), - ] -} diff --git a/WorkoutsPlus/PreviewHelper/ModelContainerPreview.swift b/WorkoutsPlus/PreviewHelper/ModelContainerPreview.swift new file mode 100644 index 0000000..a2e4181 --- /dev/null +++ b/WorkoutsPlus/PreviewHelper/ModelContainerPreview.swift @@ -0,0 +1,78 @@ +/* + Copyright © 2023 Apple Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Abstract: +A view to use only in previews that creates a model container before + showing the preview content. +*/ + +import SwiftUI +import SwiftData + +struct ModelContainerPreview: View { + var content: () -> Content + let container: ModelContainer + + /// Creates an instance of the model container preview. + /// + /// This view creates the model container before displaying the preview + /// content. The view is intended for use in previews only. + /// + /// #Preview { + /// ModelContainerPreview { + /// AnimalEditor(animal: nil) + /// .environment(NavigationContext()) + /// } modelContainer: { + /// let schema = Schema([AnimalCategory.self, Animal.self]) + /// let configuration = ModelConfiguration(isStoredInMemoryOnly: true) + /// let container = try ModelContainer(for: schema, configurations: [configuration]) + /// Task { @MainActor in + /// AnimalCategory.insertSampleData(modelContext: container.mainContext) + /// } + /// return container + /// } + /// } + /// + /// - Parameters: + /// - content: A view that describes the content to preview. + /// - modelContainer: A closure that returns a model container. + init(@ViewBuilder content: @escaping () -> Content, modelContainer: @escaping () throws -> ModelContainer) { + self.content = content + do { + self.container = try MainActor.assumeIsolated(modelContainer) + } catch { + fatalError("Failed to create the model container: \(error.localizedDescription)") + } + } + + /// Creates a view that creates the provided model container before displaying + /// the preview content. + /// + /// This view creates the model container before displaying the preview + /// content. The view is intended for use in previews only. + /// + /// #Preview { + /// ModelContainerPreview(SampleModelContainer.main) { + /// AnimalEditor(animal: .kangaroo) + /// .environment(NavigationContext()) + /// } + /// } + /// + /// - Parameters: + /// - modelContainer: A closure that returns a model container. + /// - content: A view that describes the content to preview. + init(_ modelContainer: @escaping () throws -> ModelContainer, @ViewBuilder content: @escaping () -> Content) { + self.init(content: content, modelContainer: modelContainer) + } + + var body: some View { + content() + .modelContainer(container) + } +} diff --git a/WorkoutsPlus/SampleData/SampleData+Exercise.swift b/WorkoutsPlus/SampleData/SampleData+Exercise.swift new file mode 100644 index 0000000..bc19b82 --- /dev/null +++ b/WorkoutsPlus/SampleData/SampleData+Exercise.swift @@ -0,0 +1,36 @@ +// +// SampleData+Exercise.swift +// WorkoutsPlus +// +// Created by Felix Förtsch on 13.11.24. +// + +import Foundation + +extension 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"), + Exercise("Inverted Rows"), + Exercise("Hanging Knee Raises"), + Exercise("Pistol Squats"), + Exercise("Hanging Leg Curls"), + Exercise("Sissy Squats") + ] +} diff --git a/WorkoutsPlus/SampleData/SampleData+ExerciseType.swift b/WorkoutsPlus/SampleData/SampleData+ExerciseType.swift new file mode 100644 index 0000000..ede35ac --- /dev/null +++ b/WorkoutsPlus/SampleData/SampleData+ExerciseType.swift @@ -0,0 +1,16 @@ +// +// SampleData+ExerciseType.swift +// WorkoutsPlus +// +// Created by Felix Förtsch on 13.11.24. +// + +import Foundation + +extension ExerciseType { + static let sampleData = [ + ExerciseType("Kilograms"), + ExerciseType("Kilometers"), + ExerciseType("Meters"), + ] +} diff --git a/WorkoutsPlus/SampleData/SampleData+ExerciseUnit.swift b/WorkoutsPlus/SampleData/SampleData+ExerciseUnit.swift new file mode 100644 index 0000000..7402a9f --- /dev/null +++ b/WorkoutsPlus/SampleData/SampleData+ExerciseUnit.swift @@ -0,0 +1,16 @@ +// +// SampleData+ExerciseUnit.swift +// WorkoutsPlus +// +// Created by Felix Förtsch on 13.11.24. +// + +import Foundation + +extension ExerciseUnit { + static let sampleData = [ + ExerciseUnit("Kilograms", symbol: "kg"), + ExerciseUnit("Kilometers", symbol: "km"), + ExerciseUnit("Meters", symbol: "m"), + ] +} diff --git a/WorkoutsPlus/Configuration/SampleData.swift b/WorkoutsPlus/SampleData/SampleData.swift similarity index 71% rename from WorkoutsPlus/Configuration/SampleData.swift rename to WorkoutsPlus/SampleData/SampleData.swift index 19ee4bb..3760ee4 100644 --- a/WorkoutsPlus/Configuration/SampleData.swift +++ b/WorkoutsPlus/SampleData/SampleData.swift @@ -8,6 +8,19 @@ import Foundation import SwiftData +// This is taken from the Animals example +extension ModelContainer { + static var sample: () throws -> ModelContainer = { + let schema = WorkoutsPlusApp.swiftDataSchema + let configuration = ModelConfiguration(isStoredInMemoryOnly: true) + let container = try ModelContainer(for: schema, configurations: [configuration]) + Task { @MainActor in + SampleData.insertSampleData(into: container.mainContext) + } + return container + } +} + @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() @@ -31,8 +44,17 @@ class SampleData { } } + extension SampleData { static func insertSampleData(into context: ModelContext) { + for exerciseType in ExerciseType.sampleData { + context.insert(exerciseType) + } + + for exerciseUnit in ExerciseUnit.sampleData { + context.insert(exerciseUnit) + } + for exercise in Exercise.sampleDataRecommendedRoutine { context.insert(exercise) }