wip
This commit is contained in:
		@@ -15,28 +15,30 @@ struct ContentView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        TabView {
 | 
			
		||||
            WorkoutsView()
 | 
			
		||||
                .tabItem {
 | 
			
		||||
                    Label("Workout", systemImage: "figure.strengthtraining.traditional")
 | 
			
		||||
                }
 | 
			
		||||
        NavigationView {
 | 
			
		||||
            TabView {
 | 
			
		||||
                WorkoutsView()
 | 
			
		||||
                    .tabItem {
 | 
			
		||||
                        Label("Workout", systemImage: "figure.strengthtraining.traditional")
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Reports Tab
 | 
			
		||||
            NavigationStack {
 | 
			
		||||
                Text("Reports Placeholder")
 | 
			
		||||
                    .navigationTitle("Reports")
 | 
			
		||||
            }
 | 
			
		||||
            .tabItem {
 | 
			
		||||
                Label("Reports", systemImage: "chart.bar")
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            SettingsView()
 | 
			
		||||
                .tabItem {
 | 
			
		||||
                    Label("Settings", systemImage: "gear")
 | 
			
		||||
                
 | 
			
		||||
                // Reports Tab
 | 
			
		||||
                NavigationStack {
 | 
			
		||||
                    Text("Reports Placeholder")
 | 
			
		||||
                        .navigationTitle("Reports")
 | 
			
		||||
                }
 | 
			
		||||
                .tabItem {
 | 
			
		||||
                    Label("Reports", systemImage: "chart.bar")
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                SettingsView()
 | 
			
		||||
                    .tabItem {
 | 
			
		||||
                        Label("Settings", systemImage: "gear")
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .observeCloudKitChanges()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,8 @@ final class Exercise {
 | 
			
		||||
        self.reps = reps
 | 
			
		||||
        self.weight = weight
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static let unnamed = "Unnamed Exercise"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension Exercise: EditableEntity {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,8 @@ final class Split {
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.intro = intro
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static let unnamed = "Unnamed Split"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: - EditableEntity Conformance
 | 
			
		||||
@@ -63,7 +65,7 @@ fileprivate struct SplitFormView: View {
 | 
			
		||||
            if let assignments = model.exercises, !assignments.isEmpty {
 | 
			
		||||
                ForEach(assignments) { item in
 | 
			
		||||
                    ListItem(
 | 
			
		||||
                        title: item.exercise?.name ?? "Unnamed",
 | 
			
		||||
                        title: item.exercise?.name ?? Exercise.unnamed,
 | 
			
		||||
                        subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs"
 | 
			
		||||
                    )
 | 
			
		||||
                    .swipeActions {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								Workouts/Schema/CloudKitSyncObserver.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								Workouts/Schema/CloudKitSyncObserver.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
/// A view modifier that refreshes the view when CloudKit data changes
 | 
			
		||||
struct CloudKitSyncObserver: ViewModifier {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    @State private var refreshID = UUID()
 | 
			
		||||
    
 | 
			
		||||
    func body(content: Content) -> some View {
 | 
			
		||||
        content
 | 
			
		||||
            .id(refreshID) // Force view refresh when this changes
 | 
			
		||||
            .onReceive(NotificationCenter.default.publisher(for: .cloudKitDataDidChange)) { _ in
 | 
			
		||||
                // When we receive a notification that CloudKit data changed:
 | 
			
		||||
                // 1. Create a new UUID to force view refresh
 | 
			
		||||
                refreshID = UUID()
 | 
			
		||||
                
 | 
			
		||||
                // 2. Optionally, you can also manually refresh the model context
 | 
			
		||||
                // This is sometimes needed for complex relationships
 | 
			
		||||
                Task { @MainActor in
 | 
			
		||||
                    try? modelContext.fetch(FetchDescriptor<Exercise>())
 | 
			
		||||
                    // Add other model types as needed
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extension to make it easier to use the modifier
 | 
			
		||||
extension View {
 | 
			
		||||
    /// Adds observation for CloudKit sync changes and refreshes the view when changes occur
 | 
			
		||||
    func observeCloudKitChanges() -> some View {
 | 
			
		||||
        self.modifier(CloudKitSyncObserver())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
struct InitialData {
 | 
			
		||||
struct DataLoader {
 | 
			
		||||
    static let logger = AppLogger(
 | 
			
		||||
        subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
 | 
			
		||||
        category: "InitialData"
 | 
			
		||||
@@ -2,21 +2,15 @@ import Foundation
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
final class WorkoutsContainer {
 | 
			
		||||
    static let logger = AppLogger(subsystem: "Workouts", category: "WorkoutsContainer")
 | 
			
		||||
    static let logger = AppLogger(
 | 
			
		||||
        subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
 | 
			
		||||
        category: "WorkoutsContainer"
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    static func create(shouldCreateDefaults: inout Bool) -> ModelContainer {
 | 
			
		||||
    static func create() -> ModelContainer {
 | 
			
		||||
        let schema = Schema(versionedSchema: SchemaV1.self)
 | 
			
		||||
        let configuration = ModelConfiguration(cloudKitDatabase: .automatic)
 | 
			
		||||
        let container = try! ModelContainer(for: schema, migrationPlan: WorkoutsMigrationPlan.self, configurations: [configuration])
 | 
			
		||||
        
 | 
			
		||||
        let context = ModelContext(container)
 | 
			
		||||
        let descriptor = FetchDescriptor<Exercise>()
 | 
			
		||||
        let results = try! context.fetch(descriptor)
 | 
			
		||||
        
 | 
			
		||||
        if results.isEmpty {
 | 
			
		||||
            shouldCreateDefaults = true
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return container
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@@ -30,7 +24,7 @@ final class WorkoutsContainer {
 | 
			
		||||
            let context = ModelContext(container)
 | 
			
		||||
            
 | 
			
		||||
            // Create default data for previews
 | 
			
		||||
            InitialData.create(modelContext: context)
 | 
			
		||||
            DataLoader.create(modelContext: context)
 | 
			
		||||
            
 | 
			
		||||
            return container
 | 
			
		||||
        } catch {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,17 @@
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
import CloudKit
 | 
			
		||||
 | 
			
		||||
enum AppStorageKeys {
 | 
			
		||||
    static let iCloudSyncEnabled = "iCloudSyncEnabled"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct SettingsView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    @State private var showingPopulateData = false
 | 
			
		||||
    @State private var showingClearAllDataConfirmation = false
 | 
			
		||||
    
 | 
			
		||||
    var splitsCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Split>()) }
 | 
			
		||||
    var musclesCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Muscle>()) }
 | 
			
		||||
@@ -70,10 +77,81 @@ struct SettingsView: View {
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                Section(header: Text("Developer")) {
 | 
			
		||||
 | 
			
		||||
                    Button(action: {
 | 
			
		||||
                        showingPopulateData = true
 | 
			
		||||
                    }) {
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Label("Populate Data", systemImage: "plus")
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    .confirmationDialog(
 | 
			
		||||
                        "Populate Data?",
 | 
			
		||||
                        isPresented: $showingPopulateData,
 | 
			
		||||
                        titleVisibility: .hidden
 | 
			
		||||
                    ) {
 | 
			
		||||
                        Button("Populate Data") {
 | 
			
		||||
                            DataLoader.create(modelContext: modelContext)
 | 
			
		||||
                        }
 | 
			
		||||
                        Button("Cancel", role: .cancel) {}
 | 
			
		||||
//                    } message: {
 | 
			
		||||
//                        Text("This action cannot be undone. All data will be permanently deleted.")
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    Button(action: {
 | 
			
		||||
                        showingClearAllDataConfirmation = true
 | 
			
		||||
                    }) {
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Label("Clear All Data", systemImage: "trash")
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    .foregroundColor(.red)
 | 
			
		||||
                    .confirmationDialog(
 | 
			
		||||
                        "Clear All Data?",
 | 
			
		||||
                        isPresented: $showingClearAllDataConfirmation,
 | 
			
		||||
                        titleVisibility: .visible
 | 
			
		||||
                    ) {
 | 
			
		||||
                        Button("Clear All Data", role: .destructive) {
 | 
			
		||||
                            clearAllData()
 | 
			
		||||
                        }
 | 
			
		||||
                        Button("Cancel", role: .cancel) {}
 | 
			
		||||
                    } message: {
 | 
			
		||||
                        Text("This action cannot be undone. All data will be permanently deleted.")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .navigationTitle("Settings")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func deleteAllObjects<T: PersistentModel>(ofType type: T.Type, from context: ModelContext) throws {
 | 
			
		||||
        let descriptor = FetchDescriptor<T>()
 | 
			
		||||
        let allObjects = try context.fetch(descriptor)
 | 
			
		||||
        for object in allObjects {
 | 
			
		||||
            context.delete(object)
 | 
			
		||||
        }
 | 
			
		||||
        try context.save()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func clearAllData () {
 | 
			
		||||
        do {
 | 
			
		||||
            try deleteAllObjects(ofType: ExerciseType.self, from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: Exercise.self,     from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: Muscle.self,       from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: MuscleGroup.self,  from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: Split.self,        from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: SplitExerciseAssignment.self, from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: Workout.self,      from: modelContext)
 | 
			
		||||
            try deleteAllObjects(ofType: WorkoutLog.self,   from: modelContext)
 | 
			
		||||
            try modelContext.save()
 | 
			
		||||
        } catch {
 | 
			
		||||
            print("Failed to clear all data: \(error)")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ExercisesListView: View {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										83
									
								
								Workouts/Views/Workouts/WorkoutEditView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								Workouts/Views/Workouts/WorkoutEditView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
//
 | 
			
		||||
// WorkoutEditView.swift
 | 
			
		||||
// Workouts
 | 
			
		||||
//
 | 
			
		||||
// Created by rzen on 7/14/25 at 7:35 AM.
 | 
			
		||||
//
 | 
			
		||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
 | 
			
		||||
struct WorkoutEditView: View {
 | 
			
		||||
    @Environment(\.modelContext) private var modelContext
 | 
			
		||||
    @Environment(\.dismiss) private var dismiss
 | 
			
		||||
    
 | 
			
		||||
    @State var workout: Workout
 | 
			
		||||
    @State var endDateHidden: Bool = false
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
            Form {
 | 
			
		||||
                Section (header: Text("Split")) {
 | 
			
		||||
                    Text("\(workout.split?.name ?? Split.unnamed)")
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                Section (header: Text("Start/End")) {
 | 
			
		||||
                    DatePicker("Started", selection: $workout.start)
 | 
			
		||||
                    Toggle("Workout Ended", isOn: Binding(
 | 
			
		||||
                        get: { workout.end != nil },
 | 
			
		||||
                        set: { newValue in
 | 
			
		||||
                            withAnimation {
 | 
			
		||||
                                if newValue {
 | 
			
		||||
                                    workout.end = Date()
 | 
			
		||||
                                    endDateHidden = false
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    workout.end = nil
 | 
			
		||||
                                    endDateHidden = true
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    ))
 | 
			
		||||
                    if !endDateHidden {
 | 
			
		||||
                        DatePicker("Ended", selection: $workout.start)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .onAppear {
 | 
			
		||||
                    endDateHidden = workout.end == nil
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                Section (header: Text("Workout Log")) {
 | 
			
		||||
                    if let workoutLogs = workout.logs {
 | 
			
		||||
                        List {
 | 
			
		||||
                            ForEach (workoutLogs) { log in
 | 
			
		||||
                                ListItem(
 | 
			
		||||
                                    title: log.exercise?.name ?? Exercise.unnamed,
 | 
			
		||||
                                    subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Text("No workout logs yet")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .toolbar {
 | 
			
		||||
                ToolbarItem(placement: .navigationBarLeading) {
 | 
			
		||||
                    Button("Cancel") {
 | 
			
		||||
                        dismiss()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                ToolbarItem(placement: .navigationBarTrailing) {
 | 
			
		||||
                    Button("Save") {
 | 
			
		||||
                        try? modelContext.save()
 | 
			
		||||
                        dismiss()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -18,64 +18,66 @@ struct WorkoutLogEditView: View {
 | 
			
		||||
    @State private var showingSaveConfirmation = false
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        Form {
 | 
			
		||||
            Section (header: Text("Exercise")) {
 | 
			
		||||
                Text("\(workoutLog.exercise?.name ?? "Unnamed Exercise")")
 | 
			
		||||
                    .font(.headline)
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            Section(header: Text("Sets/Reps")) {
 | 
			
		||||
                Stepper("Sets: \(workoutLog.sets)", value: $workoutLog.sets, in: 1...10)
 | 
			
		||||
                Stepper("Reps: \(workoutLog.reps)", value: $workoutLog.reps, in: 1...50)
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            Section(header: Text("Weight")) {
 | 
			
		||||
                HStack {
 | 
			
		||||
                    VStack(alignment: .center) {
 | 
			
		||||
                        Text("\(workoutLog.weight) lbs")
 | 
			
		||||
                            .font(.headline)
 | 
			
		||||
        NavigationStack {
 | 
			
		||||
            Form {
 | 
			
		||||
                Section (header: Text("Exercise")) {
 | 
			
		||||
                    Text("\(workoutLog.exercise?.name ?? Exercise.unnamed)")
 | 
			
		||||
                        .font(.headline)
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                Section(header: Text("Sets/Reps")) {
 | 
			
		||||
                    Stepper("Sets: \(workoutLog.sets)", value: $workoutLog.sets, in: 1...10)
 | 
			
		||||
                    Stepper("Reps: \(workoutLog.reps)", value: $workoutLog.reps, in: 1...50)
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                Section(header: Text("Weight")) {
 | 
			
		||||
                    HStack {
 | 
			
		||||
                        VStack(alignment: .center) {
 | 
			
		||||
                            Text("\(workoutLog.weight) lbs")
 | 
			
		||||
                                .font(.headline)
 | 
			
		||||
                        }
 | 
			
		||||
                        Spacer()
 | 
			
		||||
                        VStack(alignment: .trailing) {
 | 
			
		||||
                            Stepper("±1", value: $workoutLog.weight, in: 0...1000)
 | 
			
		||||
                            Stepper("±5", value: $workoutLog.weight, in: 0...1000, step: 5)
 | 
			
		||||
                        }
 | 
			
		||||
                        .frame(width: 130)
 | 
			
		||||
                    }
 | 
			
		||||
                    Spacer()
 | 
			
		||||
                    VStack(alignment: .trailing) {
 | 
			
		||||
                        Stepper("±1", value: $workoutLog.weight, in: 0...1000)
 | 
			
		||||
                        Stepper("±5", value: $workoutLog.weight, in: 0...1000, step: 5)
 | 
			
		||||
                    }
 | 
			
		||||
                    .frame(width: 130)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .toolbar {
 | 
			
		||||
            ToolbarItem(placement: .navigationBarLeading) {
 | 
			
		||||
                Button("Cancel") {
 | 
			
		||||
            .toolbar {
 | 
			
		||||
                ToolbarItem(placement: .navigationBarLeading) {
 | 
			
		||||
                    Button("Cancel") {
 | 
			
		||||
                        dismiss()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                ToolbarItem(placement: .navigationBarTrailing) {
 | 
			
		||||
                    Button("Save") {
 | 
			
		||||
                        showingSaveConfirmation = true
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .confirmationDialog("Save Options", isPresented: $showingSaveConfirmation) {
 | 
			
		||||
                Button("Save Workout Log Only") {
 | 
			
		||||
                    try? modelContext.save()
 | 
			
		||||
                    dismiss()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            ToolbarItem(placement: .navigationBarTrailing) {
 | 
			
		||||
                Button("Save") {
 | 
			
		||||
                    showingSaveConfirmation = true
 | 
			
		||||
                
 | 
			
		||||
                Button("Save Workout Log and Update Split") {
 | 
			
		||||
                    // Save the workout log
 | 
			
		||||
                    try? modelContext.save()
 | 
			
		||||
                    
 | 
			
		||||
                    // Update the split with this workout log's data
 | 
			
		||||
                    // Note: Implementation depends on how splits are updated in your app
 | 
			
		||||
                    updateSplit(from: workoutLog)
 | 
			
		||||
                    
 | 
			
		||||
                    dismiss()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .confirmationDialog("Save Options", isPresented: $showingSaveConfirmation) {
 | 
			
		||||
            Button("Save Workout Log Only") {
 | 
			
		||||
                try? modelContext.save()
 | 
			
		||||
                dismiss()
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            Button("Save Workout Log and Update Split") {
 | 
			
		||||
                // Save the workout log
 | 
			
		||||
                try? modelContext.save()
 | 
			
		||||
                
 | 
			
		||||
                // Update the split with this workout log's data
 | 
			
		||||
                // Note: Implementation depends on how splits are updated in your app
 | 
			
		||||
                updateSplit(from: workoutLog)
 | 
			
		||||
                
 | 
			
		||||
                dismiss()
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            Button("Cancel", role: .cancel) {
 | 
			
		||||
                // Do nothing, dialog will dismiss
 | 
			
		||||
                Button("Cancel", role: .cancel) {
 | 
			
		||||
                    // Do nothing, dialog will dismiss
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -28,50 +28,55 @@ struct WorkoutLogView: View {
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        Form {
 | 
			
		||||
            List {
 | 
			
		||||
                ForEach (sortedWorkoutLogs) { log in
 | 
			
		||||
                    let badges = log.completed ? [Badge(text: "Completed", color: .green)] : []
 | 
			
		||||
                    ListItem(
 | 
			
		||||
                        title: log.exercise?.name ?? "Untitled Exercise",
 | 
			
		||||
                        subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs",
 | 
			
		||||
                        badges: badges
 | 
			
		||||
                    )
 | 
			
		||||
                    .swipeActions(edge: .leading, allowsFullSwipe: false) {
 | 
			
		||||
                        if (log.completed) {
 | 
			
		||||
                            Button {
 | 
			
		||||
                                log.completed = false
 | 
			
		||||
                                try? modelContext.save()
 | 
			
		||||
                            } label: {
 | 
			
		||||
                                Label("Complete", systemImage: "circle.fill")
 | 
			
		||||
            Section {
 | 
			
		||||
                Text("Started \(workout.label)")
 | 
			
		||||
            }
 | 
			
		||||
            Section {
 | 
			
		||||
                List {
 | 
			
		||||
                    ForEach (sortedWorkoutLogs) { log in
 | 
			
		||||
                        let badges = log.completed ? [Badge(text: "Completed", color: .green)] : []
 | 
			
		||||
                        ListItem(
 | 
			
		||||
                            title: log.exercise?.name ?? "Untitled Exercise",
 | 
			
		||||
                            subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs",
 | 
			
		||||
                            badges: badges
 | 
			
		||||
                        )
 | 
			
		||||
                        .swipeActions(edge: .leading, allowsFullSwipe: false) {
 | 
			
		||||
                            if (log.completed) {
 | 
			
		||||
                                Button {
 | 
			
		||||
                                    log.completed = false
 | 
			
		||||
                                    try? modelContext.save()
 | 
			
		||||
                                } label: {
 | 
			
		||||
                                    Label("Complete", systemImage: "circle.fill")
 | 
			
		||||
                                }
 | 
			
		||||
                                .tint(.green)
 | 
			
		||||
                            } else {
 | 
			
		||||
                                Button {
 | 
			
		||||
                                    log.completed = true
 | 
			
		||||
                                    try? modelContext.save()
 | 
			
		||||
                                } label: {
 | 
			
		||||
                                    Label("Reset", systemImage: "checkmark.circle.fill")
 | 
			
		||||
                                }
 | 
			
		||||
                                .tint(.green)
 | 
			
		||||
                            }
 | 
			
		||||
                            .tint(.green)
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Button {
 | 
			
		||||
                                log.completed = true
 | 
			
		||||
                                try? modelContext.save()
 | 
			
		||||
                        }
 | 
			
		||||
                        .swipeActions(edge: .trailing, allowsFullSwipe: false) {
 | 
			
		||||
                            Button(role: .destructive) {
 | 
			
		||||
                                itemToDelete = log
 | 
			
		||||
                            } label: {
 | 
			
		||||
                                Label("Reset", systemImage: "checkmark.circle.fill")
 | 
			
		||||
                                Label("Delete", systemImage: "trash")
 | 
			
		||||
                            }
 | 
			
		||||
                            .tint(.green)
 | 
			
		||||
                            Button {
 | 
			
		||||
                                itemToEdit = log
 | 
			
		||||
                            } label: {
 | 
			
		||||
                                Label("Edit", systemImage: "pencil")
 | 
			
		||||
                            }
 | 
			
		||||
                            .tint(.indigo)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    .swipeActions(edge: .trailing, allowsFullSwipe: false) {
 | 
			
		||||
                        Button(role: .destructive) {
 | 
			
		||||
                            itemToDelete = log
 | 
			
		||||
                        } label: {
 | 
			
		||||
                            Label("Delete", systemImage: "trash")
 | 
			
		||||
                        }
 | 
			
		||||
                        Button {
 | 
			
		||||
                            itemToEdit = log
 | 
			
		||||
                        } label: {
 | 
			
		||||
                            Label("Edit", systemImage: "pencil")
 | 
			
		||||
                        }
 | 
			
		||||
                        .tint(.indigo)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .navigationTitle("Workout")
 | 
			
		||||
        .navigationTitle("\(workout.split?.name ?? Split.unnamed)")
 | 
			
		||||
        .toolbar {
 | 
			
		||||
            ToolbarItem(placement: .navigationBarTrailing) {
 | 
			
		||||
                Button(action: { showingAddSheet.toggle() }) {
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,10 @@ struct WorkoutsView: View {
 | 
			
		||||
                    List {
 | 
			
		||||
                        ForEach (workouts) { workout in
 | 
			
		||||
                            NavigationLink(destination: WorkoutLogView(workout: workout)) {
 | 
			
		||||
                                ListItem(title: workout.label)
 | 
			
		||||
                                ListItem(
 | 
			
		||||
                                    title: workout.split?.name ?? Split.unnamed,
 | 
			
		||||
                                    subtitle: workout.label
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                            .swipeActions(edge: .trailing, allowsFullSwipe: false) {
 | 
			
		||||
                                Button(role: .destructive) {
 | 
			
		||||
@@ -61,9 +64,9 @@ struct WorkoutsView: View {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
//            .sheet(item: $itemToEdit) { item in
 | 
			
		||||
//                T.formView(for: item)
 | 
			
		||||
//            }
 | 
			
		||||
            .sheet(item: $itemToEdit) { item in
 | 
			
		||||
                WorkoutEditView(workout: item)
 | 
			
		||||
            }
 | 
			
		||||
            .confirmationDialog(
 | 
			
		||||
                "Delete?",
 | 
			
		||||
                isPresented: Binding<Bool>(
 | 
			
		||||
 
 | 
			
		||||
@@ -10,17 +10,51 @@
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import SwiftData
 | 
			
		||||
import CloudKit
 | 
			
		||||
import CoreData
 | 
			
		||||
 | 
			
		||||
@main
 | 
			
		||||
struct WorkoutsApp: App {
 | 
			
		||||
        let container: ModelContainer
 | 
			
		||||
    let container: ModelContainer
 | 
			
		||||
    @State private var cloudKitObserver: NSObjectProtocol?
 | 
			
		||||
    
 | 
			
		||||
    init() {
 | 
			
		||||
        var shouldCreateDefaults = false
 | 
			
		||||
        self.container = WorkoutsContainer.create(shouldCreateDefaults: &shouldCreateDefaults)
 | 
			
		||||
        self.container = WorkoutsContainer.create()
 | 
			
		||||
        
 | 
			
		||||
        if shouldCreateDefaults {
 | 
			
		||||
            InitialData.create(modelContext: ModelContext(container))
 | 
			
		||||
        // Set up CloudKit notification observation
 | 
			
		||||
        setupCloudKitObservation()
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func setupCloudKitObservation() {
 | 
			
		||||
        // Access the underlying NSPersistentCloudKitContainer
 | 
			
		||||
        if let persistentContainer = Mirror(reflecting: container).descendant("persistentContainer") as? NSPersistentContainer {
 | 
			
		||||
            // Register for remote change notifications
 | 
			
		||||
            cloudKitObserver = NotificationCenter.default.addObserver(
 | 
			
		||||
                forName: NSPersistentCloudKitContainer.eventChangedNotification,
 | 
			
		||||
                object: persistentContainer,
 | 
			
		||||
                queue: .main
 | 
			
		||||
            ) { notification in
 | 
			
		||||
                // Handle the notification
 | 
			
		||||
                self.handleCloudKitNotification(notification)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func handleCloudKitNotification(_ notification: Notification) {
 | 
			
		||||
        guard let event = notification.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey] as? NSPersistentCloudKitContainer.Event else {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Log the event for debugging
 | 
			
		||||
        print("CloudKit event: \(event.type), \(event.succeeded ? "succeeded" : "failed")")
 | 
			
		||||
        
 | 
			
		||||
        // If the event was a successful import, refresh the UI
 | 
			
		||||
        if event.type == .import, event.succeeded {
 | 
			
		||||
            // Create a new context to force UI refresh
 | 
			
		||||
            let context = ModelContext(container)
 | 
			
		||||
            
 | 
			
		||||
            // Trigger UI refresh by posting a notification that views can observe
 | 
			
		||||
            NotificationCenter.default.post(name: .cloudKitDataDidChange, object: nil)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -31,3 +65,8 @@ struct WorkoutsApp: App {
 | 
			
		||||
        .modelContainer(container)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extension to define the notification name
 | 
			
		||||
extension Notification.Name {
 | 
			
		||||
    static let cloudKitDataDidChange = Notification.Name("cloudKitDataDidChange")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user