add ModelContainerPreview conecept, move SampleData, fix Exercise, ExerciseUnit, ExerciseType
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
@@ -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!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -19,11 +19,3 @@ final class ExerciseType: Identifiable {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
extension ExerciseType {
|
||||
static let getTypes = [
|
||||
ExerciseType("Kilograms"),
|
||||
ExerciseType("Kilometers"),
|
||||
ExerciseType("Meters"),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
}
|
||||
|
||||
78
WorkoutsPlus/PreviewHelper/ModelContainerPreview.swift
Normal file
78
WorkoutsPlus/PreviewHelper/ModelContainerPreview.swift
Normal file
@@ -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<Content: View>: 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)
|
||||
}
|
||||
}
|
||||
36
WorkoutsPlus/SampleData/SampleData+Exercise.swift
Normal file
36
WorkoutsPlus/SampleData/SampleData+Exercise.swift
Normal file
@@ -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")
|
||||
]
|
||||
}
|
||||
16
WorkoutsPlus/SampleData/SampleData+ExerciseType.swift
Normal file
16
WorkoutsPlus/SampleData/SampleData+ExerciseType.swift
Normal file
@@ -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"),
|
||||
]
|
||||
}
|
||||
16
WorkoutsPlus/SampleData/SampleData+ExerciseUnit.swift
Normal file
16
WorkoutsPlus/SampleData/SampleData+ExerciseUnit.swift
Normal file
@@ -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"),
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user