Files
workouts/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift
rzen 9a881e841b Add WatchConnectivity for bidirectional iOS-Watch sync
Implement real-time sync between iOS and Apple Watch apps using
WatchConnectivity framework. This replaces reliance on CloudKit
which doesn't work reliably in simulators.

- Add WatchConnectivityManager to both iOS and Watch targets
- Sync workouts, splits, exercises, and logs between devices
- Update iOS views to trigger sync on data changes
- Add onChange observer to ExerciseView for live progress updates
- Configure App Groups for shared container storage
- Add Watch app views: WorkoutLogsView, WorkoutLogListView, ExerciseProgressView
2026-01-19 19:15:38 -05:00

190 lines
6.2 KiB
Swift

//
// WorkoutLogsView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 6:52 PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import CoreData
struct WorkoutLogsView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Workout.start, ascending: false)],
animation: .default
)
private var workouts: FetchedResults<Workout>
@State private var showingSplitPicker = false
@State private var showingSettings = false
@State private var itemToDelete: Workout? = nil
var body: some View {
NavigationStack {
List {
ForEach(workouts, id: \.objectID) { workout in
NavigationLink(destination: WorkoutLogListView(workout: workout)) {
CalendarListItem(
date: workout.start,
title: workout.split?.name ?? Split.unnamed,
subtitle: getSubtitle(for: workout),
subtitle2: workout.statusName
)
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button {
itemToDelete = workout
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
}
}
.overlay {
if workouts.isEmpty {
ContentUnavailableView(
"No Workouts Yet",
systemImage: "list.bullet.clipboard",
description: Text("Start a new workout from one of your splits.")
)
}
}
.navigationTitle("Workout Logs")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
showingSettings = true
} label: {
Image(systemName: "gearshape.2")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Start New") {
showingSplitPicker.toggle()
}
}
}
.sheet(isPresented: $showingSettings) {
SettingsView()
}
.sheet(isPresented: $showingSplitPicker) {
SplitPickerSheet()
}
.confirmationDialog(
"Delete Workout?",
isPresented: Binding<Bool>(
get: { itemToDelete != nil },
set: { if !$0 { itemToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let item = itemToDelete {
withAnimation {
viewContext.delete(item)
try? viewContext.save()
WatchConnectivityManager.shared.syncAllData()
itemToDelete = nil
}
}
}
Button("Cancel", role: .cancel) {
itemToDelete = nil
}
}
}
}
private func getSubtitle(for workout: Workout) -> String {
if workout.status == .completed, let endDate = workout.end {
return workout.start.humanTimeInterval(to: endDate)
} else {
return workout.start.formattedDate()
}
}
}
// MARK: - Split Picker Sheet
struct SplitPickerSheet: View {
@Environment(\.managedObjectContext) private var viewContext
@Environment(\.dismiss) private var dismiss
@FetchRequest(
sortDescriptors: [
NSSortDescriptor(keyPath: \Split.order, ascending: true),
NSSortDescriptor(keyPath: \Split.name, ascending: true)
],
animation: .default
)
private var splits: FetchedResults<Split>
var body: some View {
NavigationStack {
List {
ForEach(splits, id: \.objectID) { split in
Button {
startWorkout(with: split)
} label: {
HStack {
Image(systemName: split.systemImage)
.foregroundColor(Color.color(from: split.color))
Text(split.name)
Spacer()
Text("\(split.exercisesArray.count) exercises")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
.navigationTitle("Select a Split")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
}
}
}
private func startWorkout(with split: Split) {
let workout = Workout(context: viewContext)
workout.start = Date()
workout.status = .notStarted
workout.split = split
for exercise in split.exercisesArray {
let workoutLog = WorkoutLog(context: viewContext)
workoutLog.exerciseName = exercise.name
workoutLog.date = Date()
workoutLog.order = exercise.order
workoutLog.sets = exercise.sets
workoutLog.reps = exercise.reps
workoutLog.weight = exercise.weight
workoutLog.loadType = exercise.loadType
workoutLog.duration = exercise.duration
workoutLog.status = .notStarted
workoutLog.workout = workout
}
try? viewContext.save()
// Sync to Watch
WatchConnectivityManager.shared.syncAllData()
dismiss()
}
}
#Preview {
WorkoutLogsView()
.environment(\.managedObjectContext, PersistenceController.preview.viewContext)
}