add ModelContainerPreview conecept, move SampleData, fix Exercise, ExerciseUnit, ExerciseType

This commit is contained in:
Felix Förtsch
2024-11-13 13:59:39 +01:00
parent 370e070fcd
commit 02e2937094
11 changed files with 211 additions and 63 deletions

View File

@@ -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)

View File

@@ -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")
]
}

View File

@@ -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!)
}
}
}

View File

@@ -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()

View File

@@ -19,11 +19,3 @@ final class ExerciseType: Identifiable {
self.name = name
}
}
extension ExerciseType {
static let getTypes = [
ExerciseType("Kilograms"),
ExerciseType("Kilometers"),
ExerciseType("Meters"),
]
}

View File

@@ -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"),
]
}

View 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)
}
}

View 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")
]
}

View 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"),
]
}

View 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"),
]
}

View File

@@ -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, youre 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, youve 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)
}